blockyard 0.0.1 → 0.0.9

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