The stage drew a dot per note and left the reader to guess. A dot travelling inward could be anybody paying anybody, and without text beside it the animation is an ornament rather than a reading of the chain. Each pulse now carries a sample of the individual notes — kind, recipient, amount — and a console below the stage lists them newest-first, the way the block ticker reads, which also means no auto-scroll to fight with and the line a reader is looking for never moves. `kind` is the third split this codebase makes on the producing extrinsic and the first that separates all three cases: a reward has no extrinsic at all because it is paid in block initialisation, a transfer has an ordinary one, and an exit has a `verify_*_batch`. The recorded sender distinguishes none of them. Two caps, and they turned out to interact. `MAX_PULSE_NOTES` bounds the wire — a block has carried hundreds of notes and sending every one for a handful of lines is kilobytes a block of text nobody reads. `CONSOLE_LINES_PER_BLOCK` bounds one block's share of the console, because without it a single busy block fills every line and twelve lines from one block look exactly like twelve blocks of one, which loses the reader all sense of rate. The count of what is not shown hangs on the block's first line rather than its last: the list is newest-first, so a count on the last line is the first thing trimmed, and a block of 260 showed four notes while saying nothing about it. That one reached a screenshot before it was caught. The console fills before the reduced-motion guard, so a viewer who asked for less motion still gets the whole reading — twelve lines, no moving marks, population still drawn, caption still live. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1795 lines
43 KiB
CSS
1795 lines
43 KiB
CSS
/*
|
||
* blackbeard.observer — the design system.
|
||
*
|
||
* Dark, and only dark. This is a deliberate commitment rather than an omission:
|
||
* the site is a scoreboard for a proof-of-work chain, read in the same rooms as
|
||
* the miners it reports on, and a light variant would be a second palette to
|
||
* validate for the sake of a context this content does not have. `color-scheme`
|
||
* is stated so the browser paints form controls and scrollbars to match instead
|
||
* of flashing white.
|
||
*
|
||
* The two data colours were validated with the dataviz palette checker against
|
||
* the chart surface (#14110d): bronze #bd8829 passes the lightness band, chroma
|
||
* floor and 3:1 contrast. Bronze and crimson are adjacent hues and do NOT pass
|
||
* as a categorical pair — which is why no chart here uses them as two series.
|
||
* Every chart is single-series (magnitude, one hue); identity is carried by
|
||
* labels and row treatment, never by a second colour a reader has to tell apart.
|
||
*/
|
||
|
||
/* Two palettes, both selected rather than computed from each other.
|
||
*
|
||
* A light theme is not a dark theme with the lightness inverted. The bronze
|
||
* that carries every magnitude on this site reads at 6.6:1 against the
|
||
* warm-black and only **2.77:1** against paper — the validator says so — so
|
||
* light mode gets its own step of the same hue, dark enough to hold the
|
||
* surface, and the emphasis step moves *down* rather than up because on paper
|
||
* darker is louder. Every value below was chosen by the dataviz validator
|
||
* against the surface it actually sits on:
|
||
*
|
||
* node <skill>/scripts/validate_palette.js "#bd8829" --mode dark --surface "#14110d"
|
||
* node <skill>/scripts/validate_palette.js "#a8741c" --mode light --surface "#f5f1e8"
|
||
*
|
||
* Re-run both after touching any of them. The single-hue rule is unchanged and
|
||
* still load-bearing: bronze and crimson are adjacent hues that fail CVD
|
||
* separation as a categorical pair, in either theme.
|
||
*
|
||
* `data-theme` is always a concrete `dark` or `light`, resolved before first
|
||
* paint by the inline script in `index.html`. "Auto" is a stored *preference*,
|
||
* not a third set of values — which is what keeps this file to one definition
|
||
* per palette instead of one per palette per media query.
|
||
*/
|
||
:root,
|
||
:root[data-theme='dark'] {
|
||
color-scheme: dark;
|
||
|
||
/* Surfaces, warm-black — a cold grey reads as a developer tool, not a arena. */
|
||
--surface-0: #0d0b09;
|
||
--surface-1: #14110d;
|
||
--surface-2: #1c1813;
|
||
--surface-3: #262019;
|
||
--border: #2f2719;
|
||
--border-strong: #453923;
|
||
|
||
/* Ink. Contrast against --surface-1: 16.0, 6.3, 3.7. */
|
||
--text-primary: #f2ece0;
|
||
--text-secondary: #a2937c;
|
||
--text-muted: #7a6c59;
|
||
|
||
/* The single data hue: magnitude, share bars, the history area. */
|
||
--data: #bd8829;
|
||
--data-bright: #e0a63a;
|
||
--data-wash: rgba(189, 136, 41, 0.1);
|
||
|
||
/* Identity and alarm. Never a second series alongside --data. */
|
||
--accent: #d8453a;
|
||
--accent-bright: #f05a4c;
|
||
--accent-wash: rgba(216, 69, 58, 0.1);
|
||
|
||
--good: #3f9d76;
|
||
--warn: #d9a441;
|
||
|
||
--font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
|
||
--font-body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||
--font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
|
||
|
||
/* Washes and glows, kept as tokens because each one is a colour and the
|
||
light theme needs its own. A wash that works as a glow on warm-black is a
|
||
smear on paper at the same alpha. */
|
||
--glow: rgba(189, 136, 41, 0.08);
|
||
--good-halo: rgba(63, 157, 118, 0.18);
|
||
--data-chip: rgba(189, 136, 41, 0.18);
|
||
--row-rule: rgba(47, 39, 25, 0.55);
|
||
--mine-hover: rgba(216, 69, 58, 0.16);
|
||
--warn-border: rgba(217, 164, 65, 0.4);
|
||
--failed-chip: rgba(216, 69, 58, 0.12);
|
||
|
||
--rule: 1px solid var(--border);
|
||
--shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset;
|
||
}
|
||
|
||
/* Light. Contrast against --surface-1: 15.8, 6.9, 4.2 — the ink; then 3.6 for
|
||
* the data hue, 4.8 for the accent, 3.9 and 3.8 for the two status colours.
|
||
* All above the 3:1 the validator holds marks to, and the ink above 4.5.
|
||
*/
|
||
:root[data-theme='light'] {
|
||
color-scheme: light;
|
||
|
||
/* Warm paper, for the same reason the dark surfaces are warm-black: a cold
|
||
grey reads as a developer tool. Elevation runs the other way here — the
|
||
page is the lightest thing and each step above it is *darker*, which keeps
|
||
the ordering of visual weight the dark theme has. */
|
||
--surface-0: #fbf8f2;
|
||
--surface-1: #f5f1e8;
|
||
--surface-2: #ece7da;
|
||
--surface-3: #e2dbc9;
|
||
--border: #d9d0ba;
|
||
--border-strong: #b8ab8d;
|
||
|
||
--text-primary: #1b1710;
|
||
--text-secondary: #5b5142;
|
||
--text-muted: #7c7263;
|
||
|
||
/* The bronze, re-stepped. `--data-bright` is *darker* than `--data`: it is
|
||
the emphasis step, and emphasis on paper is weight, not glare. */
|
||
--data: #a8741c;
|
||
--data-bright: #8a5f10;
|
||
--data-wash: rgba(168, 116, 28, 0.12);
|
||
|
||
--accent: #c3382d;
|
||
--accent-bright: #a82c20;
|
||
--accent-wash: rgba(195, 56, 45, 0.1);
|
||
|
||
--good: #0f8a58;
|
||
--warn: #b8651a;
|
||
|
||
--glow: rgba(168, 116, 28, 0.09);
|
||
--good-halo: rgba(15, 138, 88, 0.16);
|
||
--data-chip: rgba(168, 116, 28, 0.16);
|
||
--row-rule: rgba(184, 171, 141, 0.5);
|
||
--mine-hover: rgba(195, 56, 45, 0.1);
|
||
--warn-border: rgba(184, 101, 26, 0.45);
|
||
--failed-chip: rgba(195, 56, 45, 0.09);
|
||
|
||
/* Down rather than up: an inset white highlight is how a dark surface catches
|
||
the light, and on paper the same gesture is a shadow. */
|
||
--shadow: 0 1px 0 rgba(27, 23, 16, 0.04) inset;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#root {
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background:
|
||
/* A faint warm glow behind the masthead, so the page has a top rather than
|
||
being a flat field. Fixed so it does not travel with the scroll. */
|
||
radial-gradient(120% 60% at 50% -10%, var(--glow), transparent 60%), var(--surface-0);
|
||
background-attachment: fixed;
|
||
color: var(--text-primary);
|
||
font-family: var(--font-body);
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
a {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
|
||
/* Visible focus everywhere. The site is a table of links; keyboard users need
|
||
to see where they are, and the default ring is invisible on this ground. */
|
||
:focus-visible {
|
||
outline: 2px solid var(--data-bright);
|
||
outline-offset: 2px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* ---- type ---------------------------------------------------------------- */
|
||
|
||
.display {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.eyebrow {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.numeral {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
/* Tabular figures are the whole point: a column of hashrates that shifts
|
||
horizontally on every update is unreadable on a page that updates every
|
||
few seconds. */
|
||
}
|
||
|
||
/* ---- layout -------------------------------------------------------------- */
|
||
|
||
.shell {
|
||
max-width: 1180px;
|
||
margin: 0 auto;
|
||
padding: 0 20px 72px;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--surface-1);
|
||
border: var(--rule);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.panel-head {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 14px 18px;
|
||
border-bottom: var(--rule);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.panel-title {
|
||
font-family: var(--font-display);
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
margin: 0;
|
||
}
|
||
|
||
/* ---- masthead ------------------------------------------------------------ */
|
||
|
||
.masthead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
padding: 22px 0 18px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mark {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.mark-name {
|
||
font-family: var(--font-display);
|
||
font-size: 25px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
line-height: 1;
|
||
}
|
||
|
||
.mark-name span {
|
||
color: var(--data);
|
||
}
|
||
|
||
.mark-tagline {
|
||
font-size: 11px;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.masthead-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* ---- node panel ---------------------------------------------------------- */
|
||
|
||
.node-grid {
|
||
display: grid;
|
||
/* Two columns on a phone, as many as fit on a desktop. A CPU model is the
|
||
longest value here by far and gets the room it needs rather than wrapping
|
||
mid-word. */
|
||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||
gap: 1px;
|
||
background: var(--border);
|
||
border-top: var(--rule);
|
||
}
|
||
|
||
.node-field {
|
||
background: var(--surface-1);
|
||
padding: 11px 16px 13px;
|
||
}
|
||
|
||
.node-value {
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
margin-top: 5px;
|
||
/* A CPU model can be sixty characters. Wrapping is better than a scrollbar
|
||
in a cell this small. */
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.network-sparks {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1px;
|
||
background: var(--border);
|
||
border-top: var(--rule);
|
||
}
|
||
|
||
.network-sparks .node-field {
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.node-filter {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 6px 10px;
|
||
border: var(--rule);
|
||
border-radius: 3px;
|
||
background: var(--surface-0);
|
||
color: var(--text-primary);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.node-filter:focus-visible {
|
||
outline: 2px solid var(--data);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* ---- theme toggle -------------------------------------------------------- */
|
||
|
||
.theme-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* 32px, which is the smallest a lone icon control can be and still be a
|
||
comfortable target on a touch screen. The icon inside is 16. */
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
border: var(--rule);
|
||
border-radius: 999px;
|
||
background: var(--surface-1);
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
/* Not the colour: `color-scheme` repaints the scrollbar and the form chrome
|
||
at the same instant, and a 150ms fade on the page while the gutter snaps is
|
||
worse than both moving together. */
|
||
transition:
|
||
border-color 0.15s ease,
|
||
color 0.15s ease;
|
||
}
|
||
|
||
.theme-toggle:hover {
|
||
color: var(--data-bright);
|
||
border-color: var(--border-strong);
|
||
}
|
||
|
||
.theme-toggle:focus-visible {
|
||
outline: 2px solid var(--data);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* ---- connection light ---------------------------------------------------- */
|
||
|
||
.status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 11px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.status-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: var(--text-muted);
|
||
flex: none;
|
||
}
|
||
|
||
.status-live .status-dot {
|
||
background: var(--good);
|
||
box-shadow: 0 0 0 3px var(--good-halo);
|
||
}
|
||
|
||
.status-reconnecting .status-dot,
|
||
.status-connecting .status-dot {
|
||
background: var(--warn);
|
||
animation: pulse 1.1s ease-in-out infinite;
|
||
}
|
||
|
||
.status-offline .status-dot {
|
||
background: var(--accent);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
50% {
|
||
opacity: 0.25;
|
||
}
|
||
}
|
||
|
||
/* ---- chain switcher & window selector ------------------------------------ */
|
||
|
||
.segmented {
|
||
display: inline-flex;
|
||
border: var(--rule);
|
||
background: var(--surface-1);
|
||
}
|
||
|
||
.segmented a {
|
||
appearance: none;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
font: inherit;
|
||
font-size: 12px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
padding: 7px 13px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.segmented a + a {
|
||
border-left: var(--rule);
|
||
}
|
||
|
||
.segmented a:hover {
|
||
color: var(--text-primary);
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.segmented a[aria-current='page'] {
|
||
background: var(--data-wash);
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
/* ---- chain nav ----------------------------------------------------------- */
|
||
|
||
.chains {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px 28px;
|
||
flex-wrap: wrap;
|
||
padding: 0 0 18px;
|
||
}
|
||
|
||
/* One labelled group per kind of network. The gap between groups is wider than
|
||
the gap inside one, so the split reads before either label does. */
|
||
.chain-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.chains-label {
|
||
flex: none;
|
||
}
|
||
|
||
.chains-list {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.chain-chip {
|
||
display: inline-flex;
|
||
/* The tracked chips are links now, so they arrive underlined. */
|
||
text-decoration: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
appearance: none;
|
||
background: var(--surface-1);
|
||
border: var(--rule);
|
||
color: var(--text-secondary);
|
||
font: inherit;
|
||
font-size: 13px;
|
||
padding: 6px 11px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chain-chip:hover:not(:disabled) {
|
||
color: var(--text-primary);
|
||
border-color: var(--border-strong);
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.chain-chip.is-active {
|
||
color: var(--data-bright);
|
||
border-color: var(--data);
|
||
background: var(--data-wash);
|
||
}
|
||
|
||
.chain-name {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.chain-nodes {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
background: var(--surface-3);
|
||
padding: 1px 6px;
|
||
}
|
||
|
||
.chain-chip.is-active .chain-nodes {
|
||
color: var(--data-bright);
|
||
background: var(--data-chip);
|
||
}
|
||
|
||
/* ---- stat tiles ---------------------------------------------------------- */
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
|
||
gap: 1px;
|
||
background: var(--border);
|
||
border: var(--rule);
|
||
margin-bottom: 26px;
|
||
}
|
||
|
||
.stat {
|
||
background: var(--surface-1);
|
||
padding: 15px 18px 16px;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Two columns and five tiles leaves a blank cell. Letting the hero span the row
|
||
makes six, which fills the grid — and gives the headline number the width it
|
||
deserves on the screen where it is most cramped. */
|
||
@media (max-width: 620px) {
|
||
.stat-hero {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.stat-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.stat-value {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 25px;
|
||
font-weight: 600;
|
||
line-height: 1.15;
|
||
margin-top: 7px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.stat-hero .stat-value {
|
||
font-size: 33px;
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
.stat-note {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 5px;
|
||
min-height: 1.2em;
|
||
}
|
||
|
||
.stat-note.nominal {
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* The note line doubles as the sparkline's readout, so a hover changes its
|
||
text. Brightening it says the line is being read rather than captioned —
|
||
without moving anything, which at this size would be a flicker. */
|
||
.stat-note.reading {
|
||
color: var(--text-secondary);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* ---- sparklines ---------------------------------------------------------- */
|
||
|
||
/* Reserved on every tile, drawn on four: the fifth (height) has nothing worth
|
||
plotting, and an absent slot there would leave one tile's baselines out of
|
||
step with the rest of the row. */
|
||
.spark {
|
||
height: 30px;
|
||
margin-top: 9px;
|
||
}
|
||
|
||
.spark-svg {
|
||
display: block;
|
||
touch-action: none;
|
||
}
|
||
|
||
.spark-svg:focus-visible {
|
||
outline: 2px solid var(--data-bright);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* Single series, single hue — the same bronze every other mark on the site
|
||
uses. The palette's second colour is reserved for identity and alarm, and as
|
||
an adjacent hue it would not survive CVD as a second series here. */
|
||
.spark-line {
|
||
fill: none;
|
||
stroke: var(--data);
|
||
stroke-width: 2;
|
||
stroke-linejoin: round;
|
||
stroke-linecap: round;
|
||
}
|
||
|
||
/* Emphasis on the newest point, as a lighter step rather than a second hue.
|
||
`--data-bright` sits above the validator's categorical lightness band (L
|
||
0.762 against a 0.48–0.67 ceiling) — that check governs slots a reader has to
|
||
tell apart from each other, which a single-hue emphasis step is not. It is
|
||
the same token the hero figure already wears, and it clears contrast against
|
||
the tile surface. Do not "fix" it by giving the marker the accent colour. */
|
||
.spark-marker {
|
||
fill: var(--data-bright);
|
||
}
|
||
|
||
.spark-crosshair {
|
||
stroke: var(--border-strong);
|
||
stroke-width: 1;
|
||
}
|
||
|
||
/* The miner's identifiers, stacked label-over-value. A definition list because
|
||
that is what it is: terms and their values, which also gives a screen reader
|
||
the pairing without any ARIA. */
|
||
.identifiers {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 2px 12px;
|
||
margin: 8px 0 0;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.identifiers dt {
|
||
color: var(--text-muted);
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
font-size: 9px;
|
||
align-self: center;
|
||
}
|
||
|
||
.identifiers dd {
|
||
margin: 0;
|
||
color: var(--text-secondary);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* ---- block facts --------------------------------------------------------- */
|
||
|
||
/* The same grid as the stat tiles: this is the same kind of content — a handful
|
||
of single values with a caveat each — and giving it a second look would say
|
||
the two are different sorts of fact when they are not. */
|
||
.facts {
|
||
display: grid;
|
||
/* Three, not auto-fit. There are exactly six facts, and auto-fit resolves to
|
||
five at this width — leaving one on a row of its own beside four empty
|
||
cells that read as a grid that failed to load. Six divides evenly by three
|
||
and by two, so every breakpoint below is a full rectangle. A seventh fact
|
||
would need this revisited, which is the right amount of friction. */
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1px;
|
||
background: var(--border);
|
||
border-bottom: var(--rule);
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.facts {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 460px) {
|
||
.facts {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.fact {
|
||
background: var(--surface-1);
|
||
padding: 14px 18px 15px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.fact-label {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.fact-value {
|
||
font-family: var(--font-mono);
|
||
font-size: 17px;
|
||
margin-top: 7px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.fact-note {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* A value nobody has. Muted and in the body face so it does not read as data —
|
||
it is a sentence explaining an absence, and formatting it like a figure would
|
||
invite it to be compared with one. */
|
||
.fact-unknown {
|
||
font-family: var(--font-body);
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.block-links {
|
||
padding: 15px 18px 18px;
|
||
display: grid;
|
||
gap: 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.block-links a {
|
||
color: var(--text-secondary);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.block-links a:hover {
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
/* The one place the accent hue belongs: not a series, a state. A block that
|
||
lost its height is the single most important thing its page can say. */
|
||
.badge {
|
||
margin-left: 10px;
|
||
padding: 2px 7px;
|
||
font-family: var(--font-body);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.badge-warn {
|
||
background: var(--accent-wash);
|
||
color: var(--accent-bright);
|
||
border: 1px solid var(--accent);
|
||
}
|
||
|
||
a.ticker-height:hover {
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
/* ---- leaderboard --------------------------------------------------------- */
|
||
|
||
.board {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.board th {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
text-align: right;
|
||
padding: 10px 14px;
|
||
border-bottom: var(--rule);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.board th:first-child,
|
||
.board th.left {
|
||
text-align: left;
|
||
}
|
||
|
||
.board td {
|
||
padding: 9px 14px;
|
||
border-bottom: 1px solid var(--row-rule);
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.board td:first-child,
|
||
.board td.left {
|
||
text-align: left;
|
||
}
|
||
|
||
.board tbody tr:hover {
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
/* The pinned row: a left rule and a wash, plus an explicit "YOU" chip. Three
|
||
redundant cues, none of them colour alone. */
|
||
.board tbody tr.mine {
|
||
background: var(--accent-wash);
|
||
box-shadow: inset 3px 0 0 var(--accent);
|
||
}
|
||
|
||
.board tbody tr.mine:hover {
|
||
background: var(--mine-hover);
|
||
}
|
||
|
||
.rank {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
width: 1%;
|
||
}
|
||
|
||
.rank-1,
|
||
.rank-2,
|
||
.rank-3 {
|
||
color: var(--data-bright);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.miner-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.miner-name {
|
||
font-weight: 600;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.miner-name.anonymous {
|
||
font-family: var(--font-mono);
|
||
font-weight: 400;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.chip {
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
padding: 2px 6px;
|
||
border: 1px solid currentColor;
|
||
flex: none;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.chip-you {
|
||
color: var(--accent-bright);
|
||
}
|
||
|
||
.chip-named {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* A mapping resting on one block, or on a narrow lead. Dashed rather than a
|
||
second colour: the palette's accent is reserved for identity and alarm, and
|
||
this is neither — it is the same claim held more loosely, which a change in
|
||
border weight says without introducing a hue a reader has to decode. */
|
||
.chip-guess {
|
||
border-style: dashed;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
/* Share: the percentage as the direct label, the bar beneath it as magnitude.
|
||
One hue, 4px rounded data-end at the far side, square against the baseline it
|
||
grows from. */
|
||
.share-cell {
|
||
/* Wide enough that the bar has usable resolution between a 20% leader and
|
||
a 0.6% tail; narrower and every small miner's bar is the same sliver. */
|
||
min-width: 92px;
|
||
}
|
||
|
||
.share-bar {
|
||
position: relative;
|
||
height: 5px;
|
||
margin-top: 4px;
|
||
background: var(--surface-3);
|
||
}
|
||
|
||
.share-bar > i {
|
||
display: block;
|
||
height: 100%;
|
||
background: var(--data);
|
||
border-radius: 0 3px 3px 0;
|
||
}
|
||
|
||
tr.mine .share-bar > i {
|
||
background: var(--accent);
|
||
}
|
||
|
||
/* Below this width the table would need a horizontal scrollbar, and a column
|
||
behind a scrollbar is a column nobody reads. "Last block" is the one a reader
|
||
can most easily do without — the live ticker beside the table already says
|
||
who mined most recently. */
|
||
@media (max-width: 1320px) {
|
||
.board .last-seen {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* On a phone the table still scrolls inside its own box — which is correct for
|
||
a data table — but dropping the least load-bearing column and tightening the
|
||
cells means far less of it is behind that scroll. Blocks, share and estimated
|
||
hashrate, the three a miner actually came for, stay visible without scrolling. */
|
||
@media (max-width: 720px) {
|
||
.board .streak {
|
||
display: none;
|
||
}
|
||
|
||
.board th,
|
||
.board td {
|
||
padding: 9px 9px;
|
||
}
|
||
|
||
.miner-name {
|
||
max-width: 150px;
|
||
}
|
||
}
|
||
|
||
.pin-button {
|
||
appearance: none;
|
||
background: transparent;
|
||
border: 0;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 15px;
|
||
line-height: 1;
|
||
padding: 3px 5px;
|
||
}
|
||
|
||
.pin-button:hover {
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
.pin-button[aria-pressed='true'] {
|
||
color: var(--accent-bright);
|
||
}
|
||
|
||
/* ---- block ticker -------------------------------------------------------- */
|
||
|
||
.ticker-row {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
gap: 12px;
|
||
align-items: baseline;
|
||
padding: 8px 18px;
|
||
border-bottom: 1px solid var(--row-rule);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.ticker-row.mine {
|
||
background: var(--accent-wash);
|
||
box-shadow: inset 3px 0 0 var(--accent);
|
||
}
|
||
|
||
/* Newly arrived blocks flash once. Suppressed for reduced-motion, below. */
|
||
.ticker-row.fresh {
|
||
animation: arrive 1.4s ease-out;
|
||
}
|
||
|
||
@keyframes arrive {
|
||
from {
|
||
background: var(--data-wash);
|
||
}
|
||
}
|
||
|
||
.ticker-height {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.ticker-gap {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* ---- chart --------------------------------------------------------------- */
|
||
|
||
.chart {
|
||
width: 100%;
|
||
display: block;
|
||
touch-action: none;
|
||
}
|
||
|
||
.chart .grid {
|
||
stroke: var(--border);
|
||
stroke-width: 1;
|
||
}
|
||
|
||
.chart .area {
|
||
fill: var(--data-wash);
|
||
}
|
||
|
||
.chart .line {
|
||
fill: none;
|
||
stroke: var(--data);
|
||
stroke-width: 2;
|
||
stroke-linejoin: round;
|
||
stroke-linecap: round;
|
||
}
|
||
|
||
.chart .crosshair {
|
||
stroke: var(--border-strong);
|
||
stroke-width: 1;
|
||
}
|
||
|
||
.chart .marker {
|
||
fill: var(--data);
|
||
stroke: var(--surface-1);
|
||
stroke-width: 2;
|
||
}
|
||
|
||
/* The live wormhole stage.
|
||
*
|
||
* Every value is one hue at a different wash weight — a throat seen edge-on,
|
||
* not a set of series. Direction is carried by which way a note travels, the
|
||
* same encoding the flow chart uses and for the same measured reason. */
|
||
.stage-wrap {
|
||
position: relative;
|
||
background: var(--surface-1);
|
||
border: var(--rule);
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 26px;
|
||
/* Matches the stat row it stands in for, so swapping one for the other does
|
||
not move the page underneath. */
|
||
padding: 6px 0 2px;
|
||
}
|
||
|
||
.stage {
|
||
display: block;
|
||
}
|
||
|
||
.stage-rim {
|
||
fill: none;
|
||
stroke: var(--data);
|
||
}
|
||
|
||
.stage-rim-3 {
|
||
stroke-width: 1;
|
||
opacity: 0.18;
|
||
}
|
||
|
||
.stage-rim-2 {
|
||
stroke-width: 1;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.stage-rim-1 {
|
||
stroke-width: 1.5;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.stage-core {
|
||
fill: var(--data-wash);
|
||
stroke: var(--data);
|
||
stroke-width: 1.5;
|
||
}
|
||
|
||
.stage-note {
|
||
fill: var(--data-bright);
|
||
}
|
||
|
||
/* The notes already inside, turning slowly.
|
||
*
|
||
* A CSS rotation on a group rather than a `requestAnimationFrame` loop: the
|
||
* flight marks stop when the chain is quiet, but the standing population is
|
||
* always there, and a JS loop running forever on a page left open all day is a
|
||
* cost this does not need to pay. Three composited rotations are free.
|
||
*
|
||
* `transform-origin: 0 0` because the group is already translated to the mouth;
|
||
* the squash that turns these circles into orbits lives on its parent. */
|
||
.stage-shell {
|
||
animation-name: stage-orbit;
|
||
animation-timing-function: linear;
|
||
animation-iteration-count: infinite;
|
||
transform-origin: 0 0;
|
||
}
|
||
|
||
.stage-held {
|
||
fill: var(--data);
|
||
opacity: 0.38;
|
||
}
|
||
|
||
@keyframes stage-orbit {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* The viewer asked for less motion, so the population is held still — it is
|
||
still drawn, because how much is inside is the information; only the turning
|
||
goes. The flight marks are stopped in the component, which also keeps the
|
||
caption and its real numbers. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.stage-shell {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
.stage-caption {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 18px;
|
||
align-items: baseline;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
padding: 0 4px 4px;
|
||
}
|
||
|
||
.stage-caption strong {
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
.stage-idle {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* The console beside the stage: what each mark was.
|
||
*
|
||
* Monospace and columnar so the eye can run down one field — a log that
|
||
* reflows between lines is a log nobody scans. Older lines recede rather than
|
||
* vanishing at a hard edge, which is what makes the buffer read as a buffer. */
|
||
.stage-log {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 2px 12px 10px;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.stage-log li {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0 12px;
|
||
align-items: baseline;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* The newest line is the one being read; the rest fade back in order. Applied
|
||
to the tail rather than to every line so the top of the list stays at full
|
||
strength as it grows. */
|
||
.stage-log li:nth-child(n + 5) {
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.stage-log li:nth-child(n + 8) {
|
||
opacity: 0.45;
|
||
}
|
||
|
||
.stage-log-empty {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.stage-log-height {
|
||
color: var(--text-muted);
|
||
min-width: 66px;
|
||
}
|
||
|
||
/* Direction repeats what the mark did, in a glyph, so a line and its dot can be
|
||
matched without reading the label. Same reasoning as the flow chart: the
|
||
encoding is geometry, never a second hue — every kind wears the one colour. */
|
||
.stage-log-kind {
|
||
min-width: 72px;
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
.stage-log-amount {
|
||
min-width: 132px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.stage-log-to {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* A meter: one ratio against its limit.
|
||
*
|
||
* The track is the same hue at wash weight rather than a neutral grey, so the
|
||
* whole figure reads as one quantity partly filled instead of as two competing
|
||
* categories. Same reason the site has no two-slice pies. */
|
||
.meter {
|
||
height: 8px;
|
||
background: var(--data-wash);
|
||
border: 1px solid var(--border);
|
||
margin: 18px 0 14px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.meter-fill {
|
||
height: 100%;
|
||
background: var(--data);
|
||
/* Rounded data-end, anchored to the track's start, per the mark spec. */
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
|
||
/* Flow bars: both directions wear the SAME hue.
|
||
*
|
||
* In and out are opposite directions, which normally calls for a diverging
|
||
* palette. This site has one data hue, and the two-shade alternative was
|
||
* measured rather than assumed: `#bd8829` against `#e0a63a` scores a
|
||
* normal-vision ΔE of 9.9, well under the 15 floor — full-colour readers
|
||
* cannot reliably tell them apart, so it is a hard fail and not something a
|
||
* legend would excuse. Direction is therefore carried entirely by which side
|
||
* of the zero rule a bar sits on, which survives greyscale, print,
|
||
* forced-colors and every form of colour vision. Do not give these two
|
||
* different fills. */
|
||
.chart .flow-in,
|
||
.chart .flow-out {
|
||
fill: var(--data);
|
||
}
|
||
|
||
/* The chart's spine, drawn over the bars it anchors. A step stronger than the
|
||
* grid, because which side of it a bar sits on is the entire direction
|
||
* encoding. */
|
||
.chart .flow-zero {
|
||
stroke: var(--border-strong);
|
||
stroke-width: 1;
|
||
}
|
||
|
||
/* The hovered column, behind its bars: a wash rather than an outline, so the
|
||
* hit target reads without drawing a border around the marks. */
|
||
.chart .flow-hover {
|
||
fill: var(--data-wash);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.chart-axis {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
fill: var(--text-muted);
|
||
}
|
||
|
||
.tooltip {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
background: var(--surface-3);
|
||
border: 1px solid var(--border-strong);
|
||
padding: 7px 10px;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
white-space: nowrap;
|
||
z-index: 5;
|
||
}
|
||
|
||
.tooltip-label {
|
||
color: var(--text-muted);
|
||
font-size: 10px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* ---- misc ---------------------------------------------------------------- */
|
||
|
||
.empty {
|
||
padding: 40px 18px;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 11px 16px;
|
||
border: 1px solid var(--border-strong);
|
||
background: var(--surface-2);
|
||
font-size: 13px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.banner-warn {
|
||
border-color: var(--warn-border);
|
||
color: var(--warn);
|
||
}
|
||
|
||
.footer {
|
||
margin-top: 44px;
|
||
padding-top: 20px;
|
||
border-top: var(--rule);
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
gap: 18px;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.footer a {
|
||
color: var(--text-secondary);
|
||
text-decoration: underline;
|
||
text-underline-offset: 3px;
|
||
}
|
||
|
||
.footer a:hover {
|
||
color: var(--data-bright);
|
||
}
|
||
|
||
/* The source link, on its own line under the caveats. `flex-basis: 100%` rather
|
||
than a fourth flex item: `space-between` would otherwise strand a short line
|
||
against a long one, and this is a different kind of statement from the
|
||
three above it — where to check the numbers, not a caveat about them. */
|
||
.colophon {
|
||
flex-basis: 100%;
|
||
padding-top: 14px;
|
||
border-top: var(--rule);
|
||
}
|
||
|
||
/* Wide content scrolls inside its own box; the page never scrolls sideways. */
|
||
.scroll-x {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.two-col {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
|
||
gap: 22px;
|
||
align-items: start;
|
||
}
|
||
|
||
@media (max-width: 880px) {
|
||
.two-col {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
}
|
||
}
|
||
|
||
/* Motion is decoration here — the numbers carry the information, so it can all
|
||
go without losing anything. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.001ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.001ms !important;
|
||
}
|
||
}
|
||
|
||
.visually-hidden {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0 0 0 0);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
|
||
/* ---- account activity ---------------------------------------------------- */
|
||
|
||
/* The buttons in a panel head. Written out once here rather than inline on each
|
||
one: they were four copies of the same eight declarations, and a fifth was
|
||
about to be a fifth copy. */
|
||
.panel-button {
|
||
padding: 7px 13px;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* One event's fields on one line. They are read left to right as a phrase —
|
||
"0.31 QUAN", "to qzp2AxZw…rs8L" — so they flow rather than tabulate: the
|
||
fields differ per event type and a column per possible field would be mostly
|
||
empty. */
|
||
.event-fields {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 4px 10px;
|
||
}
|
||
|
||
.event-field {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 5px;
|
||
}
|
||
|
||
.event-key {
|
||
color: var(--text-muted);
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Deliberately not `.last-seen`, which the leaderboard drops below 1320px. On
|
||
a payment history the date is the second thing anyone reads after the amount;
|
||
it only goes on a phone, where the table scrolls anyway. */
|
||
.event-when {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.board .event-when {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* The caveat under a panel, in the same register as the page footer: what the
|
||
numbers above are, and what they are not. */
|
||
.panel-note {
|
||
margin: 0;
|
||
padding: 12px 16px;
|
||
border-top: var(--rule);
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* ---- runtime ------------------------------------------------------------- */
|
||
|
||
/* The version strip. Reuses the chain chip: it is the same gesture — a row of
|
||
short labels where one is current — and a second visual language for it
|
||
would say the two are different sorts of choice. */
|
||
.runtime-strip {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 14px 16px;
|
||
border-bottom: var(--rule);
|
||
}
|
||
|
||
.runtime-pallet {
|
||
border-bottom: var(--rule);
|
||
}
|
||
|
||
.runtime-pallets > .runtime-pallet:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
/* A whole-row target. A pallet's name is not the only part of the row worth
|
||
clicking — the counts beside it are what tells you whether opening it is
|
||
worth doing. */
|
||
.runtime-pallet-head {
|
||
display: grid;
|
||
grid-template-columns: 34px 1fr auto 20px;
|
||
align-items: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
appearance: none;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-primary);
|
||
font: inherit;
|
||
text-align: left;
|
||
padding: 11px 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.runtime-pallet-head:hover {
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.runtime-pallet-name {
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* The pallet index, and the variant indexes inside. Monospace and tabular
|
||
because they are the bytes on the wire, not decoration: reading down the
|
||
column is how you see that a pallet was inserted rather than appended. */
|
||
.runtime-index {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
background: var(--surface-3);
|
||
padding: 1px 6px;
|
||
text-align: right;
|
||
min-width: 26px;
|
||
}
|
||
|
||
.runtime-counts {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.runtime-chevron {
|
||
font-family: var(--font-mono);
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
}
|
||
|
||
.runtime-pallet-body {
|
||
padding: 4px 16px 18px;
|
||
background: var(--surface-1);
|
||
}
|
||
|
||
.runtime-group {
|
||
padding: 12px 0 4px;
|
||
}
|
||
|
||
.runtime-envelope {
|
||
padding: 14px 16px;
|
||
border-bottom: var(--rule);
|
||
}
|
||
|
||
.runtime-list {
|
||
list-style: none;
|
||
margin: 8px 0 0;
|
||
padding: 0;
|
||
display: grid;
|
||
gap: 5px;
|
||
}
|
||
|
||
/* Flow rather than a grid: the parts of a line are a name, sometimes a value,
|
||
sometimes a type and sometimes a sentence, and a column per possible part
|
||
would be four columns mostly empty. */
|
||
.runtime-list li {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 4px 10px;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.runtime-list code {
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
color: var(--text-primary);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* The compiled-in value, in the data hue: it is the one thing on the line that
|
||
is a fact about this runtime rather than about its shape. */
|
||
.runtime-value {
|
||
font-family: var(--font-mono);
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--data-bright);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.runtime-type {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* `optional` or `default`, which is a property of the entry and belongs on its
|
||
line rather than under it. */
|
||
.runtime-modifier {
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* Documentation the runtime carries. Given the whole remaining width so it
|
||
wraps as prose, because that is what it is. */
|
||
.runtime-doc {
|
||
flex-basis: 100%;
|
||
color: var(--text-secondary);
|
||
font-size: 11px;
|
||
padding-left: 36px;
|
||
}
|
||
|
||
.runtime-extensions {
|
||
counter-reset: none;
|
||
}
|
||
|
||
/* ---- section nav --------------------------------------------------------- */
|
||
|
||
/* A quieter register than the chain chips above it: choosing a chain changes
|
||
what every number on the page is about, and choosing a section only changes
|
||
which of them you are looking at. Words rather than chips says that. */
|
||
.sections {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 18px;
|
||
padding: 0 0 18px;
|
||
margin-top: -4px;
|
||
}
|
||
|
||
.section-link {
|
||
font-size: 12px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
text-decoration: none;
|
||
padding-bottom: 3px;
|
||
border-bottom: 1px solid transparent;
|
||
}
|
||
|
||
.section-link:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Underline rather than a fill: the current section is a position, and a filled
|
||
chip here would compete with the active chain chip directly above it. */
|
||
.section-link[aria-current='page'] {
|
||
color: var(--data-bright);
|
||
border-bottom-color: var(--data);
|
||
}
|
||
|
||
/* ---- pager --------------------------------------------------------------- */
|
||
|
||
.pager {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 14px 16px;
|
||
border-top: var(--rule);
|
||
}
|
||
|
||
.pager button:disabled {
|
||
color: var(--text-muted);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* The address beside a name on the accounts index. Muted and monospace: it is
|
||
the row's identity but not what a reader scans for, and one node name can
|
||
belong to several accounts. */
|
||
.account-address {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* A nested value inside a call's arguments — a `MultiAddress::Id` wrapper, or a
|
||
whole call inside a `Utility::batch_all`. Indented rather than bracketed:
|
||
the nesting is the structure the runtime describes, and flattening it would
|
||
lose which recipient belongs to which transfer. */
|
||
.event-nested {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 4px 8px;
|
||
}
|
||
|
||
/* Where a history row came from, and whether it worked. `chip-submitted` is
|
||
deliberately quiet — it is a category, not a warning — and `chip-failed`
|
||
borrows the accent because it is the one thing on the row that changes what
|
||
the row means. */
|
||
.chip-submitted {
|
||
color: var(--text-muted);
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.chip-failed {
|
||
color: var(--accent-bright);
|
||
border-color: var(--accent);
|
||
background: var(--failed-chip);
|
||
}
|
||
|
||
/* A hashrate and how far to trust it. The error rides beside the figure rather
|
||
than under it: the two are one statement, and separating them invites reading
|
||
the number alone. */
|
||
.rate-cell {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.rate-error {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Under four blocks the error bar is wider than half the value. Dimming says
|
||
"this is a guess" in the same register the `node?` chip does for names —
|
||
without hiding a figure somebody may still want. */
|
||
.rate-weak {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* A call, its signature and its documentation, stacked. The signature is the
|
||
runtime's own and long enough to wrap, so it gets the width rather than a
|
||
column of its own. */
|
||
.call-cell {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 3px 8px;
|
||
/* Bounded, so a long signature wraps inside its cell rather than widening
|
||
the table and pushing the counts — which are what the page is for — off
|
||
the side of the viewport. */
|
||
max-width: 60ch;
|
||
}
|
||
|
||
.call-cell .runtime-doc {
|
||
padding-left: 0;
|
||
}
|
||
|
||
/* Declared and never dispatched. Dimmed rather than hidden: the unused surface
|
||
is the half of this table nobody else shows. */
|
||
.call-unused {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.call-unused code {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* A decoded storage value and, when the chain held nothing, the note that this
|
||
is the runtime's default rather than something it wrote. */
|
||
.state-value {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 4px 10px;
|
||
}
|
||
|
||
/* A role the chain assigns. Data-hued rather than accent: it is a fact about
|
||
the account, not a warning about it, and the accent is spoken for by things
|
||
that need attention. */
|
||
.chip-role {
|
||
color: var(--data-bright);
|
||
border-color: var(--data);
|
||
background: var(--data-wash);
|
||
}
|
||
|
||
/* Endowed at genesis. Quieter than a role, because it is history rather than a
|
||
job — an account can be endowed and do nothing. */
|
||
.chip-genesis {
|
||
color: var(--text-secondary);
|
||
border-color: var(--border-strong);
|
||
}
|
||
|
||
/* Chips beside a panel title, which is a heading and therefore uppercased and
|
||
letter-spaced — neither of which a chip should inherit. */
|
||
.title-chips {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
margin-left: 10px;
|
||
vertical-align: middle;
|
||
letter-spacing: normal;
|
||
}
|
||
|
||
/* The citation under an account's title. Same shape as the chain-status
|
||
banners, in the data hue, because it explains rather than warns. */
|
||
.banner-role {
|
||
border-color: var(--data);
|
||
background: var(--data-wash);
|
||
color: var(--text-secondary);
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.banner-doc {
|
||
display: block;
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* Events nested under the extrinsic that caused them. Indented and quieter
|
||
than the call above: they are its consequences, not siblings of it. */
|
||
.block-event-list {
|
||
margin-top: 6px;
|
||
padding-left: 10px;
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
|
||
/* A testnet. Dashed rather than a colour, and a second channel behind the group
|
||
heading rather than the only one — a single visual difference reads as
|
||
decoration to anyone who has not been told what it means. */
|
||
.chain-chip.is-testnet {
|
||
border-style: dashed;
|
||
}
|