blockyard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
/* BlockYard -- dark instrument panel.
|
|
2
|
+
No webfonts, no CDN: this runs on a LAN box and must look identical with no
|
|
3
|
+
network. Numbers are monospaced and tabular so they do not jitter as they
|
|
4
|
+
change, which matters more in a live panel than typography opinions. */
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--bg: #0b0d10;
|
|
8
|
+
--bg-1: #12151a;
|
|
9
|
+
--bg-2: #171b21;
|
|
10
|
+
--bg-3: #1e232b;
|
|
11
|
+
--line: #262c36;
|
|
12
|
+
--line-soft: #1d222a;
|
|
13
|
+
--fg: #dfe5ee;
|
|
14
|
+
--fg-dim: #96a0b0;
|
|
15
|
+
--fg-faint: #6a7484;
|
|
16
|
+
--accent: #f7931a;
|
|
17
|
+
--accent-dim: #b06c10;
|
|
18
|
+
--ok: #2ecc8f;
|
|
19
|
+
--warn: #f0b429;
|
|
20
|
+
--bad: #ef5a5a;
|
|
21
|
+
--info: #58a6ff;
|
|
22
|
+
--purple: #a78bfa;
|
|
23
|
+
--cyan: #4dd0e1;
|
|
24
|
+
--pink: #f06ba7;
|
|
25
|
+
/* APPEARANCE (theme.js, 2026-09-16): every colour above and these below are rewritten on <html>
|
|
26
|
+
when a theme is chosen; the values here are the shipped dark look, which is the default theme */
|
|
27
|
+
--bg-0: #0c0e11; /* the well: inputs, bars, code */
|
|
28
|
+
--accent-ink: #17110a; /* text on an accent button */
|
|
29
|
+
--hover: rgba(255, 255, 255, 0.025);
|
|
30
|
+
--scrim: rgba(2, 5, 9, 0.62);
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
--radius: 10px;
|
|
33
|
+
--radius-sm: 6px;
|
|
34
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
35
|
+
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
36
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .28);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
* { box-sizing: border-box; }
|
|
40
|
+
html, body { height: 100%; }
|
|
41
|
+
body {
|
|
42
|
+
margin: 0;
|
|
43
|
+
background: var(--bg);
|
|
44
|
+
color: var(--fg);
|
|
45
|
+
font: 13px/1.5 var(--sans);
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
a { color: var(--info); text-decoration: none; }
|
|
50
|
+
a:hover { text-decoration: underline; }
|
|
51
|
+
|
|
52
|
+
/* ---------------------------------------------------------------- shell */
|
|
53
|
+
|
|
54
|
+
/* ONE COLUMN, NEVER WIDER THAN THE WINDOW (operator, 2026-09-11: "it's rendering off
|
|
55
|
+
the screen to the right. We need to make sure the panel fits entirely into the
|
|
56
|
+
viewspace"). An implicit grid column is as wide as its widest child's min-content,
|
|
57
|
+
and the locked, unwrappable top bar's is ~1164 px -- so under that width the whole
|
|
58
|
+
page, Block space panel included, ran off the right edge. minmax(0, 1fr) holds the
|
|
59
|
+
column to the window; the bar's nav then gives way by scrolling, as designed. */
|
|
60
|
+
.app { display: grid; grid-template-rows: auto 1fr; grid-template-columns: minmax(0, 1fr); height: 100vh; }
|
|
61
|
+
|
|
62
|
+
/* THE BAR IS ONE LINE AND DOES NOT MOVE (operator, 2026-09-11: "Look at how
|
|
63
|
+
text in upper right is being jammed up on resizing. We need to lock shit down
|
|
64
|
+
better on that line"). Every item is nowrap with a fixed footprint; readouts
|
|
65
|
+
that change (stream state, rpc latency, uptime) reserve their widest width in
|
|
66
|
+
tabular digits, so a new value cannot shove its neighbours. When the window
|
|
67
|
+
narrows, whole low-priority items drop out at fixed breakpoints (below)
|
|
68
|
+
instead of anything wrapping, and the nav is the one part allowed to give
|
|
69
|
+
way -- it scrolls sideways rather than squeezing the right-hand side. */
|
|
70
|
+
header.top {
|
|
71
|
+
display: flex; align-items: center; gap: 14px; flex-wrap: nowrap; white-space: nowrap;
|
|
72
|
+
min-width: 0; overflow: hidden; /* last resort: the bar clips, it never widens the page */
|
|
73
|
+
padding: 0 14px; height: 46px;
|
|
74
|
+
background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
|
|
75
|
+
border-bottom: 1px solid var(--line, #262c35);
|
|
76
|
+
z-index: 20;
|
|
77
|
+
}
|
|
78
|
+
.brand { display: flex; align-items: center; gap: 9px; font-weight: 650; letter-spacing: .2px; flex: 0 0 auto; }
|
|
79
|
+
/* THE MONOGRAM. The tile, its gradient and the three courses of blocks along the bottom all
|
|
80
|
+
live in the SVG itself, so the mark is one object and the favicon is the same drawing --
|
|
81
|
+
no CSS needed to make it look right, which is what a favicon cannot have anyway. */
|
|
82
|
+
.brand .mark { width: 22px; height: 22px; display: block; flex: 0 0 auto; border-radius: 6px; box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 27%, transparent); }
|
|
83
|
+
.wordmark { font-weight: 600; letter-spacing: .2px; }
|
|
84
|
+
.wordmark b { font-weight: 750; color: var(--accent); }
|
|
85
|
+
/* The brand is a <button> now (it routes to About), so the browser's button chrome has to go:
|
|
86
|
+
without this the header grows a border, a background and the UA's own font. */
|
|
87
|
+
button.brand { background: none; border: 0; padding: 0; margin: 0; color: inherit; font: inherit; cursor: pointer; text-align: left; }
|
|
88
|
+
button.brand:hover .wordmark b { color: color-mix(in srgb, var(--accent) 70%, #fff); }
|
|
89
|
+
button.brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
|
|
90
|
+
|
|
91
|
+
/* ==========================================================================
|
|
92
|
+
ABOUT (public/js/about.js). The galaxy is the page, not a decoration: this is
|
|
93
|
+
the one view with no data for it to sit behind, so the sky canvas fills the
|
|
94
|
+
panel and the text floats over it -- the same arrangement Tetrust uses.
|
|
95
|
+
========================================================================== */
|
|
96
|
+
/* the same packing every tab got (2026-09-15, "same treatment on the about page"): the sky fills the
|
|
97
|
+
window to its bottom edge, the inner air is tighter, and the GitHub link and the tip pill share a
|
|
98
|
+
row -- so the whole page, credit line included, sits on one screen with no scroll */
|
|
99
|
+
.aboutwrap { position: relative; border-radius: var(--radius); overflow: hidden; background: #000; border: 1px solid var(--line-soft); min-height: calc(100vh - 100px); }
|
|
100
|
+
.absky { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
|
101
|
+
.aboutinner { position: relative; z-index: 1; padding: 28px clamp(20px, 5vw, 64px) 22px; display: flex; flex-direction: column; gap: 18px; max-width: 1080px; margin: 0 auto; min-height: calc(100vh - 100px); box-sizing: border-box; }
|
|
102
|
+
.ablinks { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
|
|
103
|
+
.abhead { display: flex; align-items: flex-start; gap: 18px; }
|
|
104
|
+
.ablogo { width: 64px; height: 64px; flex: 0 0 auto; border-radius: 14px; box-shadow: 0 0 28px #f7931a55; }
|
|
105
|
+
.abhead h1 { margin: 0 0 6px; font-size: 34px; font-weight: 600; letter-spacing: .3px; text-shadow: 0 2px 14px #000; }
|
|
106
|
+
.abhead h1 b { font-weight: 780; color: var(--accent); }
|
|
107
|
+
/* Bright white, like the credit line (operator, 2026-09-12). The dim grey the rest of the app
|
|
108
|
+
uses for captions reads as secondary; this sentence is the page's actual statement of what
|
|
109
|
+
BlockYard is, and it sits over a galaxy, so it needs the contrast. */
|
|
110
|
+
.abhead p { margin: 0; max-width: 62ch; color: #ffffff; line-height: 1.6; text-shadow: 0 1px 8px #000; }
|
|
111
|
+
.abcols { display: grid; grid-template-columns: repeat(auto-fit, minmax(238px, 1fr)); gap: 14px; }
|
|
112
|
+
.abcard { background: rgba(8, 12, 18, .72); border: 1px solid #1d2733; border-radius: var(--radius); padding: 11px 14px; }
|
|
113
|
+
.abcard h3 { margin: 0 0 7px; font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-faint); font-weight: 600; }
|
|
114
|
+
.abrow { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 2px 0; font-family: var(--mono); font-size: 12.5px; }
|
|
115
|
+
.abrow span { color: var(--fg-dim); }
|
|
116
|
+
.abrow b { font-weight: 600; color: var(--fg); text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
117
|
+
.abgh { display: inline-flex; align-items: center; gap: 10px; align-self: flex-start; padding: 9px 15px; border-radius: 999px;
|
|
118
|
+
border: 1px solid #2a3543; background: rgba(8, 12, 18, .8); color: var(--fg); text-decoration: none; font-size: 13px; }
|
|
119
|
+
.abgh:hover { border-color: var(--accent); color: var(--accent); }
|
|
120
|
+
/* the donation address: the same pill, and the whole thing is the copy button */
|
|
121
|
+
.abdon { display: inline-flex; align-items: center; gap: 16px; align-self: flex-start; padding: 12px 18px 12px 12px; border-radius: 16px;
|
|
122
|
+
border: 1px solid #2a3543; background: rgba(8, 12, 18, .8); color: var(--fg); font: inherit; font-size: 13px; cursor: pointer; text-align: left; }
|
|
123
|
+
.abdon .abqr { flex: 0 0 auto; width: 112px; height: 112px; border-radius: 6px; image-rendering: pixelated; image-rendering: crisp-edges; background: #fff; }
|
|
124
|
+
.abdon .abdontext { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
125
|
+
.abdon .abdonlbl { display: inline-flex; align-items: center; gap: 8px; }
|
|
126
|
+
.abdon code { font-size: 12px; letter-spacing: .2px; color: var(--accent); word-break: break-all; }
|
|
127
|
+
.abdon .abdonhint { font-size: 11px; color: var(--fg-faint); }
|
|
128
|
+
.abdon:hover { border-color: var(--accent); }
|
|
129
|
+
.abdon:hover .abdonhint { color: var(--fg-dim); }
|
|
130
|
+
.abdon.done { border-color: var(--ok, #35c26f); }
|
|
131
|
+
.abdon.done .abdonhint { color: var(--ok, #35c26f); }
|
|
132
|
+
@media (max-width: 620px) { .abdon { flex-direction: column; align-items: flex-start; } }
|
|
133
|
+
/* Solid white, not the dim grey the other captions use (operator, 2026-09-12) -- it is the
|
|
134
|
+
one line on the page that is a statement rather than a reading. */
|
|
135
|
+
.abcredit { margin: auto 0 0; padding-top: 6px; color: #ffffff; font-size: 13px; font-style: italic; letter-spacing: .3px; text-shadow: 0 1px 8px #000; }
|
|
136
|
+
@media (max-width: 620px) { .abhead { flex-direction: column; gap: 12px; } .abhead h1 { font-size: 27px; } }
|
|
137
|
+
#loginCard h1 b, .login h1 b { color: var(--accent); }
|
|
138
|
+
.brand small { color: var(--fg-faint); font-weight: 500; }
|
|
139
|
+
|
|
140
|
+
nav.pages { display: flex; gap: 2px; margin-left: 6px; flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
141
|
+
nav.pages::-webkit-scrollbar { display: none; }
|
|
142
|
+
nav.pages button {
|
|
143
|
+
background: none; border: 0; color: var(--fg-dim); font: inherit; font-weight: 550;
|
|
144
|
+
padding: 6px 10px; border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap;
|
|
145
|
+
}
|
|
146
|
+
nav.pages button:hover { background: var(--bg-3, #12161c); color: var(--fg); }
|
|
147
|
+
nav.pages button.on { background: var(--bg-3, #12161c); color: var(--accent); box-shadow: inset 0 -2px 0 -1px var(--accent); }
|
|
148
|
+
|
|
149
|
+
.spacer { flex: 1 1 0; min-width: 8px; }
|
|
150
|
+
.top .meta { display: flex; align-items: center; gap: 12px; color: var(--fg-faint); font-family: var(--mono); font-size: 11px;
|
|
151
|
+
flex: 0 0 auto; white-space: nowrap; }
|
|
152
|
+
.top .meta > span { flex: 0 0 auto; white-space: nowrap; }
|
|
153
|
+
.top .meta b { color: var(--fg-dim); font-weight: 600; }
|
|
154
|
+
/* widest value each readout can take, reserved, in tabular digits */
|
|
155
|
+
#sseState { display: inline-block; min-width: 10ch; }
|
|
156
|
+
#rpcLat { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; }
|
|
157
|
+
#appUp { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; }
|
|
158
|
+
#nodePick > span { display: inline-block; max-width: 28ch; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
|
|
159
|
+
|
|
160
|
+
.userchip { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; white-space: nowrap; }
|
|
161
|
+
/* Whole items leave, in this order, rather than anything wrapping. */
|
|
162
|
+
@media (max-width: 1760px) { #whoami { display: none; } nav.pages button { padding: 6px 8px; } }
|
|
163
|
+
@media (max-width: 1500px) { .brand small { display: none; } .top .meta > span[title="Monitor uptime"] { display: none; } }
|
|
164
|
+
/* 16ch cut a real node name in half (measured at 1280: a 106px box holding 159px of text, so
|
|
165
|
+
a long node label read as "Bitcoin Core (mainn..."), and 22ch still fell 13px short of it.
|
|
166
|
+
26ch clears the longest built-in label with room to spare, and there is room to give: the RPC
|
|
167
|
+
readout is already hidden at this width and the chip still ends ~255px clear of the window edge.
|
|
168
|
+
The ellipsis and the tooltip (which names the node AND its endpoint) remain the backstop for
|
|
169
|
+
names longer than this. */
|
|
170
|
+
@media (max-width: 1280px) { .top .meta > span[title="Node RPC round trip"] { display: none; } #nodePick > span { max-width: 26ch; } }
|
|
171
|
+
|
|
172
|
+
/* THE TABS GET THEIR OWN ROW WHEN THE WINDOW IS NARROW (operator, 2026-09-11: "we need scrolling to
|
|
173
|
+
see menu/tabs that are being cut off and not displayed"). The bar is one unwrappable line, so at
|
|
174
|
+
1150 px the nav held 868 px of tabs in a 650 px box -- 218 px of them unreachable: it does scroll
|
|
175
|
+
sideways, but with the scrollbar hidden nothing said so, and a wheel scrolls the page, not the bar.
|
|
176
|
+
Under 1400 px the bar wraps and the nav takes a full row of its own (every tab fits at 1150), and
|
|
177
|
+
where it still overflows -- a phone -- its scrollbar is visible and it can be dragged.
|
|
178
|
+
|
|
179
|
+
2026-09-12: 1400 was far too low, and adding the Diversions menu made it show. Measured across
|
|
180
|
+
the inline range -- the nav wants 979 px of tabs (1031 above 1760 px, where the buttons regain
|
|
181
|
+
their padding and the user chip comes back), and the inline bar gives it 828 px at 1500, 747 at
|
|
182
|
+
1600, 841 at 1800 and 961 at 1920. So between 1400 and 2000 the last two or three tabs were past
|
|
183
|
+
the fold at EVERY width, behind a hidden scrollbar. It first genuinely fits at 2000 (1031 in
|
|
184
|
+
1031), so that is where the bar stops wrapping. */
|
|
185
|
+
@media (max-width: 1999px) {
|
|
186
|
+
header.top { flex-wrap: wrap; height: auto; padding-bottom: 6px; row-gap: 2px; }
|
|
187
|
+
nav.pages { order: 10; flex: 1 0 100%; margin-left: 0; scrollbar-width: thin; overscroll-behavior-x: contain; }
|
|
188
|
+
nav.pages::-webkit-scrollbar { display: block; height: 6px; }
|
|
189
|
+
nav.pages::-webkit-scrollbar-thumb { background: var(--bg-3, #12161c); border-radius: 6px; }
|
|
190
|
+
}
|
|
191
|
+
.userchip select, .btn {
|
|
192
|
+
background: var(--bg-3, #12161c); color: var(--fg); border: 1px solid var(--line, #262c35);
|
|
193
|
+
border-radius: var(--radius-sm); padding: 4px 8px; font: inherit; font-size: 12px; cursor: pointer;
|
|
194
|
+
}
|
|
195
|
+
.btn:hover { border-color: var(--fg-faint); }
|
|
196
|
+
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: #0000; font-weight: 650; }
|
|
197
|
+
.btn.danger { color: var(--bad); }
|
|
198
|
+
.btn:disabled { opacity: .45; cursor: not-allowed; }
|
|
199
|
+
|
|
200
|
+
/* A DEFINITE HEIGHT, so a panel may fill it (the kiosk asks for height: 100%). main is the
|
|
201
|
+
grid row .app gives it; without a height of its own, a percentage child cannot resolve and the
|
|
202
|
+
kiosk collapsed to its min-height -- a 480 px kiosk in a 782 px window (2026-09-11). */
|
|
203
|
+
main { overflow: auto; padding: 14px; height: 100%; box-sizing: border-box; scrollbar-color: var(--bg-3, #12161c) transparent; }
|
|
204
|
+
main::-webkit-scrollbar { width: 10px; }
|
|
205
|
+
main::-webkit-scrollbar-thumb { background: var(--bg-3, #12161c); border-radius: 6px; }
|
|
206
|
+
|
|
207
|
+
/* pages are hidden rather than unmounted: charts keep their buffers and the
|
|
208
|
+
active page can re-render without a rebuild */
|
|
209
|
+
.page { display: none; }
|
|
210
|
+
.page.on { display: block; }
|
|
211
|
+
|
|
212
|
+
/* ---------------------------------------------------------------- cards */
|
|
213
|
+
|
|
214
|
+
.grid { display: grid; gap: 12px; grid-template-columns: repeat(12, 1fr); align-items: start; } /* start, not stretch: a short card beside a tall one keeps its own height (2026-09-15) */
|
|
215
|
+
.card {
|
|
216
|
+
grid-column: span 4;
|
|
217
|
+
background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: var(--radius);
|
|
218
|
+
/* 9/11/8, not 12/13 (operator, 2026-09-15, after Mining, Overview and Block space one by one:
|
|
219
|
+
"same treatment on all the ... tabs"): every card packs tight, the heading closer to its body */
|
|
220
|
+
padding: 9px 11px 8px; box-shadow: var(--shadow); min-width: 0;
|
|
221
|
+
}
|
|
222
|
+
.card.w3 { grid-column: span 3; } .card.w4 { grid-column: span 4; } .card.w5 { grid-column: span 5; }
|
|
223
|
+
.card.w6 { grid-column: span 6; } .card.w7 { grid-column: span 7; } .card.w8 { grid-column: span 8; }
|
|
224
|
+
.card.w9 { grid-column: span 9; } .card.w12 { grid-column: span 12; }
|
|
225
|
+
@media (max-width: 1280px) { .card { grid-column: span 6 !important; } .card.w12 { grid-column: span 12 !important; } }
|
|
226
|
+
@media (max-width: 860px) { .card { grid-column: span 12 !important; } }
|
|
227
|
+
|
|
228
|
+
.card > h3 {
|
|
229
|
+
margin: 0 0 6px; font-size: 11px; font-weight: 700; letter-spacing: .09em;
|
|
230
|
+
text-transform: uppercase; color: var(--fg-faint);
|
|
231
|
+
display: flex; align-items: center; gap: 8px;
|
|
232
|
+
}
|
|
233
|
+
.card > h3 .src { font: 400 10px/1.2 var(--mono); text-transform: none; letter-spacing: 0; color: var(--fg-faint); opacity: .8; }
|
|
234
|
+
.card > h3 .sp { flex: 1; }
|
|
235
|
+
|
|
236
|
+
.kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; font-family: var(--mono); font-size: 12px; }
|
|
237
|
+
.kv dt { color: var(--fg-dim); }
|
|
238
|
+
.kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
|
|
239
|
+
|
|
240
|
+
.metric { font-family: var(--mono); }
|
|
241
|
+
.metric .v { font-size: 24px; font-weight: 600; letter-spacing: -.5px; font-variant-numeric: tabular-nums; }
|
|
242
|
+
.metric .u { font-size: 11px; color: var(--fg-faint); margin-left: 3px; }
|
|
243
|
+
.metric .l { font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em; color: var(--fg-faint); margin-bottom: 2px; }
|
|
244
|
+
.metric .s { font-size: 11px; color: var(--fg-dim); margin-top: 2px; font-variant-numeric: tabular-nums; }
|
|
245
|
+
|
|
246
|
+
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
|
|
247
|
+
|
|
248
|
+
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
249
|
+
.dim { color: var(--fg-dim); } .faint { color: var(--fg-faint); }
|
|
250
|
+
.ok { color: var(--ok); } .warn { color: var(--warn); } .bad { color: var(--bad); } .info { color: var(--info); }
|
|
251
|
+
.accent { color: var(--accent); } .purple { color: var(--purple); } .cyan { color: var(--cyan); }
|
|
252
|
+
.mono { font-family: var(--mono); }
|
|
253
|
+
.tiny { font-size: 11px; } .micro { font-size: 10px; }
|
|
254
|
+
|
|
255
|
+
canvas.chart { width: 100%; height: 130px; display: block; }
|
|
256
|
+
canvas.chart.tall { height: 220px; }
|
|
257
|
+
canvas.chart.short { height: 84px; }
|
|
258
|
+
|
|
259
|
+
/* ------------------------------------------------------------ sync strip */
|
|
260
|
+
|
|
261
|
+
/* One dense instrument row + a hairline bar, for every state. The header used to
|
|
262
|
+
expand to ~10 rows whenever a sync was running, pushing the charts off screen
|
|
263
|
+
at exactly the wrong moment -- and spent those same rows printing "100%" when
|
|
264
|
+
nothing was happening. Numbers are tabular so the row does not shimmer. */
|
|
265
|
+
.sync {
|
|
266
|
+
grid-column: span 12;
|
|
267
|
+
background: linear-gradient(180deg, var(--bg-2) 0%, var(--bg-1) 100%);
|
|
268
|
+
border: 1px solid var(--line, #262c35); border-radius: var(--radius);
|
|
269
|
+
padding: 8px 12px 9px; box-shadow: var(--shadow); position: relative;
|
|
270
|
+
}
|
|
271
|
+
.sync::after {
|
|
272
|
+
content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: var(--radius);
|
|
273
|
+
background: radial-gradient(140% 220% at 4% -60%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 55%);
|
|
274
|
+
}
|
|
275
|
+
.strip { display: flex; align-items: baseline; gap: 4px 13px; flex-wrap: wrap; font-family: var(--mono); font-size: 12px; position: relative; z-index: 1; }
|
|
276
|
+
.sf { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
|
|
277
|
+
.sf i { font-style: normal; font-size: 9.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-faint); }
|
|
278
|
+
.sf b { font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; letter-spacing: -.1px; }
|
|
279
|
+
.sf.node b { font-family: var(--sans); font-size: 12.5px; letter-spacing: 0; }
|
|
280
|
+
.sf + .sf::before { content: ''; display: inline-block; width: 0; }
|
|
281
|
+
.strip > .sf:not(.node) { border-left: 1px solid var(--line-soft); padding-left: 13px; }
|
|
282
|
+
.pct-big b { font-size: 15px; letter-spacing: -.4px; }
|
|
283
|
+
.sf.accent b { color: var(--accent); } .sf.ok b { color: var(--ok); } .sf.bad b { color: var(--bad); }
|
|
284
|
+
.sf.warn b { color: var(--warn); }
|
|
285
|
+
.sf.jump { background: none; border: 0; border-left: 1px solid var(--line-soft); padding: 0 0 0 13px; cursor: pointer; font: inherit; }
|
|
286
|
+
.sf.jump b { color: var(--info); text-decoration: underline dotted color-mix(in srgb, var(--info) 40%, transparent); text-underline-offset: 3px; }
|
|
287
|
+
.sf.jump:hover b { color: color-mix(in srgb, var(--info) 55%, var(--fg)); }
|
|
288
|
+
.strip-tail { margin-left: auto; display: inline-flex; align-items: center; gap: 9px; }
|
|
289
|
+
.sf.note-count { cursor: pointer; }
|
|
290
|
+
.sf.note-count b { text-decoration: underline dotted currentColor; text-underline-offset: 3px; }
|
|
291
|
+
|
|
292
|
+
.bar {
|
|
293
|
+
margin-top: 7px; height: 6px; border-radius: 4px; position: relative;
|
|
294
|
+
background: var(--bg-0); border: 1px solid var(--line, #262c35); box-shadow: inset 0 1px 2px color-mix(in srgb, var(--bg-0) 60%, #000); overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
.bar .fill {
|
|
297
|
+
position: absolute; inset: 0 auto 0 0; width: 0;
|
|
298
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
299
|
+
transition: width .6s cubic-bezier(.22, .61, .36, 1);
|
|
300
|
+
}
|
|
301
|
+
.bar .fill.done { background: linear-gradient(90deg, color-mix(in srgb, var(--ok) 60%, var(--bg)), var(--ok)); }
|
|
302
|
+
.bar .fill.stall { background: linear-gradient(90deg, color-mix(in srgb, var(--bad) 60%, var(--bg)), var(--bad)); }
|
|
303
|
+
/* the node's own difficulty-weighted estimate, as a separate marker: the two
|
|
304
|
+
percentages measure different things and are never merged into one number */
|
|
305
|
+
.bar .vp-tick { position: absolute; top: -1px; bottom: -1px; width: 2px; background: #58a6ffee; box-shadow: 0 0 6px #58a6ff99; }
|
|
306
|
+
.expanded { margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line-soft); font-size: 12px; }
|
|
307
|
+
.expanded .kv { grid-template-columns: minmax(190px, auto) 1fr; }
|
|
308
|
+
.expanded .kv dd { text-align: left; color: var(--fg); }
|
|
309
|
+
.tiny-btn { padding: 2px 8px; font-size: 11px; }
|
|
310
|
+
|
|
311
|
+
.caveat {
|
|
312
|
+
margin-top: 6px; padding: 6px 10px; border-radius: var(--radius-sm);
|
|
313
|
+
background: color-mix(in srgb, var(--warn) 9%, var(--bg-1)); border: 1px solid color-mix(in srgb, var(--warn) 32%, var(--bg-1)); color: color-mix(in srgb, var(--warn) 70%, var(--fg));
|
|
314
|
+
font-size: 11.5px; line-height: 1.45;
|
|
315
|
+
}
|
|
316
|
+
.caveat.bad { background: color-mix(in srgb, var(--bad) 9%, var(--bg-1)); border-color: color-mix(in srgb, var(--bad) 32%, var(--bg-1)); color: color-mix(in srgb, var(--bad) 65%, var(--fg)); }
|
|
317
|
+
/* Markets with polling off: the note at the top, the board / chart / depth panel folded away */
|
|
318
|
+
.caveat.mkoff { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; font-size: 12.5px; padding: 10px 12px; }
|
|
319
|
+
.mkcard.mk-off .mklayout, .mkcard.mk-off .mkchart, .mkcard.mk-off .mksub, .mkcard.mk-off #mkDepthBar, .mkcard.mk-off #mkDepthNote, .mkcard.mk-off .mkbottom { display: none; }
|
|
320
|
+
.caveat b { color: var(--fg); font-weight: 650; }
|
|
321
|
+
|
|
322
|
+
/* status badges (used across every page, not just the sync strip) */
|
|
323
|
+
.badge {
|
|
324
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 999px;
|
|
325
|
+
font: 650 10.5px/1.6 var(--sans); letter-spacing: .04em; text-transform: uppercase;
|
|
326
|
+
background: var(--bg-3, #12161c); border: 1px solid var(--line, #262c35); white-space: nowrap;
|
|
327
|
+
}
|
|
328
|
+
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 8px currentColor; }
|
|
329
|
+
.badge.ok { color: var(--ok); } .badge.sync { color: var(--accent); }
|
|
330
|
+
.badge.warn { color: var(--warn); } .badge.bad { color: var(--bad); }
|
|
331
|
+
.badge.muted { color: var(--fg-faint); } .badge.info { color: var(--info); }
|
|
332
|
+
.pulse .dot { animation: pulse 1.6s ease-in-out infinite; }
|
|
333
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .28; } }
|
|
334
|
+
|
|
335
|
+
/* --------------------------------------------------------------- tables */
|
|
336
|
+
|
|
337
|
+
table.t { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
338
|
+
table.t th {
|
|
339
|
+
text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
|
|
340
|
+
color: var(--fg-faint); font-weight: 650; padding: 5px 7px; border-bottom: 1px solid var(--line, #262c35);
|
|
341
|
+
position: sticky; top: 0; background: var(--bg-1); z-index: 1;
|
|
342
|
+
}
|
|
343
|
+
table.t td { padding: 4px 7px; border-bottom: 1px solid var(--line-soft); font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
344
|
+
table.t tr:hover td { background: var(--hover); }
|
|
345
|
+
table.t td.r { text-align: right; }
|
|
346
|
+
table.t td.w { font-family: var(--sans); }
|
|
347
|
+
|
|
348
|
+
/* POLL CADENCE: the figures sit UNDER their headings (operator, 2026-09-13: "Pool cadence data
|
|
349
|
+
needs to be centered in the table, under the header, not right justified").
|
|
350
|
+
Scoped by id on purpose. `table.t td.r` right-aligns the body cells of every table in the app,
|
|
351
|
+
and `table.t` has no `th.r` rule at all -- the other tables that right-align (.xtbl, .mktbl,
|
|
352
|
+
table.xtable, .kp-ex) each define their own, which is why only this one read as misaligned.
|
|
353
|
+
An id selector outranks `table.t td.r` without moving anybody else's columns. */
|
|
354
|
+
#ndCadence th, #ndCadence td { text-align: center; }
|
|
355
|
+
#ndCadence th:first-child, #ndCadence td:first-child { text-align: left; }
|
|
356
|
+
.scroll { max-height: 320px; overflow: auto; }
|
|
357
|
+
|
|
358
|
+
/* ------------------------------------------------------------ feed */
|
|
359
|
+
|
|
360
|
+
.feed { max-height: 340px; overflow: auto; font-family: var(--mono); font-size: 11.5px; }
|
|
361
|
+
/* Column widths that survive real content, because both original tracks were wrong.
|
|
362
|
+
- `1fr` has an automatic minimum of `min-width: auto`, so one long unbreakable token
|
|
363
|
+
(a block hash, `(run 150/438/7/73/0)`) STRETCHED the text track past the card and
|
|
364
|
+
the row spilled over the next column. `minmax(0, 1fr)` is the cap. Note
|
|
365
|
+
`word-break: break-word` does not reduce a track's min-content contribution --
|
|
366
|
+
`overflow-wrap: anywhere` is the property that does.
|
|
367
|
+
- the tag track was 66px, narrower than the kind names it must hold
|
|
368
|
+
(`peer_live_probe`, `dial_failures`, `check_problems`), so the tag overflowed into
|
|
369
|
+
the text column -- reported as "fields carry over from one column to the next".
|
|
370
|
+
Widened, and allowed to wrap rather than truncate: a clipped kind is a lost fact. */
|
|
371
|
+
.feed .row { display: grid; grid-template-columns: 74px 124px minmax(0, 1fr); gap: 8px; padding: 2.5px 0; border-bottom: 1px solid var(--hover); align-items: baseline; }
|
|
372
|
+
.feed .row .ts { color: var(--fg-faint); font-size: 10.5px; }
|
|
373
|
+
.feed .row .tag { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; opacity: .95; }
|
|
374
|
+
.feed .row .txt { color: var(--fg-dim); min-width: 0; overflow-wrap: anywhere; }
|
|
375
|
+
/* An event kind is one word and stays one line (operator, 2026-09-11: "field text getting cut
|
|
376
|
+
off and wrapped for event name" -- COLLECTOR_ERROR broke as COLLECTOR_ERRO / R in a 96 px
|
|
377
|
+
column with overflow-wrap: anywhere). 124 px holds it; anything longer is trimmed with an
|
|
378
|
+
ellipsis and named in full in its title. */
|
|
379
|
+
.feed .row .tag { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
380
|
+
.feed .row.warn .txt { color: color-mix(in srgb, var(--warn) 60%, var(--fg)); }
|
|
381
|
+
.feed .row.warn .tag { color: var(--warn); }
|
|
382
|
+
.feed .row.error .txt, .feed .row.error .tag { color: var(--bad); }
|
|
383
|
+
.feed .row.info .tag { color: var(--info); }
|
|
384
|
+
.feed .row.new { animation: flash .9s ease-out; }
|
|
385
|
+
@keyframes flash { from { background: color-mix(in srgb, var(--accent) 13%, transparent); } to { background: transparent; } }
|
|
386
|
+
|
|
387
|
+
.rowform { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
|
|
388
|
+
input.f, select.f, textarea.f {
|
|
389
|
+
background: var(--bg-0); border: 1px solid var(--line, #262c35); color: var(--fg);
|
|
390
|
+
border-radius: var(--radius-sm); padding: 5px 8px; font: inherit; font-size: 12px;
|
|
391
|
+
}
|
|
392
|
+
input.f:focus, select.f:focus, textarea.f:focus { outline: none; border-color: var(--accent-dim); }
|
|
393
|
+
textarea.f { width: 100%; min-height: 74px; font-family: var(--mono); resize: vertical; }
|
|
394
|
+
|
|
395
|
+
.note { font-size: 11.5px; color: var(--fg-dim); line-height: 1.5; }
|
|
396
|
+
.note.warn { color: color-mix(in srgb, var(--warn) 60%, var(--fg)); }
|
|
397
|
+
.hr { height: 1px; background: var(--line-soft); margin: 10px 0; }
|
|
398
|
+
|
|
399
|
+
.toast {
|
|
400
|
+
position: fixed; right: 14px; bottom: 14px; z-index: 100; display: flex; flex-direction: column; gap: 7px;
|
|
401
|
+
}
|
|
402
|
+
.toast div {
|
|
403
|
+
background: var(--bg-3, #12161c); border: 1px solid var(--line, #262c35); border-left: 3px solid var(--accent);
|
|
404
|
+
padding: 8px 12px; border-radius: var(--radius-sm); font-size: 12px; box-shadow: var(--shadow);
|
|
405
|
+
max-width: 380px; animation: slide .18s ease-out;
|
|
406
|
+
}
|
|
407
|
+
.toast div.bad { border-left-color: var(--bad); }
|
|
408
|
+
.toast div.ok { border-left-color: var(--ok); }
|
|
409
|
+
@keyframes slide { from { transform: translateY(8px); opacity: 0; } }
|
|
410
|
+
|
|
411
|
+
.offline-bar {
|
|
412
|
+
grid-column: span 12; padding: 9px 12px; border-radius: var(--radius);
|
|
413
|
+
background: color-mix(in srgb, var(--bad) 9%, var(--bg-1)); border: 1px solid color-mix(in srgb, var(--bad) 32%, var(--bg-1)); color: color-mix(in srgb, var(--bad) 65%, var(--fg)); font-size: 12px;
|
|
414
|
+
}
|
|
415
|
+
.hidden { display: none !important; }
|
|
416
|
+
|
|
417
|
+
/* ---------------------------------------------------------------- login */
|
|
418
|
+
|
|
419
|
+
.login-body { display: grid; place-items: center; height: 100vh; overflow: auto; padding: 20px; }
|
|
420
|
+
.login-card {
|
|
421
|
+
width: 100%; max-width: 372px; background: var(--bg-1); border: 1px solid var(--line, #262c35);
|
|
422
|
+
border-radius: 12px; padding: 20px 22px 18px; box-shadow: var(--shadow); /* packed (2026-09-15): 20/22 not 26 */
|
|
423
|
+
}
|
|
424
|
+
.login-card h1 { margin: 0 0 2px; font-size: 19px; letter-spacing: -.2px; }
|
|
425
|
+
.login-card p.sub { margin: 0 0 14px; color: var(--fg-faint); font-size: 12px; }
|
|
426
|
+
.login-card .note { margin-top: 10px; }
|
|
427
|
+
.field { margin-bottom: 10px; }
|
|
428
|
+
.field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--fg-faint); margin-bottom: 4px; }
|
|
429
|
+
.field input {
|
|
430
|
+
width: 100%; background: var(--bg-0); border: 1px solid var(--line, #262c35); color: var(--fg);
|
|
431
|
+
border-radius: var(--radius-sm); padding: 8px 10px; font: 14px var(--mono);
|
|
432
|
+
}
|
|
433
|
+
.field input:focus { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
434
|
+
.err { background: color-mix(in srgb, var(--bad) 9%, var(--bg-1)); border: 1px solid color-mix(in srgb, var(--bad) 32%, var(--bg-1)); color: color-mix(in srgb, var(--bad) 65%, var(--fg)); padding: 8px 10px; border-radius: var(--radius-sm); font-size: 12px; margin-bottom: 12px; }
|
|
435
|
+
.locked { background: color-mix(in srgb, var(--warn) 9%, var(--bg-1)); border-color: color-mix(in srgb, var(--warn) 32%, var(--bg-1)); color: color-mix(in srgb, var(--warn) 70%, var(--fg)); }
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
/* ----------------------------------------------------------------- extras */
|
|
439
|
+
|
|
440
|
+
.offline-bar { grid-column: span 12; padding: 9px 12px; border-radius: var(--radius);
|
|
441
|
+
background: color-mix(in srgb, var(--bad) 9%, var(--bg-1)); border: 1px solid color-mix(in srgb, var(--bad) 32%, var(--bg-1)); color: color-mix(in srgb, var(--bad) 65%, var(--fg)); font-size: 12px; }
|
|
442
|
+
.hidden { display: none !important; }
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
/* ------------------------------------------------- layout helpers, and why
|
|
446
|
+
|
|
447
|
+
These exist so that nothing in this app needs a `style="…"` attribute.
|
|
448
|
+
`style-src 'self'` refuses style attributes, and for most of this file's life the
|
|
449
|
+
policy carried `style-src-attr 'unsafe-inline'` as an allowance, because the
|
|
450
|
+
markup drew bars and progress widths that way. The allowance is now gone (it was
|
|
451
|
+
a CSP hole kept open by convenience), so:
|
|
452
|
+
|
|
453
|
+
* static widths like these became classes;
|
|
454
|
+
* data-driven widths are written through the CSSOM by app.js applyDataSizes(),
|
|
455
|
+
which reads `data-w` / `data-left` / `data-h` — CSP permits CSSOM writes, and
|
|
456
|
+
a width that goes through el.style.width cannot be an injection vector.
|
|
457
|
+
|
|
458
|
+
Putting a `style="` back will fail test/csp.test.js before it fails the browser
|
|
459
|
+
console, which is the order these things should happen in. */
|
|
460
|
+
|
|
461
|
+
.mt-5 { margin-top: 5px; }
|
|
462
|
+
.mt-6 { margin-top: 6px; }
|
|
463
|
+
.mt-8 { margin-top: 8px; }
|
|
464
|
+
.mt-14 { margin-top: 14px; }
|
|
465
|
+
.mb-8 { margin-bottom: 8px; }
|
|
466
|
+
.w-full { width: 100%; }
|
|
467
|
+
.grow { flex: 1; }
|
|
468
|
+
.minw-140 { min-width: 140px; }
|
|
469
|
+
.minw-180 { min-width: 180px; }
|
|
470
|
+
.minw-200 { min-width: 200px; }
|
|
471
|
+
|
|
472
|
+
.scroll.sm { max-height: 220px; }
|
|
473
|
+
/* the Network tab's sources table takes what the window has left under the two stacks (2026-09-15) */
|
|
474
|
+
.page[data-page="network"] #ntSources .scroll { max-height: max(200px, calc(100vh - 470px)); }
|
|
475
|
+
.scroll.md { max-height: 250px; }
|
|
476
|
+
.scroll.lg { max-height: 300px; }
|
|
477
|
+
.feed.tall { max-height: calc(100vh - 178px); } /* to the window's bottom edge (2026-09-15: "same treatment on the events tab" -- it stopped 100px short) */
|
|
478
|
+
canvas.chart.meter { height: 112px; }
|
|
479
|
+
|
|
480
|
+
pre.pre-block { margin: 6px 0 0; white-space: pre-wrap; }
|
|
481
|
+
pre.output-box {
|
|
482
|
+
max-height: 260px; overflow: auto; margin: 0; padding: 8px;
|
|
483
|
+
background: var(--bg-0); border: 1px solid var(--line, #262c35); border-radius: var(--radius-sm);
|
|
484
|
+
white-space: pre-wrap;
|
|
485
|
+
}
|
|
486
|
+
.btn.block { width: 100%; padding: 9px; }
|
|
487
|
+
.select-all { user-select: all; -webkit-user-select: all; }
|
|
488
|
+
|
|
489
|
+
/* Drill-down (a block, then its transactions): a detail pane inside a card, not a
|
|
490
|
+
new page, because "which transaction?" is asked while looking at a block. */
|
|
491
|
+
.drill { border-top: 1px dashed var(--line, #262c35); margin-top: 10px; padding-top: 10px; }
|
|
492
|
+
.drill .rowform { margin-bottom: 6px; }
|
|
493
|
+
.txid-list { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
494
|
+
.txid-list button {
|
|
495
|
+
font: 11px var(--mono); background: var(--bg-2); color: var(--fg-dim);
|
|
496
|
+
border: 1px solid var(--line, #262c35); border-radius: 4px; padding: 2px 5px; cursor: pointer;
|
|
497
|
+
}
|
|
498
|
+
.txid-list button:hover { color: var(--fg); border-color: var(--accent-dim); }
|
|
499
|
+
.kv.tight { grid-template-columns: auto 1fr; }
|
|
500
|
+
|
|
501
|
+
/* ---------------------------------------------------------- block flow */
|
|
502
|
+
/* The direction is the content: blocks leave to the right, so the mined ones sit on the
|
|
503
|
+
left, the tip in the middle, and the block that does not exist yet on the right. A
|
|
504
|
+
static grid of the same numbers would be a table, not a flow. */
|
|
505
|
+
/* The train reads as a timeline with the present at the divider: forecasts and the
|
|
506
|
+
block being assembled on the left, the confirmed chain receding right. The row
|
|
507
|
+
scrolls horizontally and the scroll starts parked AT the divider (JS sets
|
|
508
|
+
scrollLeft on the first render), so the interesting edge is what you land on and
|
|
509
|
+
dragging right walks through mined history. Cast shadows and any depth cues live
|
|
510
|
+
outside the card box; a scroll box clips at its padding edge, so the row needs
|
|
511
|
+
bottom padding or cards get their edges shaved when the row overflows. */
|
|
512
|
+
.flowwrap { overflow-x: auto; padding-bottom: 8px; }
|
|
513
|
+
/* The flow is a timeline with the present at the divider: [forecasts | being built] |
|
|
514
|
+
[tip | history receding]. The divider is ONE full-height line with an arrowhead on
|
|
515
|
+
each end and a rotated tag — the split between pending work and done work, not two
|
|
516
|
+
little arrows. The zones are flex rows; `done` gets the remaining width and its own
|
|
517
|
+
history list lives there too. */
|
|
518
|
+
.flow { display: flex; align-items: stretch; gap: 10px; padding: 4px 2px 6px; width: max-content; }
|
|
519
|
+
.flowside { display: flex; align-items: stretch; gap: 10px; min-width: 0; }
|
|
520
|
+
.flowzone-label { position: absolute; top: 0; left: 2px; font-size: 9px; letter-spacing: .09em;
|
|
521
|
+
text-transform: uppercase; color: var(--fg-faint); pointer-events: none; }
|
|
522
|
+
.flowside.todo { position: relative; flex: 0 0 auto; padding-top: 13px; }
|
|
523
|
+
.flowside.done { position: relative; flex: 1 1 auto; padding-top: 13px; overflow: hidden; }
|
|
524
|
+
.flowside.done .flowzone-label { left: auto; right: 2px; }
|
|
525
|
+
.flowsep { position: relative; flex: 0 0 26px; display: flex; flex-direction: column;
|
|
526
|
+
align-items: center; justify-content: center; gap: 4px; }
|
|
527
|
+
.flowsep-line { flex: 1 1 auto; width: 2px; border-radius: 1px;
|
|
528
|
+
background: linear-gradient(180deg, var(--accent-dim), var(--accent), var(--accent-dim)); }
|
|
529
|
+
.flowsep-arrow { flex: 0 0 auto; width: 0; height: 0;
|
|
530
|
+
border-left: 5px solid transparent; border-right: 5px solid transparent; }
|
|
531
|
+
.flowsep-arrow.up { border-bottom: 7px solid var(--accent); }
|
|
532
|
+
.flowsep-arrow.down { border-top: 7px solid var(--accent); }
|
|
533
|
+
.flowsep-tag { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-90deg);
|
|
534
|
+
transform-origin: center; font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase;
|
|
535
|
+
white-space: nowrap; color: var(--accent-dim); background: var(--bg-1); padding: 1px 6px;
|
|
536
|
+
border-radius: 3px; border: 1px solid var(--line-soft); }
|
|
537
|
+
|
|
538
|
+
.flowpast { display: flex; gap: 8px; }
|
|
539
|
+
/* The centre card is the block of the moment: wider, brighter, unmistakable. */
|
|
540
|
+
.bcard.tip { flex-basis: 158px; border-color: var(--accent); background: var(--bg-2);
|
|
541
|
+
box-shadow: 0 0 0 1px var(--accent-dim), 0 6px 18px #0006; }
|
|
542
|
+
.bcard.tip .bh b { font-size: 13px }
|
|
543
|
+
/* "latest confirmed", not "current": the block being assembled is the current activity,
|
|
544
|
+
and this card is the chain tip -- the last block the node has actually accepted. */
|
|
545
|
+
.bcard.tip::after { content: 'latest confirmed'; position: absolute; top: -8px; right: 8px; font-size: 9px;
|
|
546
|
+
letter-spacing: .08em; text-transform: uppercase; color: var(--accent); background: var(--bg-1);
|
|
547
|
+
padding: 0 4px; border-radius: 3px }
|
|
548
|
+
.bcard.pending { border-style: dashed; color: var(--fg-dim); }
|
|
549
|
+
.bcard.pending .bh b { color: var(--fg-faint) }
|
|
550
|
+
.bcard.tip.empty { color: var(--fg-dim) }
|
|
551
|
+
.bcard.next.empty { flex-basis: 210px; color: var(--fg-dim); font-size: 11.5px; line-height: 1.5; }
|
|
552
|
+
.bh { display: flex; align-items: center; gap: 5px; font-size: 12px; }
|
|
553
|
+
.bdot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex: 0 0 7px; }
|
|
554
|
+
.bpool { margin-left: auto; font-size: 11px; max-width: 78px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
555
|
+
.bgap { color: var(--fg-dim); font-size: 10.5px; margin: 2px 0 5px; }
|
|
556
|
+
.brows { display: grid; grid-template-columns: 1fr; gap: 2px; }
|
|
557
|
+
.brows > div { display: flex; justify-content: space-between; gap: 6px; font-size: 11px; }
|
|
558
|
+
.brows i { font-style: normal; color: var(--fg-dim); }
|
|
559
|
+
.brows span { font-family: var(--mono); }
|
|
560
|
+
.bfill { margin-top: 6px; height: 4px; background: var(--bg-3, #12161c); border-radius: 2px; overflow: hidden; }
|
|
561
|
+
.bfill span { display: block; height: 100%; }
|
|
562
|
+
.bnextfill { position: absolute; right: -1px; top: -1px; bottom: -1px; width: 5px;
|
|
563
|
+
background: var(--bg-3, #12161c); border-radius: 0 8px 8px 0; overflow: hidden; }
|
|
564
|
+
.bnextfill span { display: block; width: 100%; background: var(--accent); }
|
|
565
|
+
.bnextpct { margin-top: 6px; font-size: 10.5px; color: var(--accent); }
|
|
566
|
+
.live { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: 0 0 7px;
|
|
567
|
+
animation: livepulse 2.6s ease-in-out infinite; }
|
|
568
|
+
@keyframes livepulse { 0%,100% { opacity: 1 } 50% { opacity: .25 } }
|
|
569
|
+
|
|
570
|
+
/* ------------------------------------------------------ package views */
|
|
571
|
+
.pkghist { display: grid; gap: 3px; margin-bottom: 8px; }
|
|
572
|
+
.pkbar { display: grid; grid-template-columns: 46px 1fr 34px; align-items: center; gap: 6px; font-size: 11px; }
|
|
573
|
+
.pkbar span { display: block; height: 8px; background: var(--accent); opacity: .8; border-radius: 2px; }
|
|
574
|
+
.pkbar i { font-style: normal; color: var(--fg-dim); }
|
|
575
|
+
.pkbar b { font-family: var(--mono); font-weight: 500; }
|
|
576
|
+
.pkgbox { display: inline-block; width: 11px; height: 11px; border-radius: 2px; margin-right: 3px; }
|
|
577
|
+
tr.cpfp td:first-child::before { content: '↳ '; color: var(--accent); }
|
|
578
|
+
|
|
579
|
+
@media (prefers-reduced-motion: reduce) { .live { animation: none } }
|
|
580
|
+
@media (prefers-reduced-motion: reduce) { .rail, .flow.shift .flowpast, .bcard.arrive { animation: none } .flowpast, .bnextfill span { transition: none } }
|
|
581
|
+
|
|
582
|
+
/* ------------------------------------------------- the pipeline in motion */
|
|
583
|
+
/* Three motions, and each one is a fact rather than decoration -- see the comment in
|
|
584
|
+
public/js/mining.js. The rail speed comes from the measured average block gap, the row
|
|
585
|
+
steps only when a height actually arrives, and the next-block fill eases toward the
|
|
586
|
+
weight the node reported. prefers-reduced-motion keeps every number and removes all of
|
|
587
|
+
the movement. */
|
|
588
|
+
.rail { height: 3px; margin: 0 0 5px; border-radius: 2px; overflow: hidden;
|
|
589
|
+
background: repeating-linear-gradient(90deg, var(--accent-dim) 0 10px, transparent 10px 26px);
|
|
590
|
+
background-size: 26px 100%; animation: railmove var(--rail-dur, 60s) linear infinite; opacity: .75; }
|
|
591
|
+
.rail span { display: block; height: 100%; }
|
|
592
|
+
@keyframes railmove { from { background-position: 0 0 } to { background-position: 26px 0 } }
|
|
593
|
+
|
|
594
|
+
.flowpast { transition: transform .6s cubic-bezier(.22, .61, .36, 1); }
|
|
595
|
+
/* History recedes to the right: at the moment of arrival the row is drawn one slot to the
|
|
596
|
+
left of where it settles, so the eye reads "everything moved right by one". */
|
|
597
|
+
.flow.shift .flowpast { animation: recede .8s cubic-bezier(.22, .61, .36, 1); }
|
|
598
|
+
@keyframes recede { 0% { transform: translateX(-136px); opacity: .35 } 55% { opacity: 1 } 100% { transform: translateX(0); opacity: 1 } }
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
.bnextfill span { transition: height 1.1s cubic-bezier(.22, .61, .36, 1); }
|
|
602
|
+
|
|
603
|
+
@media (prefers-reduced-motion: reduce) {
|
|
604
|
+
.rail, .flow.shift .flowpast, .bcard.arrive { animation: none }
|
|
605
|
+
.flowpast, .bnextfill span { transition: none }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* ------------------------------------------------- tip freshness, the card rail, density */
|
|
609
|
+
/* The colour of the centre card is the most important colour on this page, so it is the
|
|
610
|
+
one thing that must never be decorative: green means "found within the last four
|
|
611
|
+
minutes", amber means "one block late", red means "two blocks gone and one is due".
|
|
612
|
+
tipFreshness() refuses to answer at all during initial download, while the node is
|
|
613
|
+
not answering, or while updates are paused -- a red card that means nothing teaches
|
|
614
|
+
people to ignore the colour that matters. */
|
|
615
|
+
.bcard.tip.fresh { border-color: var(--ok); box-shadow: 0 0 0 1px var(--ok), 0 0 14px #2ecc8f33, 0 1px 0 rgba(235,245,255,.06) inset, 0 -10px 18px rgba(0,0,0,.3) inset; }
|
|
616
|
+
/* The tab says what the card IS; the colour says how it is doing. Replacing the label
|
|
617
|
+
with a verdict hid the thing being labelled. */
|
|
618
|
+
.bcard.tip.fresh::after { content: 'current tip'; color: var(--ok) }
|
|
619
|
+
.bcard.tip.late { background: #f0b4291f; border-color: var(--warn); box-shadow: 0 0 0 1px var(--warn), 0 1px 0 rgba(235,245,255,.06) inset, 0 -10px 18px rgba(0,0,0,.3) inset; }
|
|
620
|
+
.bcard.tip.late::after { content: 'current tip'; color: var(--warn) }
|
|
621
|
+
.bcard.tip.overdue { background: #ef5a5a26; border-color: var(--bad); animation: due 2.4s ease-in-out infinite; box-shadow: 0 0 0 1px var(--bad), 0 1px 0 rgba(235,245,255,.06) inset, 0 -10px 18px rgba(0,0,0,.3) inset; }
|
|
622
|
+
.bcard.tip.overdue::after { content: 'current tip — a block is due'; color: var(--bad) }
|
|
623
|
+
@keyframes due { 0%,100% { box-shadow: 0 0 0 1px var(--bad) } 50% { box-shadow: 0 0 0 1px var(--bad), 0 0 16px #ef5a5a66 } }
|
|
624
|
+
|
|
625
|
+
.tiplegend { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 6px; font-size: 10.5px; }
|
|
626
|
+
.tl { padding: 1px 6px; border-radius: 3px; border: 1px solid var(--line-soft); color: var(--fg-dim) }
|
|
627
|
+
.tl.fresh { border-color: var(--ok); color: var(--ok) }
|
|
628
|
+
.tl.late { background: #f0b4291f; border-color: var(--warn); color: var(--warn) }
|
|
629
|
+
.tl.overdue { background: #ef5a5a26; border-color: var(--bad); color: var(--bad) }
|
|
630
|
+
.tl.na { color: var(--fg-faint) }
|
|
631
|
+
.tl.now { border-style: dashed }
|
|
632
|
+
|
|
633
|
+
/* The pool identity has to survive the trip to the right, or every card after the tip
|
|
634
|
+
becomes anonymous and the train stops telling you who is mining. */
|
|
635
|
+
.bcard { border-left: 3px solid var(--pool, var(--line-soft)); }
|
|
636
|
+
/* A block nobody is assembling yet: dashed, grey, and saying so in its own border. */
|
|
637
|
+
.bcard.ghost { border-style: dashed; border-color: var(--line-soft); background: transparent; opacity: .82 }
|
|
638
|
+
.bcard.ghost .bh b { color: var(--fg-faint) }
|
|
639
|
+
.chips { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 5px }
|
|
640
|
+
.chip { font-size: 9.5px; padding: 1px 5px; border-radius: 3px; border: 1px solid var(--line-soft); color: var(--fg-dim); font-variant-numeric: tabular-nums }
|
|
641
|
+
.chip.hot { border-color: var(--accent); color: var(--accent) }
|
|
642
|
+
.chip.warn { border-color: var(--warn); color: var(--warn) }
|
|
643
|
+
.chip.faint { color: var(--fg-faint) }
|
|
644
|
+
.backlog { margin-top: 4px; height: 3px; background: var(--bg-3); border-radius: 2px; overflow: hidden }
|
|
645
|
+
.backlog span { display: block; height: 100%; background: var(--accent) }
|
|
646
|
+
.bcard.unlabelled { border-left-color: var(--line-soft) }
|
|
647
|
+
|
|
648
|
+
/* Cards GROW to fill the row (measured: seven fixed 156 px cards in a 1,369 px card left
|
|
649
|
+
a third of the panel empty and pushed the row to 2,236 px of scroll). A full row now
|
|
650
|
+
means a full train; only a train longer than the panel scrolls. */
|
|
651
|
+
/* Shrink AND grow: a fixed basis larger than basis×count fits means the row overflows
|
|
652
|
+
and never grows — measured 8 cards at 156 px basis in a 1,369 px card = 1,792 px of
|
|
653
|
+
scroll, the panel half-empty of look yet technically overflowing. Cards now start at
|
|
654
|
+
132 px, may tighten to 116 px, and expand to fill, so the usual train exactly fills
|
|
655
|
+
the panel with no scrollbar and no dead column. */
|
|
656
|
+
.bcard { flex: 0 0 auto; width: 178px; min-width: 0; }
|
|
657
|
+
/* A block that does not exist yet gets a sliver, not a card: three 156 px ghost cards
|
|
658
|
+
used to OPEN the train, so the left third of the panel was dashed boxes reading
|
|
659
|
+
"not yet assembled" and the real data started at x=500 px. */
|
|
660
|
+
.bcard.ghost.sliver { width: 96px; font-size: 10.5px }
|
|
661
|
+
.bcard.older { width: auto; }
|
|
662
|
+
canvas#gnTreemap { width: 100%; height: auto; aspect-ratio: 1 / 1; display: block }
|
|
663
|
+
/* Same square for the mempool map: the two maps are meant to be compared side by side, so
|
|
664
|
+
they get the same shape, not a square and a banner. */
|
|
665
|
+
/* The .treemapwrap.sq rules above own this canvas's box (absolute, inset 0),
|
|
666
|
+
so an aspect-ratio here would only fight them. */
|
|
667
|
+
canvas#gnMempoolTreemap { display: block }
|
|
668
|
+
/* A treemap's whole claim is area, and area only reads when the box is sized to
|
|
669
|
+
what the picture shows: at w4/168px a half-full block was an 84px band of
|
|
670
|
+
transactions in a dead square (measured 2026-09-10). The box is therefore w8
|
|
671
|
+
wide and its height follows the FILL, set by mining.js against the template's
|
|
672
|
+
own weightPct (floor 240px so an empty block still shows its hatch); the
|
|
673
|
+
height:240px base above stays the placeholder/no-template case. Same data and
|
|
674
|
+
rules as the Mining page's map — the note above the Overview copy promised —
|
|
675
|
+
at the same 433px-scale square when a block is actually full. */
|
|
676
|
+
/* the .treemapwrap.sq rules own this canvas's box too */
|
|
677
|
+
canvas#ovGnTreemap { display: block }
|
|
678
|
+
.bcard.tip { flex-basis: 168px }
|
|
679
|
+
.delt { font-style: normal; color: var(--fg-faint); font-size: 10px }
|
|
680
|
+
.bwhen { font-size: 10.5px; color: var(--fg-dim); margin: 3px 0 5px; font-variant-numeric: tabular-nums }
|
|
681
|
+
.bwhen.longgap { color: var(--warn) }
|
|
682
|
+
/* columns sized by their content, so a wider font makes the cell wider rather than clipping the number */
|
|
683
|
+
.bgrid { display: grid; grid-template-columns: auto auto; justify-content: space-between; gap: 1px 6px }
|
|
684
|
+
.bgrid > div { display: flex; justify-content: space-between; gap: 4px; font-size: 10.5px }
|
|
685
|
+
.bgrid i { font-style: normal; color: var(--fg-faint) }
|
|
686
|
+
.bgrid span { font-family: var(--mono); font-variant-numeric: tabular-nums }
|
|
687
|
+
.bcap { font-size: 9.5px; color: var(--fg-faint); margin-top: 3px; font-variant-numeric: tabular-nums }
|
|
688
|
+
/* THE MINER'S PLATE (operator, 2026-09-12: "we really need to move the pool name out of the block
|
|
689
|
+
info, and display it as a plaque underneath the block like the mempool space app labels
|
|
690
|
+
things"). It used to share the header row with the height, capped at 78px, which clipped
|
|
691
|
+
"Foundry USA" to "Foundry U…" -- the one fact on the card people scan for, truncated to make
|
|
692
|
+
room for a number that was already the biggest thing on the card. Full width under the block,
|
|
693
|
+
bled to the card's edges, so it reads as a label ON the block rather than another statistic. */
|
|
694
|
+
/* A PILL, not a slab (operator, 2026-09-12: "Wtf is that unreadable shit ? Stuff it into a pill or
|
|
695
|
+
something, that is clearly visible"). The first cut carried data-pool, and applyMiningStyles
|
|
696
|
+
paints a BACKGROUND on any [data-pool] element that is not a .bcard -- so the plate came out as
|
|
697
|
+
a solid slab of the pool's colour with the pool's colour written on it. Unreadable, and entirely
|
|
698
|
+
self-inflicted. The pill takes no background from the style pass: only its TEXT is coloured,
|
|
699
|
+
through [data-pool-fg], over the card's own dark fill. */
|
|
700
|
+
.bplaque { margin: 7px 0 0; text-align: center; min-width: 0; }
|
|
701
|
+
.bplaque .pill { display: inline-block; max-width: 100%; padding: 2px 9px; border-radius: 999px;
|
|
702
|
+
background: var(--bg-3, #12161c); border: 1px solid var(--line, #262c35);
|
|
703
|
+
font-size: 9.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
|
|
704
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
|
|
705
|
+
/* a pool we could not name, and a coinbase not read yet, say so differently */
|
|
706
|
+
.bplaque .pill.raw { font-family: var(--mono); text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--fg-dim); }
|
|
707
|
+
.bplaque .pill.waiting { color: var(--fg-faint); font-weight: 500; border-style: dashed; }
|
|
708
|
+
|
|
709
|
+
@media (prefers-reduced-motion: reduce) { .bcard.tip.overdue { animation: none } }
|
|
710
|
+
|
|
711
|
+
/* ---------------------------------------------------------- block treemap */
|
|
712
|
+
.treemapwrap { position: relative }
|
|
713
|
+
canvas.treemap { width: 100%; height: 240px; display: block; background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: 4px }
|
|
714
|
+
/* The full-panel map: a square whose side is the card's own width, so "fits the entire
|
|
715
|
+
panel" is geometry the browser enforces, not a height we guessed. */
|
|
716
|
+
.treemapwrap.sq { aspect-ratio: 1 / 1 }
|
|
717
|
+
|
|
718
|
+
/* 2026-09-10, operator: "I want the panel and viewport a square". The WRAPPER
|
|
719
|
+
was already square, but the CARD around it was not -- its height is the
|
|
720
|
+
title plus the square canvas plus the note, so the panel read as a portrait
|
|
721
|
+
rectangle. The card is now square itself, and the canvas is a square sized
|
|
722
|
+
by whatever height is left after the chrome, centred in it. Both are square;
|
|
723
|
+
the canvas is simply smaller than the card's full width, which is the only
|
|
724
|
+
way both can be true at once. */
|
|
725
|
+
/* 2026-09-11: the card is as tall as its contents. It was forced square with
|
|
726
|
+
overflow hidden, but a square card cannot hold a square board PLUS its title
|
|
727
|
+
and note, so the card cut the bottom of the board off (operator: "Blocks are
|
|
728
|
+
being shown off the viewport. I want the grid bounds flush with the
|
|
729
|
+
viewport"). The BOARD is the square; the card around it is not. */
|
|
730
|
+
.card:has(> .treemapwrap.sq) { display: flex; flex-direction: column; }
|
|
731
|
+
/* OVERVIEW ORDER (2026-09-11). The operator asked, of Block flow, "Takes up way
|
|
732
|
+
too much vertical space. We can easily compress this vertically", and gave
|
|
733
|
+
the Block space viewer its own page (#space) "so it can run in a large
|
|
734
|
+
window". Block flow used to be stretched to the height of the Block space
|
|
735
|
+
square beside it (2026-09-10, "equal heights"), and every card inside it
|
|
736
|
+
stretched with it -- that rule IS the vertical space, so it is gone rather
|
|
737
|
+
than overridden. Row 1 is Block flow, full width, at its content height;
|
|
738
|
+
row 2 is the small Block space square with Mempool and Fees, 3 + 5 + 4.
|
|
739
|
+
`order` stays NEGATIVE: order defaults to 0, and these four must sort ahead
|
|
740
|
+
of every untouched card (Mined by, Last blocks, Monitor events). The DOM
|
|
741
|
+
order is Mempool, Fees, Block space, Block flow; CSS does the rearranging. */
|
|
742
|
+
/* ...and the same day, at the operator's width (<= 1280 px, where the global
|
|
743
|
+
rule above forces every card to half width), the square landed beside a
|
|
744
|
+
Block flow stretched to its height: "Look at all this wasted space on the
|
|
745
|
+
overview. Block flow panel needs to be vertically reduced. Surely we can fit
|
|
746
|
+
more info in that view. Keep the block space size as is." So the square
|
|
747
|
+
keeps the right half and spans four rows, and the left half is a stack:
|
|
748
|
+
Block flow at its content height, Mempool, Fees, and Last blocks, which is a
|
|
749
|
+
scrolling table and takes whatever height the square leaves (the 1fr row)
|
|
750
|
+
-- the spare room shows more history instead of nothing. Placement is
|
|
751
|
+
explicit and !important, because the global half-width rule is. */
|
|
752
|
+
/* SUPERSEDED the same day by two independent stacks (.ovcols, below): rows of
|
|
753
|
+
one grid coupled the columns, so the taller side padded the other ("Still
|
|
754
|
+
too much empty space here. Need to compress the mempool and fees view at the
|
|
755
|
+
very least to sit side-by-side"). Left: Block flow, Mempool | Fees, Last
|
|
756
|
+
blocks. Right: Block space, Monitor events. The last card of each column
|
|
757
|
+
takes the height that is left. */
|
|
758
|
+
[data-sync-hero="overview"] { grid-column: 1 / -1; }
|
|
759
|
+
.ovcols { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; align-items: stretch; }
|
|
760
|
+
/* the Mempool page's stacks: the Block space viewer's column is the narrower
|
|
761
|
+
one there ("in a smaller panel"), so the charts keep the width */
|
|
762
|
+
/* 4fr/1fr (operator: "we can make the block space window 30% smaller if we can fit more panels onto
|
|
763
|
+
one display"). The viewer is square, so its COLUMN WIDTH IS ITS HEIGHT, which is what makes it
|
|
764
|
+
the lever for fitting the page on one screen: at 2000px this puts it near 396px against the 566
|
|
765
|
+
it had at 5fr/2fr -- 30% down, as asked -- and gives each of the six charts ~784px.
|
|
766
|
+
The sum then lands around 810px: three chart rows (~600), the strip of absences (~103), and the
|
|
767
|
+
gaps. That is inside an 832px viewport, so the page stops scrolling. */
|
|
768
|
+
/* The floor is 300px, not 360. Measured: 360 starved the charts at mid widths -- a 1280px window
|
|
769
|
+
stopped fitting on one screen (1000px tall against 832) and the figures fell back to a single
|
|
770
|
+
column. The control strip wraps now, so the viewer no longer needs a floor wide enough to hold it
|
|
771
|
+
in one line; it only needs enough to stay a usable picture. */
|
|
772
|
+
/* Three columns: the figures and the fee histogram, the time series, and the viewer with the age
|
|
773
|
+
histogram tucked under it. The last is narrowest because the viewer is square -- its width is its
|
|
774
|
+
height, which is the lever for keeping the page on one screen. */
|
|
775
|
+
/* THE FEE HISTOGRAM FILLS THE COLUMN IT SITS IN (operator, repeatedly: "too much wasted space",
|
|
776
|
+
and the void under this card survived every other change). Measured at 2000px: its column held
|
|
777
|
+
385px against a 629px neighbour, so ~240px went spare -- while this chart, the one that most
|
|
778
|
+
rewards height, was a single spike squeezed into the shared 130px default. It is the only chart
|
|
779
|
+
given its own height, because it is the only one with a column to fill.
|
|
780
|
+
`clamp` rather than a flat value: at narrower windows the figures above it fall back to one
|
|
781
|
+
column and grow tall by themselves, and a fixed 300px would then push the page off the screen. */
|
|
782
|
+
#mpHistChart { height: clamp(130px, 26vh, 300px); }
|
|
783
|
+
|
|
784
|
+
.mpcols { grid-column: 1 / -1; display: grid;
|
|
785
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
|
|
786
|
+
gap: 12px; align-items: start; }
|
|
787
|
+
.mpcols .card { grid-column: auto !important; }
|
|
788
|
+
/* The gauge is a glance, not a chart: 190px of it left the figures beside it 317px to fit two
|
|
789
|
+
label/value columns that need ~330, which is what squeezed them into each other. */
|
|
790
|
+
.mpusage { display: grid; grid-template-columns: minmax(96px, 120px) minmax(0, 1fr); gap: 12px; align-items: center;
|
|
791
|
+
container-type: inline-size; }
|
|
792
|
+
/* THE POOL FIGURES FLOW ACROSS, they do not stack (operator, 2026-09-12: "yeah, that's a huge
|
|
793
|
+
waste of space"). `.kv` is one label-left / value-right pair per row, which is right in a narrow
|
|
794
|
+
card and absurd in a wide one: measured at 1600px the labels sat at x230 and the values at x930
|
|
795
|
+
with ~600px of nothing between them, and twelve rows stood the card 301px tall. Pairs flow into
|
|
796
|
+
three columns instead, so the card is short and the middle is not a void.
|
|
797
|
+
Scoped to this card on purpose -- `.kv` is used on eight other pages, and restyling all of them
|
|
798
|
+
from one screenshot of one page is how a fix becomes three regressions. */
|
|
799
|
+
/* `max-content`, NOT `minmax(0, 1fr)`. A 1fr track with a 0 minimum may shrink BELOW its own text,
|
|
800
|
+
and because `.kv dd` is right-aligned the overflow runs rightwards straight over the next
|
|
801
|
+
column's label: "27,947ialized", "0.02271fee", "100,000l vsize". Measured against the card it
|
|
802
|
+
looked fine -- the list stays inside its box, it is the cells that overlap each other -- which is
|
|
803
|
+
why the first check missed it and the operator's screenshot did not. A value column is exactly as
|
|
804
|
+
wide as the longest value in it, and nothing wraps. */
|
|
805
|
+
/* MEASURED AGAINST THE CARD, not the window (operator, 2026-09-12: "text coming off panel" -- the
|
|
806
|
+
second column of figures ran past the card's right edge and the next card painted over it:
|
|
807
|
+
"serialize", "min fee r", "unbroadca").
|
|
808
|
+
The column count was keyed off @media, i.e. the VIEWPORT, and the viewport does not determine how
|
|
809
|
+
wide this card ends up -- the pair ratio and the outer 3fr/2fr split do. So every breakpoint was a
|
|
810
|
+
guess that was wrong at some window width, and I got it wrong twice. A container query asks the
|
|
811
|
+
only thing that matters: how much room does THIS card actually have.
|
|
812
|
+
It fails safe. One pair column is the default and can never overflow; two and three are opt-ins
|
|
813
|
+
that require the measured space (a pair runs to ~194px at its longest -- "OP_RETURN max" against
|
|
814
|
+
"2,237,214 sat" -- so two need ~410px beside a 132px gauge). A browser without container queries
|
|
815
|
+
simply keeps the single column. */
|
|
816
|
+
/* `justify-content: start` is what closes the gulf. A grid `auto` track absorbs leftover space, so
|
|
817
|
+
the label column stretched to fill the card and threw the value against the far right edge --
|
|
818
|
+
which is the "huge waste of space" in the first place, and it came back the moment the figures
|
|
819
|
+
dropped to a single column. Content-sized tracks, leftover space left over. */
|
|
820
|
+
.mpusage dl.kv { margin: 0; grid-template-columns: auto max-content; column-gap: 22px; justify-content: start; }
|
|
821
|
+
.mpusage dl.kv dt, .mpusage dl.kv dd { white-space: nowrap; }
|
|
822
|
+
/* The container is `.mpusage` -- the grid INSIDE the card -- so it measures about 26px narrower
|
|
823
|
+
than the card, and these thresholds are its width, not the card's. Two pairs need ~410px beside
|
|
824
|
+
the 132px gauge plus the 12px gap: 554. Three need ~626px of pairs: 770. The first cut used 600
|
|
825
|
+
and 880, which quietly dropped a 1600px-wide window back to a single column -- the tall card with
|
|
826
|
+
a gulf down the middle. */
|
|
827
|
+
/* 570, not 540. Measured: a 548px container with two columns puts "6.2 MB", "300.0 MB" and
|
|
828
|
+
"0.10 sat/vB" past the card's right edge -- two columns need ~554 (410 of pairs, a 132 gauge, a
|
|
829
|
+
12 gap), so 540 was six pixels of wishful thinking. The margin is deliberate: these are live
|
|
830
|
+
figures and "2,237,214 sat" is wider than "0". */
|
|
831
|
+
@container (min-width: 560px) { .mpusage dl.kv { grid-template-columns: repeat(2, auto max-content); } }
|
|
832
|
+
@container (min-width: 790px) { .mpusage dl.kv { grid-template-columns: repeat(3, auto max-content); } }
|
|
833
|
+
|
|
834
|
+
/* Pool usage and Mempool size share a row inside the left stack. TWO columns of figures, not
|
|
835
|
+
three: in this pair the usage card is ~556px rather than the 927px it had when it sat alone, and
|
|
836
|
+
three columns would overflow it. The count is part of THIS change for that reason -- the earlier
|
|
837
|
+
media queries keyed off the viewport, which says nothing about how wide the card ended up. */
|
|
838
|
+
/* EVEN, and stretched to the same height (operator, 2026-09-12: "match heights with mempool size,
|
|
839
|
+
and shrink up pool usage more horizontally. We have more room"). At 2fr/1fr the figures card took
|
|
840
|
+
792px to show one column with ~300px of dead middle while the chart beside it got 396. Halved,
|
|
841
|
+
each is ~594: the figures fit two columns there, which also drops the card to ~175px against the
|
|
842
|
+
chart's ~182 -- and `stretch` makes them exactly equal rather than nearly. */
|
|
843
|
+
|
|
844
|
+
/* WHERE TWO COLUMNS OF FIGURES STOP FITTING, measured rather than guessed. The longest pairs are
|
|
845
|
+
about 194px each ("OP_RETURN max" + "2,237,214 sat"), so two of them need ~410px; with the gauge
|
|
846
|
+
and the card's padding that is a ~578px card, which this layout only reaches above ~1500px of
|
|
847
|
+
viewport. Below it the figures go back to one column -- a taller card is fine, a card whose
|
|
848
|
+
contents hang out of its own right edge is not. The first cut broke at 1240 and overflowed by
|
|
849
|
+
145px at 1280. */
|
|
850
|
+
/* The PAIR still stacks on a narrow window -- that is a viewport question, unlike the column count
|
|
851
|
+
above, which is a question about the card. */
|
|
852
|
+
/* `.mppair.even` outranks a bare `.mppair`, so the two fee charts would have stayed side by side at
|
|
853
|
+
any width without naming it here -- fine at 1000px, not on a phone. */
|
|
854
|
+
/* PAIRING CAN STARVE THE CARD IT PAIRS. Measured across widths, the usage container is NOT
|
|
855
|
+
monotonic in the viewport: at 1280 the pair is side by side and the figures get 454px -- narrower
|
|
856
|
+
than the 548px they get at 1000px, where the pair is stacked -- so 1280 fell back to one column
|
|
857
|
+
and a 301px card while 1000px managed a 175px one. Below ~1500 the usage card is worth more as a
|
|
858
|
+
full-width row (~750-820px, which buys two or three columns) than as half of one. */
|
|
859
|
+
/* An even split only leaves each half wide enough for two columns of figures above ~1980px. Below
|
|
860
|
+
that the figures are worth more as a full-width row -- around 950px, which buys THREE columns and
|
|
861
|
+
an even shorter card -- than as a starved half. The `.even` pair (two charts) has no such floor
|
|
862
|
+
and stays side by side much further down. */
|
|
863
|
+
/* the two distributions share a row inside the right column */
|
|
864
|
+
.mprow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; align-items: start; }
|
|
865
|
+
.mprow .card { grid-column: auto !important; }
|
|
866
|
+
/* the same packing every tab got (2026-09-15: "same treatment on the mempool tab"): the page already
|
|
867
|
+
fit one screen with room over, so the room goes to the charts -- the four time series and the
|
|
868
|
+
scatter a fifth taller -- and the note cards under them sit tight to their one line */
|
|
869
|
+
.page[data-page="mempool"] canvas.chart:not(.tall):not(.short):not(.meter) { height: 156px; }
|
|
870
|
+
.page[data-page="mempool"] .mpusage { gap: 8px; }
|
|
871
|
+
.page[data-page="mempool"] #mpAcceptCard .note, .page[data-page="mempool"] #mpOrphansCard .note { margin-top: 2px; }
|
|
872
|
+
|
|
873
|
+
/* two columns down to 900px (it stacked from 1100, which put a 1093px window -- the operator's -- on one
|
|
874
|
+
long column); the two distributions share their row down to 760 */
|
|
875
|
+
@media (max-width: 900px) { .mpcols { grid-template-columns: minmax(0, 1fr); } }
|
|
876
|
+
@media (max-width: 760px) { .mprow { grid-template-columns: minmax(0, 1fr); } }
|
|
877
|
+
|
|
878
|
+
/* A LOG-DERIVED PANEL ON A MONITOR WITH NO LOG is one sentence, and it should not hold a 130px
|
|
879
|
+
empty chart open underneath it. Measured: two such cards at 187px each, for two lines of text.
|
|
880
|
+
panels.js marks the card when the log source is off; the card then sizes to what it actually
|
|
881
|
+
has to say. */
|
|
882
|
+
.card.lognone canvas.chart { display: none; }
|
|
883
|
+
.card.lognone { align-self: start; }
|
|
884
|
+
@media (max-width: 860px) { .mpcols, .mpusage { grid-template-columns: minmax(0, 1fr); } }
|
|
885
|
+
.ovcol { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
|
886
|
+
.ovpair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
|
|
887
|
+
.ovcols .card { grid-column: auto !important; }
|
|
888
|
+
/* contain: size so the table's own rows do NOT size the row: measured at
|
|
889
|
+
1280 px, without it Last blocks came out 456 px, the left stack 1,278 px, and
|
|
890
|
+
the Block space card stretched to match with ~650 px of nothing under its
|
|
891
|
+
592 px board -- the wasted space, moved to the other side. Contained, the
|
|
892
|
+
card takes exactly what the square leaves (the 1fr row), never under 220 px,
|
|
893
|
+
and its table scrolls inside that. */
|
|
894
|
+
#ovBlocksCard, #ovEventsCard { flex: 1 1 0; display: flex; flex-direction: column; contain: size; min-height: 200px; }
|
|
895
|
+
#ovBlocksCard > .scroll, #ovEventsCard > .feed { flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; }
|
|
896
|
+
|
|
897
|
+
/* ...and the lower pair is shrunk so the four fit a screen together. Their
|
|
898
|
+
charts carry a trend, not a reading, so they lose height first. */
|
|
899
|
+
#ovMempoolCard canvas.chart, #ovFeesCard canvas.chart { height: 64px; }
|
|
900
|
+
#ovMempoolCard, #ovFeesCard { padding: 9px 11px; }
|
|
901
|
+
#ovMempoolCard .metric .v { font-size: 1.35rem; }
|
|
902
|
+
|
|
903
|
+
@media (max-width: 860px) {
|
|
904
|
+
.ovcols, .ovpair { grid-template-columns: minmax(0, 1fr); }
|
|
905
|
+
}
|
|
906
|
+
/* a label never breaks mid-card ("fees ₿" was wrapping its unit onto a line of its own) */
|
|
907
|
+
.bgrid i { white-space: nowrap; }
|
|
908
|
+
|
|
909
|
+
.card:has(> .treemapwrap.sq) > h3 { flex: 0 0 auto; }
|
|
910
|
+
/* THE VIEWER'S HEADER CARRIES CONTROLS, so it must be allowed to reflow (operator, 2026-09-12:
|
|
911
|
+
"Now block space has text running off the screen"). Measured: the mode toggle (132px), the
|
|
912
|
+
refresh countdown (113) and the refresh button (84) sit INSIDE the h3 -- 346px of controls, plus
|
|
913
|
+
a 132px caption and the title, against a 392px card. `.card > h3` is a flex row with no wrapping,
|
|
914
|
+
so it clipped, and "refresh now" hung 42px past the card's edge.
|
|
915
|
+
The caption goes first: on a viewer card it is the least useful thing in the row, and it buys
|
|
916
|
+
back more than the countdown costs. Wrapping then guarantees the rest can never hang out, at any
|
|
917
|
+
width, whatever the controls grow into. */
|
|
918
|
+
.card:has(> .treemapwrap.sq) > h3 { flex-wrap: wrap; row-gap: 5px; }
|
|
919
|
+
.card:has(> .treemapwrap.sq) > h3 .src { display: none; }
|
|
920
|
+
/* ...and the control strip has to wrap INSIDE itself too. Wrapping the h3 can only move a whole
|
|
921
|
+
child, and `.viewer-ctl` is one 346px child: at a 360px card its content box is ~334px, so the
|
|
922
|
+
strip was still 12px too wide sitting alone on its own line. Its three parts (the mode toggle,
|
|
923
|
+
the countdown, the button) wrap independently now, so there is no width at which this clips. */
|
|
924
|
+
/* `flex: 0 1 auto` is the operative part -- `flex-wrap` alone did nothing. `.viewer-ctl.in-head`
|
|
925
|
+
ships `flex: 0 0 auto`, i.e. shrink DISABLED, so the strip always sized itself to its content
|
|
926
|
+
(346px, measured) and the header could never squeeze it; a flex container only wraps when it is
|
|
927
|
+
narrower than its contents, so wrap had nothing to act on and `min-width: 0` was moot with
|
|
928
|
+
shrinking switched off. Allowed to shrink, the header narrows it and its three parts wrap. */
|
|
929
|
+
.card:has(> .treemapwrap.sq) > h3 .viewer-ctl { flex: 0 1 auto; flex-wrap: wrap; row-gap: 4px; min-width: 0; }
|
|
930
|
+
/* ...and on a NARROW viewer card the countdown goes, rather than the row becoming three rows.
|
|
931
|
+
Measured at a 358px card: the strip wrapped into mode / countdown / button on separate lines and
|
|
932
|
+
stood the header 71px tall, pushing the picture down. Wrapping is not overflow, so the spill
|
|
933
|
+
check called it clean -- it was not. Without the countdown the toggle and the button sit beside
|
|
934
|
+
the title in ~324px of a 330px box, which is one row again. The countdown is the least
|
|
935
|
+
load-bearing thing in the row: the viewer refreshes itself regardless, and the button says so. */
|
|
936
|
+
.card:has(> .treemapwrap.sq) { container-type: inline-size; }
|
|
937
|
+
@container (max-width: 430px) {
|
|
938
|
+
.card:has(> .treemapwrap.sq) > h3 .viewer-ctl .refresh-in { display: none; }
|
|
939
|
+
}
|
|
940
|
+
.card:has(> .treemapwrap.sq) > .note { flex: 0 1 auto; min-height: 0; overflow: hidden; }
|
|
941
|
+
/* The wrapper keeps its OWN aspect-ratio and full width from the base rule.
|
|
942
|
+
An earlier cut set width:auto here to centre it; the canvas inside is
|
|
943
|
+
position:absolute so it contributes no intrinsic width, and the wrapper
|
|
944
|
+
collapsed to zero -- the browser check read the overview canvas as
|
|
945
|
+
zeroSized. Let it be as wide as the card and square by its own ratio. */
|
|
946
|
+
.card:has(> .treemapwrap.sq) > .treemapwrap.sq { flex: 0 0 auto; width: 100%; }
|
|
947
|
+
.treemapwrap.sq canvas.treemap { position: absolute; inset: 0; width: 100%; height: 100% }
|
|
948
|
+
.goggles-tip { position: absolute; left: 8px; bottom: 8px; max-width: 92%; font-size: 10.5px; font-family: var(--mono);
|
|
949
|
+
background: #0b0d10ee; border: 1px solid var(--line-soft); border-radius: 4px; padding: 2px 7px; color: var(--fg) }
|
|
950
|
+
.lg, .lgc { color: var(--fg-dim) }
|
|
951
|
+
|
|
952
|
+
/* Compact copy of the map, for the Overview */
|
|
953
|
+
canvas.treemap.sm { height: 168px }
|
|
954
|
+
|
|
955
|
+
/* The row must be allowed to exceed the card, or the cards at the right are simply cut
|
|
956
|
+
off with no way to reach them. Measured in a real browser: .flow scrollWidth 2236
|
|
957
|
+
inside clientWidth 1369, so three blocks and the current-tip card were off-screen with
|
|
958
|
+
no scrollbar anywhere. The wrap scrolls, the row sizes to its content. */
|
|
959
|
+
/* (.flowwrap is defined above, with the scroll-padding the 3D depth needs.) */
|
|
960
|
+
.flow { width: 100%; min-width: 100%; justify-content: flex-start }
|
|
961
|
+
|
|
962
|
+
/* One third shorter than .short (130px), for the three Overview cards that are really a
|
|
963
|
+
number and a sparkline, not a chart to be read in detail. */
|
|
964
|
+
canvas.chart.xs, .chart.xs { height: 86px }
|
|
965
|
+
/* Tighten the compact cards: the metric line and its caption carry the information, the
|
|
966
|
+
padding did not. */
|
|
967
|
+
.card.compact { padding: 8px 10px }
|
|
968
|
+
.card.compact .metric { margin: 2px 0 4px }
|
|
969
|
+
.card.compact .metric .v { font-size: 20px }
|
|
970
|
+
|
|
971
|
+
/* The honest tail of a train longer than the panel: it says how many are off-view. */
|
|
972
|
+
.bcard.older { border-style: dotted; color: var(--fg-dim);
|
|
973
|
+
font-size: 11px; display: flex; flex-direction: column; justify-content: center }
|
|
974
|
+
.bcard.older span { color: var(--fg-faint); font-size: 10px }
|
|
975
|
+
|
|
976
|
+
/* ==========================================================================
|
|
977
|
+
BLOCK FLOW: blocks that look like blocks, on a chain
|
|
978
|
+
==========================================================================
|
|
979
|
+
2026-09-11, operator: "I want the blocks to be actual blocks with some depth
|
|
980
|
+
and linked via a chain ... Make the block flow beautiful".
|
|
981
|
+
|
|
982
|
+
This deliberately reverses the 2026-09-10 removal of card depth. That removal
|
|
983
|
+
was right at the time and for a different reason: the depth then was a
|
|
984
|
+
::before lip hanging BELOW the card, and the row's own `overflow` clipped it
|
|
985
|
+
into a smear. The depth here is painted entirely INSIDE the border box (a lit
|
|
986
|
+
top edge, a body that falls into shadow, a darkened right face) plus one
|
|
987
|
+
small cast shadow, so there is nothing for a scroll container to cut off.
|
|
988
|
+
The guard that pinned the removal is updated alongside, with the reason. */
|
|
989
|
+
|
|
990
|
+
.bcard {
|
|
991
|
+
border-radius: 5px;
|
|
992
|
+
background:
|
|
993
|
+
/* the top face catches the light, the body falls away from it */
|
|
994
|
+
linear-gradient(180deg, rgba(235, 245, 255, .07) 0, rgba(235, 245, 255, 0) 38%),
|
|
995
|
+
/* the right-hand side face: the slab has thickness */
|
|
996
|
+
linear-gradient(270deg, rgba(0, 0, 0, .34) 0, rgba(0, 0, 0, 0) 9px),
|
|
997
|
+
linear-gradient(180deg, var(--bg-2) 0, var(--bg-1) 100%);
|
|
998
|
+
box-shadow:
|
|
999
|
+
0 1px 0 rgba(235, 245, 255, .10) inset,
|
|
1000
|
+
0 -16px 22px rgba(0, 0, 0, .26) inset,
|
|
1001
|
+
3px 4px 0 -1px rgba(0, 0, 0, .40),
|
|
1002
|
+
7px 9px 16px rgba(0, 0, 0, .30);
|
|
1003
|
+
}
|
|
1004
|
+
/* A forecast is not a solid: no thickness, no shadow — it has not been built. */
|
|
1005
|
+
.bcard.ghost { background: transparent; box-shadow: none; }
|
|
1006
|
+
|
|
1007
|
+
/* --- the chain ----------------------------------------------------------
|
|
1008
|
+
Two interlocking links of a real chain, drawn in the gap between cards: a
|
|
1009
|
+
picture of what the cards describe -- each block commits to the one before
|
|
1010
|
+
it. Accent-coloured tubes with a highlight and a glow, because the pale
|
|
1011
|
+
outlines this used to be vanished against the dark panel (operator,
|
|
1012
|
+
2026-09-14: "looks bad on a black background ... much more stylized and
|
|
1013
|
+
visible"). The markup is an inline SVG in mining.js (LINK). */
|
|
1014
|
+
.chainlink {
|
|
1015
|
+
flex: 0 0 auto; align-self: center; position: relative;
|
|
1016
|
+
width: 34px; height: 20px; margin: 0 -2px;
|
|
1017
|
+
}
|
|
1018
|
+
.chainlink svg {
|
|
1019
|
+
display: block; width: 100%; height: 100%; overflow: visible;
|
|
1020
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--accent) 55%, transparent)) drop-shadow(0 1px 1px rgba(0, 0, 0, .6));
|
|
1021
|
+
}
|
|
1022
|
+
.chainlink .cl-base, .chainlink .cl-hi { fill: none; stroke-linecap: round; }
|
|
1023
|
+
.chainlink .cl-base { stroke: var(--accent-dim); stroke-width: 3.4; }
|
|
1024
|
+
.chainlink .cl-hi { stroke: #ffc862; stroke-width: 1.3; }
|
|
1025
|
+
|
|
1026
|
+
/* --- the block being built ----------------------------------------------
|
|
1027
|
+
The coloured ring belongs HERE, not on the tip: it says how long this block
|
|
1028
|
+
has been accumulating, which is a fact about work in progress. The tip keeps
|
|
1029
|
+
the accent ring, which says something else — this is the height the chain
|
|
1030
|
+
reports. */
|
|
1031
|
+
.bcard.next { border-color: var(--accent); }
|
|
1032
|
+
.bcard.next .bage {
|
|
1033
|
+
margin-left: auto; font-size: 10px; font-variant-numeric: tabular-nums;
|
|
1034
|
+
color: var(--fg-dim); font-family: var(--mono);
|
|
1035
|
+
}
|
|
1036
|
+
.bcard.next.fresh {
|
|
1037
|
+
border-color: var(--ok);
|
|
1038
|
+
box-shadow: 0 0 0 1px var(--ok), 0 0 16px #2ecc8f2e,
|
|
1039
|
+
0 1px 0 rgba(235, 245, 255, .10) inset, 0 -16px 22px rgba(0, 0, 0, .26) inset,
|
|
1040
|
+
3px 4px 0 -1px rgba(0, 0, 0, .40), 7px 9px 16px rgba(0, 0, 0, .30);
|
|
1041
|
+
}
|
|
1042
|
+
.bcard.next.fresh .bage { color: var(--ok) }
|
|
1043
|
+
.bcard.next.late {
|
|
1044
|
+
border-color: var(--warn); background: #f0b4291c;
|
|
1045
|
+
box-shadow: 0 0 0 1px var(--warn), 0 0 16px #f0b4292e,
|
|
1046
|
+
0 1px 0 rgba(235, 245, 255, .10) inset, 0 -16px 22px rgba(0, 0, 0, .26) inset,
|
|
1047
|
+
3px 4px 0 -1px rgba(0, 0, 0, .40), 7px 9px 16px rgba(0, 0, 0, .30);
|
|
1048
|
+
}
|
|
1049
|
+
.bcard.next.late .bage { color: var(--warn) }
|
|
1050
|
+
.bcard.next.overdue {
|
|
1051
|
+
border-color: var(--bad); background: #ef5a5a22;
|
|
1052
|
+
box-shadow: 0 0 0 1px var(--bad), 0 0 18px #ef5a5a38,
|
|
1053
|
+
0 1px 0 rgba(235, 245, 255, .10) inset, 0 -16px 22px rgba(0, 0, 0, .26) inset,
|
|
1054
|
+
3px 4px 0 -1px rgba(0, 0, 0, .40), 7px 9px 16px rgba(0, 0, 0, .30);
|
|
1055
|
+
animation: due 2.4s ease-in-out infinite;
|
|
1056
|
+
}
|
|
1057
|
+
.bcard.next.overdue .bage { color: var(--bad) }
|
|
1058
|
+
@media (prefers-reduced-motion: reduce) { .bcard.next.overdue { animation: none } }
|
|
1059
|
+
|
|
1060
|
+
/* the data-quality line that replaced the four-sentence explainer */
|
|
1061
|
+
.flownote { margin-top: 6px; font-size: 10.5px; color: var(--fg-dim) }
|
|
1062
|
+
|
|
1063
|
+
/* --- the card is a block, so it fills its height like one -----------------
|
|
1064
|
+
The panel is as tall as the Block space square beside it, and the cards
|
|
1065
|
+
stretch to it. They used to stretch with all their content bunched at the
|
|
1066
|
+
top and 60% of each card empty black. A block's header goes at the top, its
|
|
1067
|
+
numbers in the body, and the weight it used is the FLOOR it stands on. */
|
|
1068
|
+
.bcard { display: flex; flex-direction: column; }
|
|
1069
|
+
.bcard .bfill, .bcard .bnextfill { margin-top: auto; }
|
|
1070
|
+
.bcard .bcap, .bcard .bnextpct { flex: 0 0 auto; }
|
|
1071
|
+
|
|
1072
|
+
/* Values were being cut mid-digit ("0.00279 BT", "717,627 / 4,000,00"). A
|
|
1073
|
+
clipped number is worse than a smaller one. */
|
|
1074
|
+
.bcard { width: 186px; }
|
|
1075
|
+
.bcard.next, .bcard.next.empty { width: 212px; }
|
|
1076
|
+
.bcard.tip { flex-basis: 186px; }
|
|
1077
|
+
/* A NUMBER NEVER BREAKS IN TWO (2026-09-14, the first Mac: "sat/tx 604" drew as "60" over "4" because
|
|
1078
|
+
overflow-wrap: anywhere let a value wrap where the font ran wider). The value keeps its line;
|
|
1079
|
+
the card is wide enough for six digits beside its label in any monospace at this size. */
|
|
1080
|
+
.bgrid span, .brows span { font-size: 10px; text-align: right; min-width: 0; white-space: nowrap; }
|
|
1081
|
+
.brows > div { gap: 6px }
|
|
1082
|
+
|
|
1083
|
+
/* the zone labels are gone; reclaim the space they reserved */
|
|
1084
|
+
.flowside.todo, .flowside.done { padding-top: 0; }
|
|
1085
|
+
|
|
1086
|
+
/* --- the chain, made visible ---------------------------------------------
|
|
1087
|
+
The one piece of this panel that says what the panel IS, so it gets room. */
|
|
1088
|
+
.chainlink { width: 40px; height: 22px; }
|
|
1089
|
+
|
|
1090
|
+
/* --- the row may exceed the panel; a chopped card is not a design ---------
|
|
1091
|
+
`.flow { width: 100% }` above contradicted the comment right beside it: the
|
|
1092
|
+
row could not exceed the panel, so `.flowside.done { overflow: hidden }`
|
|
1093
|
+
silently CUT the cards past the edge (measured 2026-09-11: done scrollWidth
|
|
1094
|
+
1996 inside clientWidth 744 — the tip's neighbours were sliced in half with
|
|
1095
|
+
no scrollbar to reach them). The row sizes to its content and the wrap
|
|
1096
|
+
scrolls, which is what that comment always intended; `min-width: 100%` keeps
|
|
1097
|
+
a short train filling the panel. Snap points mean a scroll lands on a block
|
|
1098
|
+
boundary rather than mid-card. */
|
|
1099
|
+
.flow { width: max-content; min-width: 100%; }
|
|
1100
|
+
.flowside.done { overflow: visible; }
|
|
1101
|
+
.flowwrap { scroll-snap-type: x proximity; }
|
|
1102
|
+
.bcard { scroll-snap-align: start; scroll-margin-left: 10px; }
|
|
1103
|
+
|
|
1104
|
+
/* Values ran into the right border ("4,000,00|", "3.2 M|"), which reads as a
|
|
1105
|
+
clipped number even when the element does not actually overflow. */
|
|
1106
|
+
.bcard { padding: 9px 11px; }
|
|
1107
|
+
.bcard.ghost.sliver { padding: 7px 8px; }
|
|
1108
|
+
|
|
1109
|
+
/* --- a card is as tall as what it says -----------------------------------
|
|
1110
|
+
2026-09-11 (operator: "Takes up way too much vertical space"). The cards
|
|
1111
|
+
were stretched to the Block space square beside the row and their numbers
|
|
1112
|
+
were spread through the height with auto margins -- which is how a
|
|
1113
|
+
130 px card became a 460 px one with a black middle. The stretch is gone;
|
|
1114
|
+
the numbers sit under the header, and only the weight bar keeps its place
|
|
1115
|
+
on the floor (it lines the row's cards up along the bottom). */
|
|
1116
|
+
.bcard .bgrid, .bcard .brows { margin-top: 2px; }
|
|
1117
|
+
.bcard .bwhen, .bcard .bgap { flex: 0 0 auto; }
|
|
1118
|
+
.bcard .bgap { margin: 1px 0 3px; }
|
|
1119
|
+
.bcard .bmeter { margin: 2px 0 5px; }
|
|
1120
|
+
.bcard .chips { margin-top: 5px; }
|
|
1121
|
+
|
|
1122
|
+
/* --- the card body IS the block's fullness --------------------------------
|
|
1123
|
+
A card tall enough to stand in this panel had a black middle: header at the
|
|
1124
|
+
top, numbers floating, floor at the bottom. The space is now the block
|
|
1125
|
+
itself — weight used against the 4,000,000 WU cap, rising from the floor in
|
|
1126
|
+
the pool's own colour. A full block is a solid column, and a block that came
|
|
1127
|
+
in light is visibly short, which is the one thing the eye should catch while
|
|
1128
|
+
scanning a row of them. It is the same figure the bar at the foot states in
|
|
1129
|
+
words, drawn at the scale the card actually has. */
|
|
1130
|
+
.bcard { position: relative; }
|
|
1131
|
+
.bstack {
|
|
1132
|
+
position: absolute; left: 1px; right: 1px; bottom: 1px; z-index: 0;
|
|
1133
|
+
border-radius: 0 0 4px 4px; pointer-events: none;
|
|
1134
|
+
/* Opacity on the ELEMENT, not alpha mixed into the colour. color-mix was
|
|
1135
|
+
tried and came out fully opaque here, which buried the card's own text
|
|
1136
|
+
under a slab of pool colour -- the fill has to sit behind the numbers, not
|
|
1137
|
+
replace them. */
|
|
1138
|
+
background: linear-gradient(180deg, var(--pool, var(--accent)) 0, rgba(0, 0, 0, 0) 140%);
|
|
1139
|
+
opacity: .13;
|
|
1140
|
+
}
|
|
1141
|
+
.bcard.proj .bstack { background: linear-gradient(180deg, var(--pc, var(--accent)) 0, rgba(0, 0, 0, 0) 140%); }
|
|
1142
|
+
/* The block being built carries a meter and a live dot in the accent; its fill joins them
|
|
1143
|
+
(it sets no --pool, so the declaration above already falls back to the accent). */
|
|
1144
|
+
.bcard > *:not(.bstack) { position: relative; z-index: 1; }
|
|
1145
|
+
|
|
1146
|
+
/* ==========================================================================
|
|
1147
|
+
THE BLOCK METER: the block being built, filling up
|
|
1148
|
+
==========================================================================
|
|
1149
|
+
2026-09-11, operator: "That orange wavy part inside the block being built is
|
|
1150
|
+
disjointed and not contiguous across the line when moving. we need to find
|
|
1151
|
+
some other visually interesting way to convey information that the block is
|
|
1152
|
+
growing." The row of bobbing ticks (.inflow) wrapped onto a second line and
|
|
1153
|
+
each tick bobbed on its own delay, so it read as broken; it was also a
|
|
1154
|
+
sqrt-shaped decoration, not a measurement.
|
|
1155
|
+
|
|
1156
|
+
ONE row that cannot wrap: 40 segments, each 2.5% of the 4,000,000 WU cap.
|
|
1157
|
+
Lit segments are the weight the node has selected, coloured by the feerate
|
|
1158
|
+
of the transactions that fill that stretch of the template (richest first,
|
|
1159
|
+
our own feerate palette -- the same colours as the stones in Block space).
|
|
1160
|
+
The frontier segment fills fractionally and breathes, its phase carried
|
|
1161
|
+
across the page's once-a-second repaint so it never restarts; segments that
|
|
1162
|
+
a new template reading added light up once as they arrive. */
|
|
1163
|
+
.bmeter { display: flex; flex-wrap: nowrap; gap: 1px; height: 13px; margin: 3px 0 7px;
|
|
1164
|
+
padding: 2px; border-radius: 3px; background: rgba(0, 0, 0, .35);
|
|
1165
|
+
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .6), 0 1px 0 rgba(235, 245, 255, .05); }
|
|
1166
|
+
.bmeter i { flex: 1 1 0; min-width: 0; position: relative; overflow: hidden; border-radius: 1px;
|
|
1167
|
+
background: rgba(235, 245, 255, .05); }
|
|
1168
|
+
.bmeter i.on { background: var(--accent);
|
|
1169
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), inset 0 -2px 0 rgba(0, 0, 0, .28); }
|
|
1170
|
+
.bmeter i.new { animation: meterpop .9s cubic-bezier(.2, .9, .3, 1.2) both; }
|
|
1171
|
+
@keyframes meterpop { 0% { transform: scaleY(.15); filter: brightness(2.4) } 60% { filter: brightness(1.5) } 100% { transform: none; filter: none } }
|
|
1172
|
+
.bmeter i.edge span { position: absolute; left: 0; top: 0; bottom: 0; display: block; background: var(--accent);
|
|
1173
|
+
animation: meteredge 2.4s ease-in-out infinite; }
|
|
1174
|
+
.bmeter i.next { box-shadow: inset 0 0 0 1px rgba(235, 245, 255, .22); animation: meternext 2.4s ease-in-out infinite; }
|
|
1175
|
+
@keyframes meteredge { 0%, 100% { filter: brightness(.9) } 50% { filter: brightness(1.7) } }
|
|
1176
|
+
@keyframes meternext { 0%, 100% { background: rgba(235, 245, 255, .04) } 50% { background: rgba(235, 245, 255, .16) } }
|
|
1177
|
+
@media (prefers-reduced-motion: reduce) { .bmeter i.new, .bmeter i.edge span, .bmeter i.next { animation: none } }
|
|
1178
|
+
|
|
1179
|
+
/* ==========================================================================
|
|
1180
|
+
BLOCK SPACE: the Matrix
|
|
1181
|
+
==========================================================================
|
|
1182
|
+
2026-09-11, operator: "absolutely spectacular ... feel like they are in The
|
|
1183
|
+
Matrix". The canvas paints the board (details3d.js); the phosphor rim,
|
|
1184
|
+
vignette and scanlines are CSS over it, because CSS is not subject to the
|
|
1185
|
+
canvas no-composite rule and costs nothing per frame. None of it takes
|
|
1186
|
+
pointer events, so hover still reaches the board. (Digital rain was layered
|
|
1187
|
+
over this the same day and removed: "looks stupid and adds nothing
|
|
1188
|
+
practical".) */
|
|
1189
|
+
.treemapwrap.sq, .treemapwrap.space { border-radius: 5px; box-shadow: 0 0 0 1px rgba(60, 255, 150, .28), 0 0 22px rgba(40, 255, 140, .10); }
|
|
1190
|
+
.treemapwrap.sq canvas.treemap, .treemapwrap.space canvas.treemap { border-color: rgba(60, 255, 150, .18); background: #020906; }
|
|
1191
|
+
.treemapwrap.sq::after, .treemapwrap.space::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1; border-radius: 4px;
|
|
1192
|
+
background:
|
|
1193
|
+
repeating-linear-gradient(0deg, rgba(0, 0, 0, .07) 0 1px, rgba(0, 0, 0, 0) 1px 3px),
|
|
1194
|
+
radial-gradient(ellipse at 50% 45%, rgba(0, 0, 0, 0) 58%, rgba(0, 12, 6, .55) 100%); }
|
|
1195
|
+
.treemapwrap .goggles-tip { z-index: 3; color: #b8ffd0; border-color: rgba(60, 255, 150, .45);
|
|
1196
|
+
background: rgba(0, 10, 4, .88); box-shadow: 0 0 12px rgba(40, 255, 140, .18); }
|
|
1197
|
+
|
|
1198
|
+
/* ==========================================================================
|
|
1199
|
+
THE BLOCK SPACE PAGE: the viewer at window size, with the block being
|
|
1200
|
+
built and the chain tip beside it (operator, 2026-09-11)
|
|
1201
|
+
==========================================================================
|
|
1202
|
+
The square is sized to whichever runs out first -- the width left beside the
|
|
1203
|
+
side panel, or the window's height under the header -- so the whole board is
|
|
1204
|
+
always on screen without scrolling. Under 980 px the panel stacks below. */
|
|
1205
|
+
.spacelayout { display: flex; gap: 16px; align-items: flex-start; }
|
|
1206
|
+
.treemapwrap.space { position: relative; flex: 0 0 auto; aspect-ratio: 1 / 1;
|
|
1207
|
+
/* the whole card on one screen (operator, 2026-09-15: "same treatment on the block space page"):
|
|
1208
|
+
the header, the card's padding, its heading and the note under the board come to ~195px */
|
|
1209
|
+
width: min(calc(100vw - 470px), calc(100vh - 195px)); min-width: 320px; }
|
|
1210
|
+
.page[data-page="space"] .spacelayout { gap: 12px; }
|
|
1211
|
+
.treemapwrap.space canvas.treemap { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
1212
|
+
.spacehud { flex: 1 1 300px; min-width: 260px; max-width: 420px; display: flex; flex-direction: column; gap: 12px; }
|
|
1213
|
+
.hud { border: 1px solid rgba(60, 255, 150, .18); border-radius: 6px; padding: 10px 12px;
|
|
1214
|
+
background: rgba(0, 18, 9, .35); }
|
|
1215
|
+
.hudh { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; font-size: 11px;
|
|
1216
|
+
letter-spacing: .09em; text-transform: uppercase; color: var(--fg-dim); }
|
|
1217
|
+
.hudh b { color: var(--fg); font-weight: 600; }
|
|
1218
|
+
.hudk { font-family: var(--mono); font-size: 14px; letter-spacing: 0; text-transform: none; color: var(--fg); }
|
|
1219
|
+
.hudclock { margin-left: auto; padding: 1px 8px; border-radius: 10px; border: 1px solid var(--line-soft);
|
|
1220
|
+
font-family: var(--mono); font-size: 13px; letter-spacing: 0; color: var(--fg-dim); }
|
|
1221
|
+
.hudclock.fresh { color: var(--ok); border-color: var(--ok); }
|
|
1222
|
+
.hudclock.late { color: var(--warn); border-color: var(--warn); background: #f0b4291c; }
|
|
1223
|
+
.hudclock.overdue { color: var(--bad); border-color: var(--bad); background: #ef5a5a22; }
|
|
1224
|
+
.hud .bmeter { height: 18px; margin: 2px 0 8px; }
|
|
1225
|
+
.hudbig { display: flex; align-items: baseline; gap: 6px; margin: 0 0 8px; font-family: var(--mono); font-size: 28px; line-height: 1.1; }
|
|
1226
|
+
.hudbig small { font-size: 12px; color: var(--fg-dim); }
|
|
1227
|
+
.hudbig .chip { margin-left: auto; align-self: center; font-family: inherit; }
|
|
1228
|
+
.hudkv { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; margin: 0; font-size: 12px; }
|
|
1229
|
+
.hudkv dt { color: var(--fg-dim); }
|
|
1230
|
+
.hudkv dd { margin: 0; text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
1231
|
+
.hudpool { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; font-size: 15px; }
|
|
1232
|
+
.hudpool .faint { margin-left: auto; font-size: 11.5px; }
|
|
1233
|
+
.hudbar { height: 6px; margin: 4px 0 8px; border-radius: 3px; overflow: hidden; background: var(--bg-3, #12161c); }
|
|
1234
|
+
.hudbar span { display: block; height: 100%; }
|
|
1235
|
+
/* THE THREE PANELS FIT ON ONE SCREEN (operator, 2026-09-12: "Shrink up the vertical size of the
|
|
1236
|
+
being built and chain tip panels so we can fit the feerate panel on one screen"). Measured at
|
|
1237
|
+
1280x700 before: Being built 262 px, Chain tip 236, Feerate 111, and the Feerate panel's bottom
|
|
1238
|
+
62 px past the fold. Most of that height is the key/value rows -- 144 px of EACH panel is seven
|
|
1239
|
+
rows at about 20.6 px -- so the rows are tightened first and the rest trimmed a little. Scoped to
|
|
1240
|
+
.spacehud throughout, because these same classes dress the Markets legend (.mkside .hud), which
|
|
1241
|
+
is not short of room and should not change. */
|
|
1242
|
+
.spacehud { gap: 9px; }
|
|
1243
|
+
.spacehud .hud { padding: 7px 11px; }
|
|
1244
|
+
.spacehud .hudh { margin-bottom: 5px; }
|
|
1245
|
+
.spacehud .hud .bmeter { height: 14px; margin: 1px 0 5px; }
|
|
1246
|
+
.spacehud .hudbig { font-size: 22px; margin-bottom: 5px; }
|
|
1247
|
+
.spacehud .hudkv { gap: 1px 12px; line-height: 1.35; }
|
|
1248
|
+
.spacehud .hudpool { font-size: 13.5px; margin-bottom: 3px; }
|
|
1249
|
+
.spacehud .hudbar { margin: 3px 0 5px; }
|
|
1250
|
+
.feelegend { display: flex; flex-wrap: wrap; gap: 5px 11px; margin-bottom: 5px; font-family: var(--mono); font-size: 11px; color: var(--fg-dim); }
|
|
1251
|
+
.feelegend i { display: inline-block; width: 12px; height: 12px; margin-right: 4px; border-radius: 2px; vertical-align: -2px; }
|
|
1252
|
+
@media (max-width: 980px) {
|
|
1253
|
+
.spacelayout { flex-direction: column; }
|
|
1254
|
+
.treemapwrap.space { width: 100%; min-width: 0; }
|
|
1255
|
+
.spacehud { max-width: none; width: 100%; }
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/* The pool viewer's countdown to its next refresh (operator, 2026-09-11: "we need
|
|
1259
|
+
to add a countdown to refresh somewhere in that panel"). Top-right, over the
|
|
1260
|
+
sphere's margin, where no block rests; the fill behind the text is the share of
|
|
1261
|
+
the minute already gone. Tabular figures, so the chip does not twitch each second. */
|
|
1262
|
+
.treemapwrap .viewer-ctl { position: absolute; top: 8px; right: 8px; z-index: 3; display: flex; gap: 6px; align-items: center; }
|
|
1263
|
+
.treemapwrap .refresh-in { pointer-events: none;
|
|
1264
|
+
font-size: 10.5px; font-family: var(--mono); font-variant-numeric: tabular-nums; color: #b8ffd0;
|
|
1265
|
+
border: 1px solid rgba(60, 255, 150, .45); border-radius: 4px; padding: 2px 7px;
|
|
1266
|
+
background: linear-gradient(90deg, rgba(60, 255, 150, .24) var(--p, 0%), transparent var(--p, 0%)), #0b0d10dd; }
|
|
1267
|
+
.treemapwrap .refresh-in:empty { display: none }
|
|
1268
|
+
|
|
1269
|
+
/* "Trigger Refresh Now" beside the countdown: live only while the board is at rest. */
|
|
1270
|
+
.treemapwrap .refresh-now { font-size: 10.5px; font-family: var(--mono); color: #b8ffd0; background: #0b0d10dd;
|
|
1271
|
+
border: 1px solid rgba(60, 255, 150, .45); border-radius: 4px; padding: 2px 8px; cursor: pointer; }
|
|
1272
|
+
.treemapwrap .refresh-now:hover:not(:disabled) { background: rgba(60, 255, 150, .2); }
|
|
1273
|
+
.treemapwrap .refresh-now:disabled { opacity: .4; cursor: default; }
|
|
1274
|
+
|
|
1275
|
+
/* THE MINING PAGE AS TWO STACKS (operator, 2026-09-11: "#mining -- too much wasted space in
|
|
1276
|
+
this display. Crunch down the panel sizes to fit more info"). As one grid the rows were
|
|
1277
|
+
coupled: measured at 1512 px, Block flow sat in a 975x591 card using ~273 px because the
|
|
1278
|
+
square Mempool space viewer set that row, and the feerate landscape was stretched to the
|
|
1279
|
+
packages table (203 of 330 px). Stacks never stretch a card to a neighbour; the Detailed
|
|
1280
|
+
reference text folds away until asked for. */
|
|
1281
|
+
.mncols { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; align-items: start; }
|
|
1282
|
+
/* the network row on Mining (2026-09-15): big figures in threes, a donut with its legend, the year chart */
|
|
1283
|
+
.netstats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 14px; padding: 4px 0 8px; }
|
|
1284
|
+
.netstats .ns { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
1285
|
+
.netstats .ns .k { font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-faint); }
|
|
1286
|
+
.netstats .ns .v { font-size: 22px; font-weight: 650; color: var(--fg); font-variant-numeric: tabular-nums; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1287
|
+
.netstats .ns .v small { font-size: 11px; font-weight: 400; color: var(--fg-dim); margin-left: 4px; }
|
|
1288
|
+
.netstats .ns .s { font-size: 11px; color: var(--fg-dim); font-family: var(--mono); }
|
|
1289
|
+
.netstats .ns .s .usd { display: block; color: var(--ok); font-size: 13px; font-weight: 600; }
|
|
1290
|
+
.netstats .usdnote { grid-column: 1 / -1; }
|
|
1291
|
+
.netstats .up { color: var(--ok); } .netstats .down { color: var(--bad); }
|
|
1292
|
+
/* THE PIE AND THE YEAR CHART TAKE WHAT THE WINDOW HAS (operator, 2026-09-15, of a 945px-tall
|
|
1293
|
+
window: "This all needs to comfortably fit on one page. I can't even see the View More for pools
|
|
1294
|
+
view"): their height follows the viewport so the flow, the two figure cards and the two chart
|
|
1295
|
+
cards -- View more links included -- fit one screen from ~900px tall up, growing to 380/240 */
|
|
1296
|
+
canvas.chart.donut { width: 100%; height: clamp(220px, calc(100vh - 640px), 380px); }
|
|
1297
|
+
.card.mnflow { grid-column: 1 / -1 !important; } /* !important: the 1280px rule forces every .card to span 6 */
|
|
1298
|
+
/* THINNER (operator, 2026-09-15: "The block flow can be thinner vertically for instance" -- then, of
|
|
1299
|
+
Overview, "same treatment"): .flowtight is the flow card on Mining and on Overview.
|
|
1300
|
+
the same cards, narrower and without the being-built card's badge and queue lines, which the
|
|
1301
|
+
packages panel beneath already says in full */
|
|
1302
|
+
.flowtight .bcard { width: 160px; }
|
|
1303
|
+
.flowtight .bcard.next, .flowtight .bcard.next.empty { width: 188px; }
|
|
1304
|
+
.flowtight .bcard.tip { flex-basis: 160px; }
|
|
1305
|
+
.flowtight .bcard .chips, .flowtight .bcard.next .note.tiny { display: none; }
|
|
1306
|
+
.flowtight .bgrid span, .flowtight .brows span { font-size: 9.5px; }
|
|
1307
|
+
.flowtight .flowwrap { padding-bottom: 0; }
|
|
1308
|
+
.flowtight .flow { padding: 2px 2px 0; }
|
|
1309
|
+
/* ...and no dead height (operator: "Too much empty space under the blocks to the panel border"):
|
|
1310
|
+
measured 254px for a card whose blocks are 161 -- the legend row under the flow, the rail's
|
|
1311
|
+
margin, the card's and the blocks' own padding. The legend goes on this page, the rest tightens. */
|
|
1312
|
+
.card.flowtight { padding: 8px 11px 7px; }
|
|
1313
|
+
.mnflow h3 { margin-bottom: 5px; }
|
|
1314
|
+
.flowtight .rail { margin-bottom: 3px; }
|
|
1315
|
+
.flowtight .tiplegend { display: none; }
|
|
1316
|
+
.flowtight .bcard { padding-top: 6px; padding-bottom: 6px; }
|
|
1317
|
+
.flowtight .bcard .bwhen { margin-top: 2px; margin-bottom: 3px; }
|
|
1318
|
+
.flowtight .bcard .bplaque { margin-top: 4px; }
|
|
1319
|
+
/* the whole Mining page packs tighter, the same way -- and Overview with it ("same treatment on overview") */
|
|
1320
|
+
.page[data-page="overview"] .metric .v { font-size: 22px; }
|
|
1321
|
+
/* the Block space square set the right column's height (745px of a 773px column at 1600 wide) and
|
|
1322
|
+
stretched Last blocks and the events to match; capped to the viewport, the board draws wider
|
|
1323
|
+
than tall, as it does on the Kiosk, and the page fits far more of itself on one screen */
|
|
1324
|
+
.page[data-page="overview"] .treemapwrap.sq { max-height: max(360px, calc(100vh - 340px)); }
|
|
1325
|
+
.page[data-page="mining"] .netstats { padding: 2px 0 4px; gap: 6px 12px; }
|
|
1326
|
+
.page[data-page="mining"] .netstats .ns .v { font-size: 20px; }
|
|
1327
|
+
.mn3 { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: start; }
|
|
1328
|
+
/* mempool.space's two columns (2026-09-15), and our own panels in two beneath */
|
|
1329
|
+
.mn2, .mnextra { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
|
|
1330
|
+
.mn2 .card, .mnextra .card { grid-column: auto !important; } /* the page grid's span rules do not apply inside these two-column grids */
|
|
1331
|
+
.mn2 .card { display: flex; flex-direction: column; }
|
|
1332
|
+
.mn2 .card > .viewmore { margin-top: auto; }
|
|
1333
|
+
.viewmore { display: block; text-align: center; padding: 8px 0 2px; color: var(--accent, #4fb3ff); text-decoration: none; font-size: 13px; }
|
|
1334
|
+
.viewmore:hover { text-decoration: underline; }
|
|
1335
|
+
@media (max-width: 900px) { .mn2, .mnextra { grid-template-columns: minmax(0, 1fr); } }
|
|
1336
|
+
/* the full-screen card */
|
|
1337
|
+
.expandwrap { position: fixed; inset: 0; z-index: 60; display: flex; align-items: stretch; justify-content: center; padding: 14px; }
|
|
1338
|
+
.expandwrap.hidden { display: none; }
|
|
1339
|
+
.expand { position: relative; width: min(1500px, 100%); display: flex; flex-direction: column; overflow: hidden; }
|
|
1340
|
+
.expand > h3 { flex: 0 0 auto; }
|
|
1341
|
+
.expand .expandbody { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
|
|
1342
|
+
.expand canvas.chart.donut { flex: 0 0 auto; height: 58vh; width: 100%; } /* a definite height: a flex-grown canvas resized itself every frame */
|
|
1343
|
+
.expand canvas.chart.big { flex: 0 0 auto; height: 72vh; width: 100%; }
|
|
1344
|
+
.expand .netstats .ns .v { font-size: 26px; }
|
|
1345
|
+
.expand table.t { font-size: 13px; }
|
|
1346
|
+
/* three stacks only where each gets 450px or more (operator, 2026-09-15, of an 1146px window
|
|
1347
|
+
where the packages table ran into the landscape and the pie was a coin): two below 1400 */
|
|
1348
|
+
@media (max-width: 1400px) { .mn3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
|
|
1349
|
+
/* a table wider than its stack scrolls inside its card instead of bleeding into the next */
|
|
1350
|
+
.mn3 .card > div:has(> table), #mnPackages, #mnPools { overflow-x: auto; max-width: 100%; }
|
|
1351
|
+
@media (max-width: 760px) { .mn3 { grid-template-columns: minmax(0, 1fr); } }
|
|
1352
|
+
.donutlegend { display: flex; flex-direction: column; gap: 3px; font-family: var(--mono); font-size: 11.5px; max-height: 230px; overflow: auto; }
|
|
1353
|
+
.donutlegend div { display: flex; align-items: center; gap: 7px; white-space: nowrap; }
|
|
1354
|
+
.donutlegend i { width: 9px; height: 9px; border-radius: 2px; flex: none; }
|
|
1355
|
+
.donutlegend b { color: var(--fg); font-weight: 600; margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
1356
|
+
.donutlegend i.sw0 { background: #e8306a; }
|
|
1357
|
+
.donutlegend i.sw1 { background: #8b3fd9; }
|
|
1358
|
+
.donutlegend i.sw2 { background: #5b4fd6; }
|
|
1359
|
+
.donutlegend i.sw3 { background: #3a7be0; }
|
|
1360
|
+
.donutlegend i.sw4 { background: #2f9ee6; }
|
|
1361
|
+
.donutlegend i.sw5 { background: #26b8c8; }
|
|
1362
|
+
.donutlegend i.sw6 { background: #22b7a0; }
|
|
1363
|
+
.donutlegend i.sw7 { background: #3cba6c; }
|
|
1364
|
+
.donutlegend i.sw8 { background: #8bc34a; }
|
|
1365
|
+
.donutlegend i.sw9 { background: #c9d02a; }
|
|
1366
|
+
.donutlegend i.sw10 { background: #f0c419; }
|
|
1367
|
+
.donutlegend i.sw11 { background: #f39c1f; }
|
|
1368
|
+
.donutlegend i.sw12 { background: #ee7b2f; }
|
|
1369
|
+
.donutlegend i.sw13 { background: #c7c9d1; }
|
|
1370
|
+
.donutlegend i.sw14 { background: #8d93a1; }
|
|
1371
|
+
canvas.chart.tall { height: clamp(150px, calc(100vh - 720px), 240px); }
|
|
1372
|
+
@media (max-width: 700px) { .netstats { grid-template-columns: repeat(2, minmax(0, 1fr)); } canvas.chart.donut { height: 360px; } }
|
|
1373
|
+
.mncols .card { grid-column: auto !important; }
|
|
1374
|
+
.mnpair { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 12px; align-items: start; }
|
|
1375
|
+
.mndetailed > summary { list-style: none; cursor: pointer; }
|
|
1376
|
+
.mndetailed > summary::-webkit-details-marker { display: none; }
|
|
1377
|
+
.mndetailed > summary h3 { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
|
|
1378
|
+
color: var(--fg-faint); display: flex; align-items: center; gap: 8px; }
|
|
1379
|
+
.mndetailed > summary h3 .src { font: 400 10px/1.2 var(--mono); text-transform: none; letter-spacing: 0; color: var(--fg-faint); opacity: .8; }
|
|
1380
|
+
.mndetailed > summary h3 .sp { flex: 1; }
|
|
1381
|
+
.mndetailed > summary h3::after { content: "\25B8"; }
|
|
1382
|
+
.mndetailed[open] > summary h3::after { content: "\25BE"; }
|
|
1383
|
+
.mndetailed[open] > summary h3 { margin-bottom: 9px; }
|
|
1384
|
+
@media (max-width: 1100px) { .mncols, .mnpair { grid-template-columns: minmax(0, 1fr); } }
|
|
1385
|
+
|
|
1386
|
+
/* THE PEERS PAGE: one strip, then the table (see index.html). Every row that follows is a
|
|
1387
|
+
peer, so the table starts under ~150 px of summary instead of ~780. */
|
|
1388
|
+
.prtop { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(260px, 1fr) minmax(0, 2fr); gap: 12px; align-items: start; }
|
|
1389
|
+
.prtop .card { grid-column: auto !important; padding: 9px 12px; }
|
|
1390
|
+
.prsum { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 18px; align-items: center; align-content: start; }
|
|
1391
|
+
.prsum > h3 { grid-column: 1 / -1; }
|
|
1392
|
+
.prsum .metric .v { font-size: 22px; }
|
|
1393
|
+
.prbudget { font-size: 11px; gap: 1px 10px; }
|
|
1394
|
+
#prChart { height: 92px; }
|
|
1395
|
+
.prpeers .note { margin: 0 0 6px; }
|
|
1396
|
+
table.peertbl th { white-space: nowrap; }
|
|
1397
|
+
table.peertbl td { padding: 3px 7px; white-space: nowrap; }
|
|
1398
|
+
/* the same packing every tab got (2026-09-15: "same treatment on the peers tab"): a long onion or i2p
|
|
1399
|
+
address made the table wider than the window and the whole PAGE scrolled sideways; the address
|
|
1400
|
+
cell now clips with an ellipsis (the full address is its title) and a table still wider than its
|
|
1401
|
+
card scrolls inside the card. The service badges sit lower and the rows tighter. */
|
|
1402
|
+
#prTable { overflow-x: auto; max-width: 100%; }
|
|
1403
|
+
table.peertbl td.addr { max-width: 34ch; overflow: hidden; text-overflow: ellipsis; }
|
|
1404
|
+
table.peertbl td .badge { padding: 0 5px; font-size: 9.5px; line-height: 15px; }
|
|
1405
|
+
table.peertbl td { padding: 2px 7px; }
|
|
1406
|
+
@media (max-width: 860px) { .prtop { grid-template-columns: minmax(0, 1fr); } }
|
|
1407
|
+
|
|
1408
|
+
/* THE CHAIN & SYNC PAGE, THREE ACROSS (see index.html). Page-scoped, and it beats the site-wide
|
|
1409
|
+
"every card half width under 1280 px" rule down to 1000 px, where this page still fits three. */
|
|
1410
|
+
.chgrid .card.w4 { grid-column: span 4 !important; }
|
|
1411
|
+
.chgrid .card.w6 { grid-column: span 6 !important; }
|
|
1412
|
+
.chgrid canvas.chart { height: 110px; }
|
|
1413
|
+
.chgrid canvas.chart.short { height: 64px; }
|
|
1414
|
+
.chgrid .card { padding: 10px 12px; }
|
|
1415
|
+
.chgrid .card > h3 { margin-bottom: 6px; }
|
|
1416
|
+
.kv.two { grid-template-columns: auto 1fr auto 1fr; column-gap: 12px; }
|
|
1417
|
+
.kv.two dt { white-space: nowrap; }
|
|
1418
|
+
.kv.two dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* a hash or a long uptime keeps to its line */
|
|
1419
|
+
.chgrid .mksum.chtx { font-size: 11.5px; gap: 3px 14px; }
|
|
1420
|
+
.chgrid .mksum.chtx b { font-size: 12.5px; } /* "txs in window" wrapped to two lines and made its card the row's tallest (2026-09-15) */
|
|
1421
|
+
.chgrid .note.tiny { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1422
|
+
.kv.two dd { padding-right: 6px; }
|
|
1423
|
+
@media (max-width: 1000px) { .chgrid .card.w4 { grid-column: span 6 !important; } }
|
|
1424
|
+
@media (max-width: 700px) { .chgrid .card.w4, .chgrid .card.w6 { grid-column: span 12 !important; } .kv.two { grid-template-columns: auto 1fr; } }
|
|
1425
|
+
|
|
1426
|
+
/* PROJECTED BLOCKS (see projectedCards in mining.js). READABLE: a dark card, the fee colour
|
|
1427
|
+
as a left bar and a soft band behind the heading, light mono text one fact per line, no
|
|
1428
|
+
wrapping, only as tall as its content (the row stretches cards; these do not). The first cut
|
|
1429
|
+
put grey 10.5 px text on a saturated fill ("unreadable. Do better !!!"). */
|
|
1430
|
+
.flow .bcard.proj { width: 132px; flex: 0 0 auto; align-self: flex-start; padding: 8px 10px 9px; box-shadow: none;
|
|
1431
|
+
font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: #e8eef5;
|
|
1432
|
+
border: 1px solid color-mix(in srgb, var(--pc, #2e8b57) 55%, #1b2229); border-left: 4px solid var(--pc, #2e8b57);
|
|
1433
|
+
background: linear-gradient(180deg, color-mix(in srgb, var(--pc, #2e8b57) 34%, #0c1110) 0, #0c1110 50px); }
|
|
1434
|
+
.flow .bcard.proj > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1435
|
+
.flow .bcard.proj .ph { font-size: 14px; font-weight: 700; color: #ffffff; }
|
|
1436
|
+
.flow .bcard.proj .pm { font-size: 13px; font-weight: 600; color: #ffffff; margin-bottom: 3px; }
|
|
1437
|
+
.flow .bcard.proj .pr, .flow .bcard.proj .pf { color: #c9d2dc; }
|
|
1438
|
+
.flow .bcard.proj .pe { color: #8fe3b0; margin-top: 3px; }
|
|
1439
|
+
.flow .bcard.proj.rest { opacity: .85; }
|
|
1440
|
+
.flowside.todo { gap: 8px; }
|
|
1441
|
+
|
|
1442
|
+
/* ---- explorer (public/js/explorer.js) ---- */
|
|
1443
|
+
.xcard { display: flex; flex-direction: column; gap: 12px; }
|
|
1444
|
+
.xsearch { display: flex; gap: 8px; }
|
|
1445
|
+
.xsearch input { flex: 1 1 auto; min-width: 0; padding: 8px 11px; font: 13px var(--mono); color: var(--fg); background: var(--bg-2);
|
|
1446
|
+
border: 1px solid var(--line); border-radius: var(--radius-sm); }
|
|
1447
|
+
.xsearch input:focus { outline: none; border-color: #4f8f78; }
|
|
1448
|
+
.xsearch button { padding: 0 16px; }
|
|
1449
|
+
.xhead { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
1450
|
+
.xhead h2 { margin: 0; font-size: 17px; font-weight: 650; display: flex; align-items: center; gap: 8px; }
|
|
1451
|
+
.xh { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
1452
|
+
.xnav { text-decoration: none; padding: 0 7px; border: 1px solid var(--line); border-radius: var(--radius-sm); color: var(--fg-dim); }
|
|
1453
|
+
.xnav:hover { color: var(--fg); border-color: #4f8f78; }
|
|
1454
|
+
.xmono { font-family: var(--mono); }
|
|
1455
|
+
.xtxid { font-size: 13px; word-break: break-all; color: var(--fg); }
|
|
1456
|
+
.xkv { grid-template-columns: max-content 1fr max-content 1fr; max-width: 1100px; }
|
|
1457
|
+
.xkv dd { text-align: left; overflow-wrap: anywhere; }
|
|
1458
|
+
@media (max-width: 900px) { .xkv { grid-template-columns: max-content 1fr; } }
|
|
1459
|
+
.xsub { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--fg-dim); text-transform: uppercase; letter-spacing: .05em; }
|
|
1460
|
+
.xscroll { max-height: calc(100vh - 360px); min-height: 160px; }
|
|
1461
|
+
.xtbl a { color: #8fd8b8; text-decoration: none; }
|
|
1462
|
+
.xtbl a:hover, .xkv a:hover { text-decoration: underline; }
|
|
1463
|
+
.xkv a { color: #8fd8b8; text-decoration: none; }
|
|
1464
|
+
.xio { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
1465
|
+
@media (max-width: 1000px) { .xio { grid-template-columns: 1fr; } }
|
|
1466
|
+
.xf { display: inline-block; margin: 0 4px 0 0; padding: 0 6px; border-radius: 3px; font: 600 10px/16px var(--sans); border: 1px solid #ffffff22; color: var(--fg-dim); }
|
|
1467
|
+
.xf-segwit { color: #9fd3ff; border-color: #2c5878; }
|
|
1468
|
+
.xf-taproot { color: #d2b0ff; border-color: #5a3f80; }
|
|
1469
|
+
.xf-rbf { color: #ffcf8a; border-color: #7a5a24; }
|
|
1470
|
+
.xf-consolidation { color: #9fe8c4; border-color: #2d6a50; }
|
|
1471
|
+
.xf-op_return { color: #f0a8ad; border-color: #6a2e34; }
|
|
1472
|
+
.xf-cb { color: #ffe08a; border-color: #7a6424; }
|
|
1473
|
+
.xbadge { font: 600 11px/20px var(--sans); padding: 0 9px; border-radius: 10px; }
|
|
1474
|
+
.xbadge.ok { background: #143a2c; color: #8fe8c0; }
|
|
1475
|
+
.xbadge.warn { background: #3a2e14; color: #f0cf87; }
|
|
1476
|
+
.xpos { color: #8fe8c0; }
|
|
1477
|
+
.xneg { color: #f0a8ad; }
|
|
1478
|
+
.xok { color: #8fe8c0; }
|
|
1479
|
+
.xpager { display: flex; justify-content: center; gap: 18px; font: 12px var(--mono); }
|
|
1480
|
+
.xpager a { color: #8fd8b8; text-decoration: none; }
|
|
1481
|
+
.xtbl th.r { text-align: right; }
|
|
1482
|
+
.xflowwrap { background: #0b0e11; border: 1px solid var(--line-soft); border-radius: var(--radius-sm); padding: 4px 0; }
|
|
1483
|
+
.xflow { display: block; width: 100%; }
|
|
1484
|
+
.xflow path { vector-effect: non-scaling-stroke; }
|
|
1485
|
+
.xflow path:hover { stroke: rgba(255,255,255,0.6); stroke-width: 1; }
|
|
1486
|
+
.xlink { color: #8fd8b8; text-decoration: none; }
|
|
1487
|
+
.bxlink { color: inherit; text-decoration: none; }
|
|
1488
|
+
.xlink:hover, .bxlink:hover { text-decoration: underline; }
|
|
1489
|
+
|
|
1490
|
+
/* ---- markets (public/js/markets.js) ---- */
|
|
1491
|
+
.mkcard { display: flex; flex-direction: column; gap: 10px; }
|
|
1492
|
+
.mksum { display: flex; flex-wrap: wrap; gap: 6px 22px; font-family: var(--mono); font-size: 12px; align-items: baseline; }
|
|
1493
|
+
.mksum .k { color: var(--fg-faint); font-size: 10px; text-transform: uppercase; letter-spacing: .06em; margin-right: 7px; }
|
|
1494
|
+
.mksum b { font-size: 16px; color: var(--fg); font-weight: 650; }
|
|
1495
|
+
|
|
1496
|
+
/* The same line on Overview, squeezed (operator, 2026-09-13: "squeeze this line into the top of
|
|
1497
|
+
the Overview, between Sync status and Block Flow"). It sits between two dense panels, so it is
|
|
1498
|
+
tighter than the Markets page's copy and does not get a card of its own: it reads as a strip,
|
|
1499
|
+
like the sync hero above it. */
|
|
1500
|
+
.ovmksum {
|
|
1501
|
+
/* FULL WIDTH, like .ovcols. Without this it inherits the implicit placement of a .grid child --
|
|
1502
|
+
one of twelve 1fr tracks, about 120px -- and because .mksum wraps, the four figures stacked
|
|
1503
|
+
into a tall narrow column instead of a strip. The first cut shipped exactly that: a geometry
|
|
1504
|
+
check confirmed the vertical ORDER (hero, strip, Block flow) and never measured the width,
|
|
1505
|
+
which is the dimension that was wrong. */
|
|
1506
|
+
grid-column: 1 / -1;
|
|
1507
|
+
padding: 6px 12px;
|
|
1508
|
+
margin: -2px 0 2px;
|
|
1509
|
+
gap: 4px 20px;
|
|
1510
|
+
background: var(--bg-1);
|
|
1511
|
+
border: 1px solid var(--line-soft);
|
|
1512
|
+
border-radius: 6px;
|
|
1513
|
+
}
|
|
1514
|
+
.ovmksum b { font-size: 14px; }
|
|
1515
|
+
/* Each figure stays on one line: the label and its value are one unit, so "MEDIAN (USD)" must not
|
|
1516
|
+
break across two rows the way it did at a twelfth of the width. */
|
|
1517
|
+
.ovmksum > span { white-space: nowrap; }
|
|
1518
|
+
/* `[hidden]`-style collapse for whichever price view is off: .hidden is display:none elsewhere in
|
|
1519
|
+
this sheet, but .mklayout sets `display: grid` on the same element, and a later display wins. */
|
|
1520
|
+
.mklayout.hidden { display: none; }
|
|
1521
|
+
/* ONE COLUMN: the 250px track held the legend, and removing the legend without removing the track
|
|
1522
|
+
would trade a panel of prose for an empty strip -- which is the same mistake as leaving the
|
|
1523
|
+
space it was taking up. */
|
|
1524
|
+
.mklayout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
1525
|
+
.treemapwrap.mkboard { height: min(58vh, 620px); min-height: 340px; }
|
|
1526
|
+
/* (.mkside, .mkrow and .mksw dressed the board's legend and went with it.) */
|
|
1527
|
+
.mkn { display: inline-block; min-width: 36px; font-size: 10px; color: var(--fg-faint); }
|
|
1528
|
+
.mktbl td.stale { color: #e8c987; }
|
|
1529
|
+
.mktbl th.r { text-align: right; }
|
|
1530
|
+
.treemapwrap.mkboard canvas.treemap { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
1531
|
+
.mkbar { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; }
|
|
1532
|
+
.mkgrp { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
1533
|
+
.mkbtn { font: 12px var(--sans); padding: 4px 10px; border-radius: var(--radius-sm); background: var(--bg-2); color: var(--fg-dim); border: 1px solid var(--line); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
|
1534
|
+
.mkbtn.on { color: var(--fg); border-color: color-mix(in srgb, var(--ok) 55%, var(--line)); background: color-mix(in srgb, var(--ok) 14%, var(--bg-1)); }
|
|
1535
|
+
.mkdot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: #8aa0b4; }
|
|
1536
|
+
.mkdot[data-ex="coinbase"] { background: #4c8dff; }
|
|
1537
|
+
.mkdot[data-ex="kraken"] { background: #a78bfa; }
|
|
1538
|
+
.mkdot[data-ex="bitstamp"] { background: #2ecc8f; }
|
|
1539
|
+
.mkdot[data-ex="bitfinex"] { background: #4dd0e1; }
|
|
1540
|
+
.mkdot[data-ex="okx"] { background: #f0b429; }
|
|
1541
|
+
canvas.mkchart { width: 100%; height: min(54vh, 480px); min-height: 300px; display: block; background: var(--bg-0); /* the theme's well (2026-09-16): the flat chart and the depth chart draw their ink for the theme, so their ground follows too */ border: 1px solid var(--line-soft); border-radius: var(--radius-sm); cursor: crosshair; }
|
|
1542
|
+
.mksub { margin: 8px 0 0; font-size: 12px; font-weight: 600; color: var(--fg-dim); text-transform: uppercase; letter-spacing: .05em; }
|
|
1543
|
+
.mksub .faint { text-transform: none; letter-spacing: 0; font-weight: 400; }
|
|
1544
|
+
.treemapwrap.mkboard { height: min(40vh, 420px); min-height: 280px; }
|
|
1545
|
+
/* the board takes what the window leaves after the toolbar, the figures and the table-and-book row
|
|
1546
|
+
beneath (2026-09-15): at 945px tall that is 385, at 1080 it is 520, never more than 600 */
|
|
1547
|
+
.treemapwrap.mkboard { height: min(600px, max(340px, calc(100vh - 578px))); min-height: 340px; }
|
|
1548
|
+
.mkbottom { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 4px 16px; align-items: start; }
|
|
1549
|
+
.mkbottom-r .mksub { margin-top: 0; }
|
|
1550
|
+
@media (max-width: 900px) { .mkbottom { grid-template-columns: minmax(0, 1fr); } }
|
|
1551
|
+
/* sharing its row with the book, the table keeps every cell on one line and, under 1400px, drops
|
|
1552
|
+
the pair (BTC-USD throughout; OKX's USDT is in the note) and its two widest columns (the 24 h
|
|
1553
|
+
range and volume -- still in the Markets summary line and the Kiosk); the book's title loses its description at that width and its chart takes what is left */
|
|
1554
|
+
.mkbottom table.mktbl td, .mkbottom table.mktbl th { white-space: nowrap; }
|
|
1555
|
+
.mkbottom .scroll { max-height: none; overflow-x: auto; }
|
|
1556
|
+
@media (max-width: 1400px) { .mkbottom .mktbl .opt { display: none; } .mkbottom-r .mksub .faint { display: none; } }
|
|
1557
|
+
.mklab { font-size: 11px; color: var(--fg-faint); margin-right: 4px; align-self: center; }
|
|
1558
|
+
canvas.mkchart.mkdepth { height: min(420px, max(220px, calc(100vh - 725px))); min-height: 220px; }
|
|
1559
|
+
.mkbottom .note.tiny { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* the notes keep to one line beside each other; the full text is in the docs */
|
|
1560
|
+
|
|
1561
|
+
/* ---- explorer, round two (operator, 2026-09-11: "Surely we can make our display look at least as
|
|
1562
|
+
good as mempool. Do better. Make it beautiful") ---- */
|
|
1563
|
+
.xcard { font-family: var(--sans); gap: 10px; }
|
|
1564
|
+
.xclip { position: fixed; left: -9999px; top: 0; opacity: 0; }
|
|
1565
|
+
.xhero { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 2px 0 0; }
|
|
1566
|
+
.xhero h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.01em; color: #f4f7fb; display: flex; align-items: center; gap: 10px; }
|
|
1567
|
+
.xhero .xh { font-variant-numeric: tabular-nums; }
|
|
1568
|
+
.xhero .xnav { font-size: 20px; line-height: 1; padding: 4px 11px 6px; border-radius: 8px; border: 1px solid #2a3442; color: #9fb0c0; text-decoration: none; }
|
|
1569
|
+
.xhero .xnav:hover { color: #fff; border-color: #4fd1ff; }
|
|
1570
|
+
.xid { font: 500 16px var(--mono); color: #4fd1ff; word-break: break-all; user-select: all; -webkit-user-select: all; }
|
|
1571
|
+
.xid.sm { font-size: 13px; }
|
|
1572
|
+
.xcopy { background: none; border: 0; padding: 2px 4px; cursor: pointer; color: #6f8193; }
|
|
1573
|
+
.xcopy svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.4; vertical-align: -3px; }
|
|
1574
|
+
.xcopy:hover { color: #4fd1ff; }
|
|
1575
|
+
.xcopy.done { color: #3fdc8a; }
|
|
1576
|
+
.xgrow { flex: 1 1 auto; }
|
|
1577
|
+
.xpill { font: 600 14px var(--sans); padding: 7px 15px; border-radius: 7px; white-space: nowrap; }
|
|
1578
|
+
.xpill.ok { background: linear-gradient(180deg, #1c9a5d, #137a48); color: #eafff4; box-shadow: 0 0 18px rgba(40,220,140,0.18); }
|
|
1579
|
+
.xpill.warn { background: linear-gradient(180deg, #e2434f, #c52f3b); color: #fff; box-shadow: 0 0 18px rgba(240,60,70,0.22); }
|
|
1580
|
+
.xcards { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 18px; background: #121923; border-radius: 12px; border: 1px solid #1d2733; }
|
|
1581
|
+
@media (max-width: 1000px) { .xcards { grid-template-columns: 1fr; } }
|
|
1582
|
+
.xpanel { display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; }
|
|
1583
|
+
.xrow { display: grid; grid-template-columns: 170px minmax(0, 1fr); align-items: center; gap: 12px; padding: 13px 20px; background: #0b1017; font-size: 15px; }
|
|
1584
|
+
.xrow:nth-child(even) { background: #0f161f; }
|
|
1585
|
+
.xk { color: #c3cfdb; }
|
|
1586
|
+
.xv { color: #f2f6fa; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
1587
|
+
.xv small, .xamt small, .xtotpill small, table.xtable small { color: #7d8b99; font-size: 12px; margin-left: 2px; }
|
|
1588
|
+
.xusd { color: #6fe37a; font-weight: 600; margin-left: 12px; }
|
|
1589
|
+
.xdim { color: #7d8b99; }
|
|
1590
|
+
.xago { color: #7d8b99; margin-left: 10px; }
|
|
1591
|
+
.xcard .xv a, .xcard a.xaddr, .xcard table.xtable a, .xcard .xpager a { color: #4fd1ff; text-decoration: none; }
|
|
1592
|
+
.xcard .xv a:hover, .xcard a.xaddr:hover, .xcard table.xtable a:hover { text-decoration: underline; }
|
|
1593
|
+
.xsect { display: flex; align-items: baseline; gap: 14px; margin: 14px 0 0; }
|
|
1594
|
+
.xsect h2 { margin: 0; font-size: 25px; font-weight: 700; color: #f4f7fb; letter-spacing: -0.01em; }
|
|
1595
|
+
.xflowcard { padding: 18px; background: #121923; border-radius: 12px; border: 1px solid #1d2733; }
|
|
1596
|
+
.xflowcard .xflow { display: block; width: 100%; background: radial-gradient(ellipse at center, #0d1a2e 0%, #080c13 70%); border-radius: 8px; }
|
|
1597
|
+
.xflow path { transition: filter .15s ease; }
|
|
1598
|
+
.xflow a:hover path, .xflow path:hover { filter: brightness(1.35) drop-shadow(0 0 6px rgba(90,160,255,0.75)); }
|
|
1599
|
+
.xiocard { padding: 16px 20px; background: #121923; border-radius: 12px; border: 1px solid #1d2733; }
|
|
1600
|
+
.xio2 { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
|
|
1601
|
+
@media (max-width: 1000px) { .xio2 { grid-template-columns: 1fr; } }
|
|
1602
|
+
.xiohead { font-size: 12px; text-transform: uppercase; letter-spacing: .09em; color: #7d8b99; margin: 2px 0 6px; }
|
|
1603
|
+
.xiohead span { color: #c3cfdb; margin-left: 4px; }
|
|
1604
|
+
.xitem { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 10px 4px; border-bottom: 1px solid #1a2330; font-size: 15px; }
|
|
1605
|
+
.xiocol:last-child .xitem { grid-template-columns: minmax(0, 1fr) auto 24px; }
|
|
1606
|
+
.xitem:hover { background: #151e2a; }
|
|
1607
|
+
.xlabel { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1608
|
+
.xamt { font-variant-numeric: tabular-nums; color: #f2f6fa; text-align: right; white-space: nowrap; }
|
|
1609
|
+
.xmore { padding: 8px 4px; }
|
|
1610
|
+
.xico-wrap { display: inline-flex; width: 22px; height: 22px; align-items: center; justify-content: center; }
|
|
1611
|
+
.xico { width: 22px; height: 22px; }
|
|
1612
|
+
.xico circle { fill: #3a4656; transition: fill .15s; }
|
|
1613
|
+
.xico path { fill: none; stroke: #fff; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
1614
|
+
.xico-wrap.in .xico circle { fill: #e8522f; }
|
|
1615
|
+
.xico-wrap.in:hover .xico circle { fill: #ff6b45; }
|
|
1616
|
+
.xico-wrap.cb .xico circle { fill: #d9a200; }
|
|
1617
|
+
.xico-wrap.out .xico circle { fill: #55647a; }
|
|
1618
|
+
.xico-wrap.out:hover .xico circle { fill: #4fa3ff; }
|
|
1619
|
+
.xico-wrap.unspent i { width: 10px; height: 10px; border-radius: 50%; background: #3fdc8a; box-shadow: 0 0 9px #3fdc8a; }
|
|
1620
|
+
.xtotals { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; gap: 12px; }
|
|
1621
|
+
.xtot { color: #9fb0c0; font-size: 14px; }
|
|
1622
|
+
.xtotpill { background: linear-gradient(90deg, #1c6cff, #2f8fff); color: #fff; padding: 8px 18px; border-radius: 9px; font: 600 15px var(--sans); font-variant-numeric: tabular-nums; box-shadow: 0 0 22px rgba(40,130,255,0.25); }
|
|
1623
|
+
.xtotpill small { color: #d6e6ff; }
|
|
1624
|
+
.xtotusd { color: #c8ffd0; margin-left: 12px; }
|
|
1625
|
+
.xcard .xf { display: inline-block; margin: 0 5px 2px 0; padding: 1px 8px; border-radius: 4px; font: 700 12px/19px var(--sans); color: #fff; border: 0; }
|
|
1626
|
+
.xcard .xf-segwit { background: #1d9a4a; }
|
|
1627
|
+
.xcard .xf-taproot { background: #7b3fe4; }
|
|
1628
|
+
.xcard .xf-rbf { background: #1d9a4a; }
|
|
1629
|
+
.xcard .xf-consolidation { background: #0f8a8a; }
|
|
1630
|
+
.xcard .xf-op_return { background: #59616f; }
|
|
1631
|
+
.xcard .xf-cb { background: #c28f00; }
|
|
1632
|
+
.xtablecard { background: #121923; border-radius: 12px; border: 1px solid #1d2733; padding: 2px 12px 6px; max-height: calc(100vh - 292px); min-height: 160px; overflow: auto; }
|
|
1633
|
+
table.xtable { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
1634
|
+
table.xtable th { text-align: left; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: #7d8b99; font-weight: 600; padding: 9px 8px 8px; border-bottom: 1px solid #1d2733; position: sticky; top: 0; background: #121923; }
|
|
1635
|
+
table.xtable th.r, table.xtable td.r { text-align: right; }
|
|
1636
|
+
table.xtable td { padding: 8px 8px; border-bottom: 1px solid #19222e; font-variant-numeric: tabular-nums; color: #e6edf3; white-space: nowrap; }
|
|
1637
|
+
table.xtable tr:hover td { background: #16202c; }
|
|
1638
|
+
.xcard .xpager { margin-top: 4px; font-family: var(--sans); font-size: 14px; }
|
|
1639
|
+
/* packed (operator, 2026-09-15: "same treatment on the explorer tab"): the strip's padding and the
|
|
1640
|
+
scrollbar's band under it, the title's size, the table's row height, the card's gap -- 70px back
|
|
1641
|
+
to the table, which reaches the window's bottom edge */
|
|
1642
|
+
.xblocks { display: flex; gap: 22px; overflow-x: auto; padding: 8px 8px 10px; scrollbar-width: thin; }
|
|
1643
|
+
.xblk { flex: 0 0 auto; width: 128px; text-decoration: none; color: #fff; display: flex; flex-direction: column; align-items: center; gap: 9px; }
|
|
1644
|
+
.xblk-h { color: #4fd1ff; font: 600 15px var(--sans); font-variant-numeric: tabular-nums; }
|
|
1645
|
+
.xblk-face { position: relative; width: 124px; height: 124px; box-sizing: border-box; padding: 10px 8px; display: flex; flex-direction: column; justify-content: space-between; align-items: center; text-align: center; background: linear-gradient(180deg, #5b6b7d, #3a4757); transition: transform .18s ease, filter .18s ease; }
|
|
1646
|
+
/* THE ISOMETRIC BLOCK: a top face and a right face, both 10px deep (2026-09-12, operator: "the
|
|
1647
|
+
rendering of the blocks here is broken AF"). The two must share their edges with the card and
|
|
1648
|
+
with each other, or the corner tears open:
|
|
1649
|
+
top bottom edge = the card's top edge -> left: 0; right: 0 (skewX carries the top edge right by 10)
|
|
1650
|
+
right left edge = the card's right edge -> top: 0; bottom: 0 (skewY carries the right edge up by 10)
|
|
1651
|
+
They were inset 5px on two sides each, so the top face sat 5px right of the card and the right
|
|
1652
|
+
face 5px above it: a sliver at the top left, a 5px gap at the bottom right, and a dark wedge at
|
|
1653
|
+
the top right where neither face reached the corner. */
|
|
1654
|
+
.xblk-face::before { content: ''; position: absolute; left: 0; right: 0; top: -10px; height: 10px; background: inherit; filter: brightness(1.3); transform: skewX(-45deg); transform-origin: bottom left; }
|
|
1655
|
+
.xblk-face::after { content: ''; position: absolute; top: 0; bottom: 0; right: -10px; width: 10px; background: inherit; filter: brightness(0.7); transform: skewY(-45deg); transform-origin: top left; }
|
|
1656
|
+
.xblk:hover .xblk-face { transform: translateY(-4px); filter: brightness(1.15); }
|
|
1657
|
+
.xblk-face b { font-size: 15px; font-weight: 700; }
|
|
1658
|
+
.xblk-face em { font-style: normal; font-size: 19px; font-weight: 700; }
|
|
1659
|
+
.xblk-face small { font-size: 11.5px; color: rgba(255,255,255,0.85); }
|
|
1660
|
+
.xblk-pool { font-size: 12px; color: #9fb0c0; }
|
|
1661
|
+
.xblk.f1 .xblk-face { background: linear-gradient(180deg, #2fae6c, #1c7e4c); }
|
|
1662
|
+
.xblk.f2 .xblk-face { background: linear-gradient(180deg, #6bb843, #478a2a); }
|
|
1663
|
+
.xblk.f3 .xblk-face { background: linear-gradient(180deg, #a9bd35, #7c8b22); }
|
|
1664
|
+
.xblk.f4 .xblk-face { background: linear-gradient(180deg, #d9b52e, #a8841b); }
|
|
1665
|
+
.xblk.f5 .xblk-face { background: linear-gradient(180deg, #e38a2d, #b0631b); }
|
|
1666
|
+
.xblk.f6 .xblk-face { background: linear-gradient(180deg, #db543c, #a93624); }
|
|
1667
|
+
.xblk.f7 .xblk-face { background: linear-gradient(180deg, #b8347f, #86205a); }
|
|
1668
|
+
|
|
1669
|
+
/* ---- viewer modes (mining.js VIEWER_MODES) ---- */
|
|
1670
|
+
.viewer-ctl .viewer-mode { display: inline-flex; margin-right: 6px; border: 1px solid rgba(60,255,150,0.3); border-radius: 3px; overflow: hidden; }
|
|
1671
|
+
.viewer-mode button { font: 11px var(--mono); padding: 3px 9px; background: rgba(2,12,8,0.82); color: #8fd8b8; border: 0; cursor: pointer; }
|
|
1672
|
+
.viewer-mode button + button { border-left: 1px solid rgba(60,255,150,0.3); }
|
|
1673
|
+
.viewer-mode button.on { background: #1d5a40; color: #eafff4; }
|
|
1674
|
+
.viewer-mode button:hover:not(.on) { color: #eafff4; }
|
|
1675
|
+
|
|
1676
|
+
/* ---- kiosk (public/js/kiosk.js) ---- */
|
|
1677
|
+
/* THE PAGE SECTION HAS TO HAVE A HEIGHT TOO, or the kiosk cannot fill it: main is a definite
|
|
1678
|
+
grid row, but .kiosk sits inside section.page, whose height is auto -- so height: 100% fell back
|
|
1679
|
+
to min-height and gave a 480 px kiosk in a 782 px window (2026-09-11). The vh fallback covers a
|
|
1680
|
+
browser without :has(), with room for the bar in both its heights (46 px, or 69 px wrapped). */
|
|
1681
|
+
section.page:has(> .kiosk) { height: 100%; }
|
|
1682
|
+
.kiosk { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; height: 100%; min-height: min(calc(100vh - 130px), 900px); } /* 100% of main: the bar wraps below 1400 px, so its height is not a constant */
|
|
1683
|
+
.kiosk:fullscreen { height: 100vh; padding: 10px; box-sizing: border-box; background: #010306; }
|
|
1684
|
+
/* THE WALL STAYS A WALL DOWN TO 900 px (operator, 2026-09-12: "Try to improve kiosk view for this
|
|
1685
|
+
resolution", at 1066x760). At 1100 the kiosk dropped to ONE column and `height: auto`, so the
|
|
1686
|
+
panels stacked at 70vh each: measured at 1066x760 the grid came out 1730 px tall in a 760 px
|
|
1687
|
+
viewport -- 1052 px past the fold, a scroll rather than a display. Two columns fit that window
|
|
1688
|
+
with room, so the stack is for genuinely narrow screens now. */
|
|
1689
|
+
@media (max-width: 900px) { .kiosk { grid-template-columns: minmax(0, 1fr); height: auto; } .treemapwrap.kboard { height: 70vh; } }
|
|
1690
|
+
.kpanel { display: flex; flex-direction: column; min-height: 0; background: #04080c; border: 1px solid #16222c; border-radius: 10px; overflow: hidden; }
|
|
1691
|
+
.khead { padding: 5px 12px; font: 12px var(--sans); color: #c3cfdb; letter-spacing: .06em; text-transform: uppercase; }
|
|
1692
|
+
.khead .faint { text-transform: none; letter-spacing: 0; margin-left: 10px; }
|
|
1693
|
+
.treemapwrap.kboard { flex: 1 1 auto; min-height: 0; }
|
|
1694
|
+
.treemapwrap.kboard canvas.treemap { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; border-radius: 0; }
|
|
1695
|
+
.kbf .khead { display: flex; align-items: center; gap: 10px; }
|
|
1696
|
+
.kbf .khead .sp { flex: 1; }
|
|
1697
|
+
.kfull { position: static; font: 12px var(--mono); padding: 3px 10px; text-transform: none; letter-spacing: 0; background: rgba(2,12,8,0.82); color: #8fd8b8; border: 1px solid rgba(60,255,150,0.3); border-radius: 4px; cursor: pointer; }
|
|
1698
|
+
.kfull:hover { color: #eafff4; }
|
|
1699
|
+
|
|
1700
|
+
/* ---- the kiosk's price information panel (markets.js priceInfoHtml) ---- */
|
|
1701
|
+
.kprice { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "main" "foot"; gap: 3px 22px; padding: 8px 14px 4px; border-top: 1px solid #16222c; background: linear-gradient(180deg, #060b10, #04080c); font-family: var(--sans); }
|
|
1702
|
+
/* THE DEPTH CHART FILLS WHAT IS LEFT of the price panel (operator, 2026-09-12: "change to market
|
|
1703
|
+
order depth chart that fits within the view"). The panel is a flex column inside a fixed kiosk
|
|
1704
|
+
grid row, so `flex: 1 1 auto` + `min-height: 0` is what makes the canvas take the remainder
|
|
1705
|
+
instead of overflowing it -- without min-height a flex item refuses to shrink below its content. */
|
|
1706
|
+
.kdepthwrap { flex: 1 1 auto; min-height: 90px; position: relative; background: #04080c; }
|
|
1707
|
+
canvas.kdepth { position: absolute; inset: 0; width: 100%; height: 100%; display: block; border: 0; }
|
|
1708
|
+
.kp-wait { color: #7d8b99; padding: 8px 0; }
|
|
1709
|
+
.kp-main { grid-area: main; display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
|
|
1710
|
+
.kp-pair { font-size: 12px; letter-spacing: .09em; color: #8fa0b1; }
|
|
1711
|
+
.kp-price { font-size: 40px; font-weight: 700; line-height: 1.05; color: #ffec28; text-shadow: 0 0 16px rgba(255,220,40,0.45); font-variant-numeric: tabular-nums; }
|
|
1712
|
+
.kp-chg { font-size: 18px; font-weight: 600; }
|
|
1713
|
+
.kp-chg small { font-size: 11px; color: #7d8b99; font-weight: 400; }
|
|
1714
|
+
.kp-stats { grid-area: stats; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
|
1715
|
+
.kp-stats div { display: flex; flex-direction: column; gap: 3px; }
|
|
1716
|
+
.kp-stats span { font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: #7d8b99; }
|
|
1717
|
+
.kp-stats b { font-size: 15px; font-weight: 600; color: #eef4f8; font-variant-numeric: tabular-nums; }
|
|
1718
|
+
.kp-ex { grid-area: ex; width: 100%; border-collapse: collapse; font-size: 13px; align-self: center; }
|
|
1719
|
+
.kp-ex td { padding: 3px 4px; color: #dfe6ee; font-variant-numeric: tabular-nums; border-bottom: 1px solid #111b24; }
|
|
1720
|
+
.kp-ex td.r { text-align: right; }
|
|
1721
|
+
.kp-foot { grid-area: foot; font-size: 11px; color: #6c7a88; }
|
|
1722
|
+
|
|
1723
|
+
/* the kiosk as three panels: markets over price on the left, block space the full height on the right */
|
|
1724
|
+
.kiosk { grid-template-rows: minmax(0, 1fr) auto; grid-template-areas: "mk sp" "pp sp"; }
|
|
1725
|
+
.kiosk .kmk { grid-area: mk; }
|
|
1726
|
+
.kiosk .kpp { grid-area: pp; }
|
|
1727
|
+
.kiosk .ksp { grid-area: sp; }
|
|
1728
|
+
.kpp .kprice { border-top: 0; padding-top: 4px; }
|
|
1729
|
+
@media (max-width: 900px) { .kiosk { grid-template-rows: auto; grid-template-areas: "mk" "pp" "sp"; } }
|
|
1730
|
+
|
|
1731
|
+
/* the kiosk's fourth panel: Block flow under Block space */
|
|
1732
|
+
.kiosk { grid-template-areas: "mk sp" "pp bf"; }
|
|
1733
|
+
.kiosk .kbf { grid-area: bf; }
|
|
1734
|
+
.kbf .flowwrap { padding: 4px 10px 10px; overflow-x: auto; }
|
|
1735
|
+
@media (max-width: 900px) { .kiosk { grid-template-areas: "mk" "pp" "sp" "bf"; } }
|
|
1736
|
+
|
|
1737
|
+
/* THE KIOSK AS TWO INDEPENDENT COLUMNS (operator, 2026-09-11: "Kiosk mode needs to shrink down that
|
|
1738
|
+
particular block flow vertically, and prevent it from resizing when text overflows bounds").
|
|
1739
|
+
Price and Block flow shared one grid row, so a notice line in Block flow grew the row and
|
|
1740
|
+
shrank both boards. Each column is now its own stack; Block flow is a fixed, clipped strip,
|
|
1741
|
+
its cards drawn at 80% (zoom shrinks the layout box too, so the row's parking maths holds),
|
|
1742
|
+
and its notice and timing legend are left to the Overview. */
|
|
1743
|
+
.kiosk { grid-template-rows: minmax(0, 1fr); grid-template-areas: none; }
|
|
1744
|
+
.kcol { display: flex; flex-direction: column; gap: 8px; min-height: 0; min-width: 0; } /* 8, not 12: the same packing every tab got (2026-09-15) */
|
|
1745
|
+
.kcol > .kmk, .kcol > .ksp { flex: 1 1 auto; min-height: 0; }
|
|
1746
|
+
.kcol > .kpp { flex: 0 0 auto; }
|
|
1747
|
+
.kcol > .kbf { flex: 0 0 auto; height: 170px; } /* the tight flow (.flowtight on the panel): its cards are ~120 at 80% zoom, plus the head and the strip's padding */
|
|
1748
|
+
/* The price-and-depth panel is sized the same way and for the same reason: a chart cannot be read
|
|
1749
|
+
in whatever a flex item has left over, so it gets a definite height and the markets board above
|
|
1750
|
+
takes the remainder. */
|
|
1751
|
+
.kcol > .kpp { flex: 0 0 auto; height: 270px; }
|
|
1752
|
+
.kbf .flowwrap { flex: 1 1 auto; min-height: 0; overflow-x: auto; overflow-y: hidden; padding: 2px 10px 6px; }
|
|
1753
|
+
.kbf .flowwrap > .flow { zoom: 0.8; }
|
|
1754
|
+
.kbf .flownote, .kbf .tiplegend { display: none; }
|
|
1755
|
+
/* TWO COLUMNS FOR AS LONG AS THERE IS ROOM (operator, 2026-09-11: "Kiosk is absolutely fucked
|
|
1756
|
+
now"). Stacking from 1400 px was wrong: a 1350 px window, which had been two columns, became one
|
|
1757
|
+
tall column that had to be scrolled. It stacks below 1100 px as it always did; between 1100 and
|
|
1758
|
+
1400 it stays two columns and gives the space back instead -- tighter gaps and a shorter Block
|
|
1759
|
+
flow strip -- which is what "doesn't look proper on a smaller window" was about. */
|
|
1760
|
+
@media (max-width: 1400px) and (min-width: 901px) {
|
|
1761
|
+
.kiosk { gap: 8px; }
|
|
1762
|
+
.kcol { gap: 8px; }
|
|
1763
|
+
.kcol > .kbf { height: 170px; }
|
|
1764
|
+
.kbf .flowwrap > .flow { zoom: 0.7; }
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/* the viewer controls in the panel heading, off the board (mining.js modeSwitch) */
|
|
1768
|
+
.viewer-ctl.in-head { position: static; display: inline-flex; gap: 6px; align-items: center; margin-left: 12px; flex: 0 0 auto; text-transform: none; letter-spacing: 0; font-weight: 400; vertical-align: middle; }
|
|
1769
|
+
.khead { display: flex; align-items: center; }
|
|
1770
|
+
.khead .viewer-ctl.in-head { margin-left: auto; }
|
|
1771
|
+
|
|
1772
|
+
/* NOTHING JITTERS (operator, 2026-09-11: "Lock the "next refresh" text field in place so it doesnt
|
|
1773
|
+
shift the elements around. In general, lock down elements so they don't shift others around
|
|
1774
|
+
jittering the view"). Numerals are tabular everywhere, so a ticking number never changes width;
|
|
1775
|
+
live labels hold a reserved width and truncate rather than push their neighbours. */
|
|
1776
|
+
body { font-variant-numeric: tabular-nums; }
|
|
1777
|
+
.viewer-ctl .refresh-in { display: inline-block; min-width: 17ch; text-align: right; white-space: nowrap; font: 11px var(--mono); }
|
|
1778
|
+
.viewer-ctl .refresh-now { flex: 0 0 auto; min-width: 12ch; }
|
|
1779
|
+
#sseState { display: inline-block; min-width: 12ch; }
|
|
1780
|
+
.khead { min-width: 0; }
|
|
1781
|
+
.khead b { flex: 0 0 auto; }
|
|
1782
|
+
.khead .faint { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1783
|
+
.kp-chg { display: inline-block; min-width: 9ch; }
|
|
1784
|
+
.card > h3 .src { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1785
|
+
|
|
1786
|
+
/* Block flow: drag the row (mining.js dragScroll); the scrollbar goes */
|
|
1787
|
+
.flowwrap { cursor: grab; user-select: none; -webkit-user-select: none; scrollbar-width: none; }
|
|
1788
|
+
.flowwrap::-webkit-scrollbar { display: none; }
|
|
1789
|
+
.flowwrap.dragging { cursor: grabbing; scroll-snap-type: none; }
|
|
1790
|
+
.flowwrap.dragging a { pointer-events: none; }
|
|
1791
|
+
|
|
1792
|
+
/* ---- display settings (settings.js, index.html #settingsWrap) ----
|
|
1793
|
+
A panel over the page rather than a page of its own: it is changed while looking at the thing
|
|
1794
|
+
it changes, and every control applies live. No inline styles anywhere -- CSP forbids them, and
|
|
1795
|
+
the range fills are drawn by the browser's own widget. */
|
|
1796
|
+
.cfgwrap { position: fixed; inset: 0; z-index: 60; display: flex; justify-content: flex-end; }
|
|
1797
|
+
.cfgscrim { position: absolute; inset: 0; background: var(--scrim); }
|
|
1798
|
+
.cfg {
|
|
1799
|
+
position: relative; width: min(470px, 100%); height: 100%; overflow: auto; /* 470, not 420 (2026-09-15): the hints wrap a line less, the switches two across have room */
|
|
1800
|
+
background: var(--bg-1); border-left: 1px solid var(--line); box-shadow: -18px 0 40px color-mix(in srgb, var(--bg) 55%, transparent);
|
|
1801
|
+
padding: 14px 16px 28px;
|
|
1802
|
+
}
|
|
1803
|
+
.cfg h2 { display: flex; align-items: center; gap: 8px; margin: 2px 0 8px; font-size: 13px; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-dim); }
|
|
1804
|
+
.cfg h2 .sp { flex: 1; }
|
|
1805
|
+
.cfgnote { margin: 0 0 10px; color: var(--fg-faint); font-size: 11px; line-height: 1.45; }
|
|
1806
|
+
.cfgrow input[type="color"] { width: 44px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; cursor: pointer; }
|
|
1807
|
+
/* the tab strip (operator: "tabbed section panel in setup") */
|
|
1808
|
+
.cfgtabs { display: flex; flex-direction: column; gap: 4px; margin: 0 0 8px; }
|
|
1809
|
+
/* labelled rows (2026-09-16): the boards, their effects, the games last */
|
|
1810
|
+
.cfgtabrow { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
1811
|
+
.cfgtablbl { flex: 0 0 66px; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-faint); }
|
|
1812
|
+
.cfgtab { font: inherit; font-size: 11px; letter-spacing: .04em; padding: 5px 10px; border-radius: 999px;
|
|
1813
|
+
border: 1px solid var(--line); background: transparent; color: var(--fg-dim); cursor: pointer; }
|
|
1814
|
+
.cfgtab:hover { color: var(--fg); }
|
|
1815
|
+
.cfgtab.on { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
1816
|
+
.cfgbulk { display: flex; gap: 6px; margin: 0 0 8px; }
|
|
1817
|
+
.cfgbulk .btn { font-size: 10.5px; padding: 3px 8px; }
|
|
1818
|
+
.cfggroup { border: 1px solid var(--line-soft); border-radius: var(--radius); padding: 8px 10px 10px; margin-bottom: 10px; background: var(--bg-2); }
|
|
1819
|
+
.cfggroup > h3 { margin: 0 0 4px; font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--accent); }
|
|
1820
|
+
.cfggroup > p { margin: 0 0 8px; color: var(--fg-faint); font-size: 11px; line-height: 1.4; }
|
|
1821
|
+
/* packed (2026-09-15): rows 5px not 7, hints tighter, and on the effects tabs the switches two across
|
|
1822
|
+
with the hint as the row's tooltip */
|
|
1823
|
+
.cfgrow { display: grid; grid-template-columns: 1fr auto; gap: 3px 10px; align-items: center; padding: 5px 0; border-top: 1px solid var(--line-soft); }
|
|
1824
|
+
.cfgrows2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; border-top: 1px solid var(--line-soft); margin-top: 4px; }
|
|
1825
|
+
.cfgrow.compact { padding: 4px 0; cursor: help; }
|
|
1826
|
+
.cfgrows2 .cfgrow.compact:nth-child(-n+2) { border-top: 0; }
|
|
1827
|
+
.cfgrow:first-of-type { border-top: 0; }
|
|
1828
|
+
.cfgrow b { font-weight: 600; font-size: 12px; }
|
|
1829
|
+
.cfgrow i { grid-column: 1 / -1; font-style: normal; color: var(--fg-faint); font-size: 10.5px; line-height: 1.35; }
|
|
1830
|
+
.cfgrow select, .cfgrow input[type="range"] { accent-color: var(--accent); }
|
|
1831
|
+
.cfgrow select { background: var(--bg-3, #12161c); color: var(--fg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 3px 6px; font: inherit; font-size: 11.5px; }
|
|
1832
|
+
.cfgrow input[type="range"] { width: 130px; }
|
|
1833
|
+
/* A FIXED BOX, not a minimum (operator, 2026-09-14: "these sliders jump around when changing values").
|
|
1834
|
+
min-width: 3ch let a longer value widen the grid's control column and slide the range input under
|
|
1835
|
+
the pointer. 6.5ch holds the widest value any slider prints (formatRangeValue: "0.0010", "2.00"),
|
|
1836
|
+
tabular digits keep every digit the same width, and the pair never wraps. */
|
|
1837
|
+
.cfgrow .cfgrange { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
|
|
1838
|
+
.cfgrow .val { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); display: inline-block; width: 6.5ch; text-align: right; font-variant-numeric: tabular-nums; }
|
|
1839
|
+
/* the toggle: a checkbox the browser draws, sized up, so it is keyboard-reachable for free */
|
|
1840
|
+
.cfgrow input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
|
|
1841
|
+
/* THE EXPLORER ON A LIGHT THEME (2026-09-16): its block pages are their own dark design, kept as they
|
|
1842
|
+
are; only what sits on the page itself -- the section titles, the hero's id and its back link --
|
|
1843
|
+
takes the theme's ink, and only when the theme is light, so the shipped look is untouched. */
|
|
1844
|
+
:root[data-theme="light"] .xhero h1, :root[data-theme="light"] .xsect h2 { color: var(--fg); }
|
|
1845
|
+
:root[data-theme="light"] .xhero .xnav { color: var(--fg-dim); border-color: var(--line); }
|
|
1846
|
+
:root[data-theme="light"] .xhero .xnav:hover { color: var(--fg); border-color: var(--accent); }
|
|
1847
|
+
:root[data-theme="light"] .xid, :root[data-theme="light"] .xblk-h { color: var(--info); }
|
|
1848
|
+
:root[data-theme="light"] .xcopy { color: var(--fg-faint); }
|
|
1849
|
+
:root[data-theme="light"] .xhero .xdim, :root[data-theme="light"] .xhero .xago, :root[data-theme="light"] .xtot { color: var(--fg-dim); }
|
|
1850
|
+
/* THE KIOSK ON A LIGHT THEME: its frames, heads and the price panel take the theme's panel, line and
|
|
1851
|
+
ink (the price loses its neon glow: a yellow that lights up a black panel is unreadable on white);
|
|
1852
|
+
the boards inside stay space. Light only, so the shipped kiosk is untouched. */
|
|
1853
|
+
:root[data-theme="light"] .kpanel { background: var(--bg-1); border-color: var(--line); }
|
|
1854
|
+
:root[data-theme="light"] .khead { color: var(--fg-dim); }
|
|
1855
|
+
:root[data-theme="light"] .kprice { background: var(--bg-1); border-top-color: var(--line); }
|
|
1856
|
+
:root[data-theme="light"] .kdepthwrap { background: var(--bg-0); }
|
|
1857
|
+
:root[data-theme="light"] .kp-wait, :root[data-theme="light"] .kp-pair { color: var(--fg-dim); }
|
|
1858
|
+
:root[data-theme="light"] .kp-price { color: var(--accent); text-shadow: none; }
|
|
1859
|
+
/* APPEARANCE (2026-09-16): the mode as a segmented control, the themes as cards -- a swatch strip of
|
|
1860
|
+
the face the mode picks, the name, a line about it -- and the nine custom pickers dimmed until
|
|
1861
|
+
Custom is the theme. Each card carries its face's page, text and line colours as custom properties
|
|
1862
|
+
(set through the CSSOM), so the card previews the theme rather than describing it. */
|
|
1863
|
+
.cfgseg { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; background: var(--bg-3); }
|
|
1864
|
+
.cfgsegbtn { font: inherit; font-size: 11px; padding: 4px 11px; border: 0; background: transparent; color: var(--fg-dim); cursor: pointer; }
|
|
1865
|
+
.cfgsegbtn + .cfgsegbtn { border-left: 1px solid var(--line); }
|
|
1866
|
+
.cfgsegbtn:hover { color: var(--fg); }
|
|
1867
|
+
.cfgsegbtn.on { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
|
|
1868
|
+
.cfgrow.cfgcards { grid-template-columns: 1fr auto; }
|
|
1869
|
+
.thcards { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 2px 0 1px; }
|
|
1870
|
+
.thcard { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; text-align: left; font: inherit; cursor: pointer;
|
|
1871
|
+
padding: 7px 9px 8px; border-radius: var(--radius-sm); border: 1px solid var(--th-line, var(--line)); background: var(--th-bg, var(--bg-3)); color: var(--th-fg, var(--fg)); }
|
|
1872
|
+
.thcard:hover { border-color: var(--fg-dim); }
|
|
1873
|
+
.thcard.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
1874
|
+
.thcard b { font-size: 12px; font-weight: 600; }
|
|
1875
|
+
.thcard small { font-size: 10.5px; opacity: .72; line-height: 1.3; }
|
|
1876
|
+
.thsw { display: inline-flex; gap: 0; border-radius: 4px; overflow: hidden; border: 1px solid var(--th-line, var(--line)); }
|
|
1877
|
+
.thsw i { display: block; width: 14px; height: 12px; }
|
|
1878
|
+
.cfgcustomise { font-size: 10.5px; padding: 3px 8px; }
|
|
1879
|
+
.cfgrows2 .cfgrow.compact input[type="color"] { width: 36px; height: 20px; }
|
|
1880
|
+
.cfgrow.idle { opacity: .55; }
|
|
1881
|
+
.cfgrow.idle:hover { opacity: 1; }
|
|
1882
|
+
/* DIVERSIONS: a pop-down at the end of the nav. The buttons inside are `nav.pages button` like
|
|
1883
|
+
every other tab, so they inherit the tab look and only the layout is new. */
|
|
1884
|
+
/* PINNED, because the nav SCROLLS. `nav.pages` is overflow-x:auto with its scrollbar hidden, so
|
|
1885
|
+
at a wide window the last tabs simply leave the box -- measured at 1600px: the nav ended at
|
|
1886
|
+
"Eve(nts)" and this menu was nowhere on screen. Sticky to the right edge keeps it in place while
|
|
1887
|
+
the tabs scroll underneath, and the short fade stops them appearing to slide out of a hard edge
|
|
1888
|
+
(the header's own gradient ends at --bg-1). */
|
|
1889
|
+
/* NOT STICKY, and not inside the scrolling nav (operator, Safari on macOS: "still doesn't work on
|
|
1890
|
+
safari. not clickable"). This was `position: sticky; right: 0` inside `nav.pages`, an
|
|
1891
|
+
overflow-x:auto container. WebKit paints a sticky element where it sticks but hit-tests it where
|
|
1892
|
+
layout first put it, so the button showed at the right edge and ignored every click -- in that
|
|
1893
|
+
engine alone. It is a plain header flex item now, which needs no sticky at all. */
|
|
1894
|
+
.navmenu { z-index: 5; display: inline-flex; align-items: center; flex: 0 0 auto; margin-left: 6px; }
|
|
1895
|
+
.navcaret { font-size: 9px; opacity: .8; }
|
|
1896
|
+
/* ...and the panel is FIXED, not absolute: `nav.pages` clips it on the block axis (a box with
|
|
1897
|
+
overflow-x:auto clips the other axis too) and `header.top` is overflow:hidden besides, so an
|
|
1898
|
+
absolutely positioned panel inside either of them is invisible. app.js sets --x/--y from the
|
|
1899
|
+
button's own rect through the CSSOM; there are no style attributes anywhere in this project. */
|
|
1900
|
+
/* NO TRANSFORM. This was `left: <button's right edge>` plus `translateX(-100%)` to pull the panel
|
|
1901
|
+
back by its own width. Two things I cannot test from here were riding on that, so both are gone:
|
|
1902
|
+
app.js now measures the panel and sets its real left edge. See index.html for why the panel is a
|
|
1903
|
+
child of <body> rather than of the header. */
|
|
1904
|
+
.navmenu-pop { position: fixed; left: var(--x, 0); top: var(--y, 0);
|
|
1905
|
+
z-index: 200; display: flex; flex-direction: column; align-items: stretch;
|
|
1906
|
+
min-width: 160px; padding: 5px; gap: 2px; background: var(--bg-2); border: 1px solid var(--line);
|
|
1907
|
+
border-radius: var(--radius); box-shadow: 0 12px 30px #000a; }
|
|
1908
|
+
.navmenu-pop.hidden { display: none; }
|
|
1909
|
+
.navmenu-pop button { text-align: left; width: 100%; }
|
|
1910
|
+
.btn.gear { display: inline-flex; align-items: center; justify-content: center; padding: 4px 7px; }
|
|
1911
|
+
.btn.gear svg { display: block; }
|
|
1912
|
+
.btn.gear.on { color: var(--accent); border-color: var(--accent); }
|
|
1913
|
+
@media (max-width: 560px) { .cfg { width: 100%; border-left: 0; } }
|
|
1914
|
+
|
|
1915
|
+
/* ==========================================================================
|
|
1916
|
+
TETRUST (operator, 2026-09-12: "a playable tetris game using our 3d Engine ...
|
|
1917
|
+
absolutely beautiful leveraging our framework"). The well is a board3d in a tall
|
|
1918
|
+
frame; the HUD sits beside it. No inline styles anywhere: the overlay is a class.
|
|
1919
|
+
========================================================================== */
|
|
1920
|
+
.tetrust { display: grid; grid-template-columns: auto 270px; justify-content: center; gap: 12px; align-items: start; }
|
|
1921
|
+
/* the DOS pages give the screen the whole first column (an `auto` column sized it to nothing once the
|
|
1922
|
+
wrap's width became a min() of its container) */
|
|
1923
|
+
.tetrust.dosgame { grid-template-columns: minmax(0, 1fr) 270px; justify-content: stretch; }
|
|
1924
|
+
.tetrust.dosgame > .doscard { min-width: 0; } /* 270, not 290: the well and the DOS screen get the width (2026-09-15) */
|
|
1925
|
+
/* .card carries grid-column: span 4 for the page's twelve-column layout; inside this two-column
|
|
1926
|
+
grid that span forced four tracks and stacked the HUD under the well (measured: both cards
|
|
1927
|
+
"span 4" in a 72/290/72/72 px grid). The other multi-column panels reset it the same way. */
|
|
1928
|
+
.tetrust > .card { grid-column: auto !important; }
|
|
1929
|
+
/* SIZED TO THE SCREEN, not the card (operator: "the playfield needs to be shrunk so it fits on a
|
|
1930
|
+
single screen with room. it's way too large now"): the well's HEIGHT comes from the viewport,
|
|
1931
|
+
and its width follows from the 10x20 shape -- about 330px wide on a 1000px-tall window. */
|
|
1932
|
+
/* the well takes the window's height (operator, 2026-09-15: "same treatment on the diversions games
|
|
1933
|
+
pages" -- it stopped at 600px with a fifth of the window dark beneath) */
|
|
1934
|
+
.tetcard .treemapwrap.tetwell { position: relative; z-index: 1; height: min(calc(100vh - 170px), 820px); width: auto; aspect-ratio: 11 / 21; margin: 0 auto; }
|
|
1935
|
+
/* Blockout's court is 16 x 24 rather than 10 x 20, so it is wider than the Tetris well; the rest
|
|
1936
|
+
of the furniture (the sky canvas, the overlay, the HUD) is shared with it. */
|
|
1937
|
+
.tetcard .treemapwrap.tetwell.bowell { aspect-ratio: 17 / 25; }
|
|
1938
|
+
/* Blockanoid's court is 13 x 26: narrower than Blockout's and taller, which is what gives Vaus a
|
|
1939
|
+
court you can dig a channel up the side of. Same furniture, its own shape. */
|
|
1940
|
+
.tetcard .treemapwrap.tetwell.banwell { aspect-ratio: 14 / 27; }
|
|
1941
|
+
.tetwell canvas { width: 100%; height: 100%; display: block; background: transparent; border: 0; } /* the sky shows through */
|
|
1942
|
+
/* THE PANEL IS THE SKY (operator: "Have this entire panel filled black and rendering the spiral
|
|
1943
|
+
galaxy for this display. Have the text floating over the spiral galaxy, and then the playboard
|
|
1944
|
+
gets drawn when you hit play"): the card is black edge to edge, a canvas behind everything in it
|
|
1945
|
+
carries the star field and the galaxy, the title and the well sit over it, and the well is not
|
|
1946
|
+
shown until a game is on. The overlay covers the whole card; idle it is clear (text over the
|
|
1947
|
+
galaxy), paused or over it dims what is under it. */
|
|
1948
|
+
.tetcard { position: relative; background: #000; overflow: hidden; }
|
|
1949
|
+
.tetcard > h3 { position: relative; z-index: 1; color: var(--fg-dim); }
|
|
1950
|
+
.tetsky { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
|
1951
|
+
.tetwell.idle { visibility: hidden; }
|
|
1952
|
+
.tetswitches { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
1953
|
+
.tetsw { font: inherit; font-size: 11.5px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--fg-dim); cursor: pointer; }
|
|
1954
|
+
.tetsw.on { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
1955
|
+
.tetover { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
|
|
1956
|
+
background: transparent; text-align: center; padding: 20px; }
|
|
1957
|
+
.tetover.dim { background: rgba(0, 0, 0, 0.74); }
|
|
1958
|
+
.tetover.hidden { display: none; }
|
|
1959
|
+
.tetmsg, .tetsub { text-shadow: 0 1px 8px #000, 0 0 2px #000; }
|
|
1960
|
+
.tetmsg { font-size: 24px; font-weight: 650; letter-spacing: .04em; color: var(--accent); }
|
|
1961
|
+
.tetsub { font-size: 12.5px; color: var(--fg-dim); max-width: 340px; line-height: 1.5; }
|
|
1962
|
+
.tetstats { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; font-family: var(--mono); font-size: 13px; margin-bottom: 10px; }
|
|
1963
|
+
.tetstats i { font-style: normal; color: var(--fg-dim); }
|
|
1964
|
+
.tetstats b { font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
|
|
1965
|
+
.tethud h3 { margin-top: 8px; }
|
|
1966
|
+
.treemapwrap.tetnext { position: relative; width: 132px; aspect-ratio: 1 / 1; margin-bottom: 10px; }
|
|
1967
|
+
.tetnext canvas { width: 100%; height: 100%; display: block; border-radius: var(--radius-sm); }
|
|
1968
|
+
.tetkeys { font-size: 11px; color: var(--fg-dim); line-height: 1.7; margin-bottom: 10px; }
|
|
1969
|
+
/* THE CAPSULE LEGEND. Each capsule has its own silhouette on the court (arkanoid.js
|
|
1970
|
+
capsuleTiles), so the legend names the shape as well as the letter -- colour is the last of the
|
|
1971
|
+
three cues, not the only one. Swatch colours match CAPSULE_COLOR in arkanoid.js. */
|
|
1972
|
+
.caplegend { list-style: none; margin: 0 0 10px; padding: 0; font-size: 11px; }
|
|
1973
|
+
.caplegend li { display: grid; grid-template-columns: 18px auto 1fr; gap: 6px; align-items: baseline; padding: 1.5px 0; }
|
|
1974
|
+
.caplegend b { font-weight: 600; color: var(--fg); }
|
|
1975
|
+
.caplegend span { color: var(--fg-faint); text-align: right; }
|
|
1976
|
+
.capsw { display: inline-block; width: 18px; font-style: normal; font-family: var(--mono); font-size: 10px;
|
|
1977
|
+
font-weight: 700; text-align: center; border-radius: 3px; color: #06080d; }
|
|
1978
|
+
.cap-laser { background: #ef5a5a; }
|
|
1979
|
+
.cap-enlarge { background: #4d7cff; color: #eaf1ff; }
|
|
1980
|
+
.cap-catch { background: #2ecc8f; }
|
|
1981
|
+
.cap-slow { background: #f7931a; }
|
|
1982
|
+
.cap-disrupt { background: #3ec9ff; }
|
|
1983
|
+
.cap-player { background: #b07cff; }
|
|
1984
|
+
.tetscores { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 11.5px; }
|
|
1985
|
+
.tetscores td, .tetscores th { padding: 3px 4px; text-align: right; border-bottom: 1px solid var(--line-soft); font-variant-numeric: tabular-nums; }
|
|
1986
|
+
.tetscores th { color: var(--fg-faint); font-weight: 500; text-transform: uppercase; font-size: 9.5px; letter-spacing: .06em; }
|
|
1987
|
+
.tetscores td:first-child, .tetscores th:first-child { text-align: left; }
|
|
1988
|
+
.tetscores tr.now td { color: var(--accent); font-weight: 600; }
|
|
1989
|
+
@media (max-width: 900px) { .tetrust { grid-template-columns: 1fr; } }
|
|
1990
|
+
/* THE DOS GAMES (dosgame.js: DOOM and Quake): the monitor is a 4:3 box, as a VGA's 320x200 was on
|
|
1991
|
+
the CRT it was drawn for, sized to the screen's height like the courts; the pixels stay pixels
|
|
1992
|
+
unless the smooth switch is on. */
|
|
1993
|
+
.doscard { position: relative; background: #000; }
|
|
1994
|
+
.doscard > h3 { color: var(--fg-dim); }
|
|
1995
|
+
/* the DOS screen is ALWAYS 4:3 and as big as the window allows either way (2026-09-15): sized by
|
|
1996
|
+
height alone, a 1093px window capped its width and the 320x200 picture was drawn squashed into
|
|
1997
|
+
a 761x720 box */
|
|
1998
|
+
.doswrap { position: relative; width: min(100%, calc((100vh - 160px) * 4 / 3), 1067px); height: auto; aspect-ratio: 4 / 3; margin: 0 auto; background: #000; }
|
|
1999
|
+
.doswrap:fullscreen { height: 100vh; width: 100vw; max-width: none; aspect-ratio: auto; display: flex; align-items: center; justify-content: center; }
|
|
2000
|
+
.dosscreen { width: 100%; height: 100%; display: block; image-rendering: pixelated; cursor: crosshair; }
|
|
2001
|
+
.doswrap:fullscreen .dosscreen { width: auto; height: 100%; aspect-ratio: 4 / 3; max-width: 100%; }
|
|
2002
|
+
.dosscreen.smooth { image-rendering: auto; }
|
|
2003
|
+
.dosagain { position: absolute; right: 12px; bottom: 12px; z-index: 3; }
|
|
2004
|
+
/* the way out of full screen, drawn only there (keyboard lock takes Esc for the game's menu) */
|
|
2005
|
+
.dosexit { display: none; }
|
|
2006
|
+
.doswrap:fullscreen .dosexit { display: block; position: absolute; right: 14px; top: 12px; z-index: 4; opacity: .55; font: 12px var(--mono); }
|
|
2007
|
+
.doswrap:fullscreen .dosexit:hover { opacity: 1; }
|
|
2008
|
+
.dosgame .tetkeys code { font-family: var(--mono); font-size: 10.5px; }
|
|
2009
|
+
@media (max-width: 900px) { .doswrap { height: auto; width: 100%; } }
|