/* Dark instrument theme: this is a page about a machine that runs in a cell, and it reads like a
   readout rather than a document. Two accents carry meaning rather than decoration — warm means
   depolarised or just fired, cool means hyperpolarised or inhibited — and cyan is the instrument
   colour: hairlines, headings, the things that are chrome. Nothing else is coloured.

   The light palette this replaced was honest and plain, and the reader it was built for said so:
   a wall of white panels is a document, and the ring in the middle of it looked like a diagram
   rather than an instrument. The variables below keep their names, so every rule written against
   them follows the theme; the section at the foot of this file re-dresses the components whose
   colours were written out by hand. */

:root {
  --bg: #05070b;
  --panel: rgba(13, 19, 28, 0.86);
  --panel-2: rgba(19, 28, 41, 0.72);
  --ink: #e7f1f8;
  --muted: #93a4b6;
  --faint: #5c6c7e;
  --line: rgba(124, 200, 255, 0.16);
  --line-2: rgba(124, 200, 255, 0.34);
  --accent: #6ff3ff;
  --accent-dim: rgba(111, 243, 255, 0.12);
  --glow: 0 0 10px rgba(111, 243, 255, 0.22);
  --warm: #ff7a3d;
  --cool: #3fd0ff;
  --good: #46e0a0;
  --bad: #ff6b6b;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 28px clamp(16px, 4vw, 48px) 48px;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1,
h2 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.01em;
}

h1 {
  font-size: 22px;
}

h2 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* ------------------------------------------------------------------ masthead */

.masthead {
  /* Two explicit columns, not `flex-wrap`. The right-hand column holds the identity table,
     whose widest row is a 66-character type-script hash with `word-break: break-all` — so its
     max-content width is enormous, and any layout that lets it take that width pushes the whole
     column onto a line of its own. Bounding it here is what keeps the wallet button beside the
     title instead of under it.

     The bound is measured, not rounded. It was a flat `460px`, and the identity wants `570px` —
     79px of label, 14px of gap, and 477px for the longest hash — so at 460 two of the six values
     wrapped onto a second line, and the slack left over in the row (the standfirst stops at its
     own measure, well short of its column) showed up as a 238px hole between the two columns.

     `min()` rather than a second round number, so the bound gives way before the standfirst does:
     above ~1270px the identity gets the 570px it needs, and below that it takes 45% of the row and
     the prose keeps the rest. Without this the standfirst is squeezed to 350px at a 960px window,
     which is the width at which an opening paragraph stops being readable. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, min(570px, 45%));
  gap: 24px 40px;
  align-items: start;
  max-width: 1280px;
  margin: 0 auto 24px;
}

@media (max-width: 900px) {
  .masthead {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The wallet lives at the top right, above the identity table, because a connect button has to
   be findable without looking for it. `justify-items: end` keeps it flush with the table. */
.masthead-side {
  display: grid;
  gap: 14px;
  justify-items: end;
  min-width: 0;
}

/* The language switch and the wallet button share the top-right row. The switch is deliberately
   quiet — it is a setting, not the page's call to action — and the active one is marked by colour
   rather than by being a different size, so the row does not jump when it changes. */
.masthead-tools {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.lang-switch {
  display: flex;
  gap: 2px;
}

.lang-switch button.lang {
  padding: 5px 9px;
  font-size: 12px;
  border-color: transparent;
  background: transparent;
  color: var(--muted);
}

.lang-switch button.lang.active {
  border-color: var(--line);
  background: #fbfbfa;
  color: var(--ink);
}

.wallet-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.wallet-account {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
}

/* The one button on the page that is an invitation rather than a control. */
button.primary {
  border-color: #2f6f4f;
  background: #2f6f4f;
  color: #fff;
}

button.primary:hover:not(:disabled) {
  border-color: #275c42;
  background: #275c42;
}

.standfirst {
  max-width: 62ch;
  margin: 8px 0 0;
  color: var(--muted);
}

.identity {
  display: grid;
  grid-template-columns: auto auto;
  gap: 2px 14px;
  margin: 0;
  font-size: 12.5px;
}

.identity dt {
  color: var(--faint);
}

.identity dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 12px;
  word-break: break-all;
}

/* ------------------------------------------------------------------ layout */

main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: 24px;
  align-items: start;
  max-width: 1280px;
  margin: 0 auto;
}

@media (max-width: 980px) {
  main {
    grid-template-columns: minmax(0, 1fr);
  }
}

.stage,
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.stage {
  padding: 18px 18px 14px;
  /* The ring stays on screen while the reader scrolls the right-hand column, which is the whole
     point of it: the Drive buttons are at the bottom of that column, and a click that changes the
     ring is only worth watching if the ring is still visible when it changes. Before this, a
     reader clicked "tick 64" and the thing they clicked it for was a screen and a half above
     them, so the only evidence anything had happened was a number in a panel.

     Why it works is measured rather than derived, because the derivation is wrong. In the
     two-column layout the obvious story holds: the grid's row is as tall as the column and
     `align-items: start` leaves the stage at its content height, so it has the row to travel in.
     But the single-column layout below 980px puts the stage and the column in **separate rows** —
     `grid-template-rows` measures `809px 2346px` — and the stage still pins at `top: 16px` for
     the whole of `main`, 2300px past its own row. So the containing block is larger than the grid
     area the spec would give it, and the explanation above describes only one of the two cases.

     What is asserted is therefore the reader's consequence at *both* widths, not this mechanism:
     `test/panel-live.test.js` checks that with the Drive panel at the foot of the window, the
     whole ring is on screen — at 1440px and at 900px. */
  position: sticky;
  top: 16px;
}

.stage-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}

/* Capped against the viewport, not the column. A square canvas in a 900px-wide column is 900px
   tall, which is taller than most windows — so it could never be sticky, and it pushed the caption
   off the bottom of the screen. `min()` of the two means the ring is as big as it can be while
   still fitting, and `margin: 0 auto` keeps it centred when the cap is what binds. */
.stage canvas {
  display: block;
  width: min(100%, calc(100vh - 150px), 720px);
  height: auto;
  aspect-ratio: 1;
  margin: 0 auto;
}

/* A canvas with no CSS size falls back to its `width`/`height` attributes, which the
   renderer then reads back through `getBoundingClientRect` — so it draws at the wrong
   scale, or not at all. Every canvas needs a box before it can be drawn into. */
.panel canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  border: 1px solid var(--line);
  border-radius: 8px;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.panel {
  padding: 16px 18px 14px;
}

.panel h2 + * {
  margin-top: 10px;
}

/* The explanatory prose. It is the part of the page that has to be read rather than scanned, so it
   is the part that needs a line length: the wide panels are 1244px across, and a sentence set at
   that width is 180 characters long — the eye loses its place on the way back to the left margin
   and the paragraph reads as a grey block. `ch` on a proportional font is the width of `0`, which
   is close enough to the average character for this to mean "about 90 characters". */
.caption {
  /* The same character measure as `.standfirst`, not a wider one. These were `92ch`, which made a
     caption's lines 48% longer than the page's own opening paragraph — and backwards, since this
     is the *smaller* of the two. A measure is how far the eye travels back to find the start of the
     next line, and small text tolerates less of that, not more. `62ch` is about 71 Latin characters
     or 39 Chinese ones at this size, both inside the usual range; at `92ch` the Chinese ran to 58
     characters a line, which is past the point where a reader starts re-reading lines.

     Only three captions are wide enough to be affected — the two under the full-width panels and
     the one under the ring. The rest sit in the 380px column and never reach either number. */
  max-width: 62ch;
  margin: 10px 0 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
}

/* The lead of a caption: the sentence that says what the panel is, before the paragraph that says
   why it is that way. Without it every caption is one weight of grey and a reader has to read all
   of it to find out whether they needed to. */
.caption strong {
  color: var(--ink);
  font-weight: 600;
}

/* ------------------------------------------------------------------ facts */

/*
 * The label column keeps its width; the value column may shrink and wrap.
 *
 * Both halves of that are load-bearing, and the version this replaces had neither. It was
 * `grid-template-columns: 1fr auto`, and `auto` means "as wide as the content wants", so a single
 * value that cannot be broken — a bech32 address, a 64-character type hash — set that column to
 * its own width. An address is 63 characters of monospace, far wider than the panel, and a grid
 * cannot give back width it has already promised: the 1fr label column was left with whatever
 * remained and collapsed to *one character* — 地/址, 签/名/方/式 — while the address ran off the
 * side of the panel.
 *
 * Measured on the deployed page against the old rule, with a real 63-character address in a
 * 262px-wide `.facts`: the label column came out **14px** wide (its two characters wrapped onto
 * three lines), the value column **689px**, and the grid **718px** — 456px wider than the panel
 * it sits in. The label column was narrower than the value's first two characters.
 *
 * `max-content` for the label (two to four characters, it never needs to shrink) and
 * `minmax(0, 1fr)` for the value (it may shrink and wrap) is the whole fix. `overflow-wrap:
 * anywhere` is not decoration either: `break-word` lets the value overflow while still *reporting*
 * its full min-content width to the grid, so the column would stay wide. `anywhere` is the value
 * that also reduces the min-content contribution, which is what `minmax(0, …)` then gets to use.
 */
.facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 16px;
  margin: 0;
  font-size: 13.5px;
}

.facts dt {
  color: var(--muted);
}

.facts dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: var(--mono);
  font-size: 13px;
  min-width: 0;
  overflow-wrap: anywhere;
}

.facts dd.ok {
  color: var(--good);
}

.facts dd.bad {
  color: var(--bad);
}

.facts dd.warm {
  color: var(--warm);
}

/* ------------------------------------------------------------------ drive */

.buttons {
  display: grid;
  /* One column, full width. The cards were two columns of 167px while they were being written,
     and `补充 10,000 步寿命 · +1 CKB` needs about 162px — so the feed card, the one that costs
     money, was the one whose explanation wrapped onto a second line. A grid that has to be
     re-measured in every language is the wrong grid for a line of prose; a list is not. */
  grid-template-columns: 1fr;
  gap: 6px;
}

/* The Drive heading is the only panel title in ink rather than grey. Five buttons are the whole
   point of the page and they sit at the bottom of a column of six panels; the heading is what
   makes them findable on the way past. */
.drive-panel h2 {
  color: var(--ink);
}

button {
  font: inherit;
  font-size: 13px;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fbfbfa;
  color: var(--ink);
  cursor: pointer;
}

button:hover:not(:disabled) {
  border-color: #c9c9c2;
  background: #f3f3f0;
}

button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* An action is a card: the verb, then what it costs. Left-aligned, because a centred two-line
   button reads as a label with a stray line under it. */
button.action {
  display: grid;
  gap: 2px;
  padding: 9px 12px;
  text-align: left;
  line-height: 1.35;
}

button.action .action-label {
  font-size: 13.5px;
  font-weight: 600;
}

button.action .action-sub {
  color: var(--muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* The button that is in flight. `opacity: 1` on purpose: `button:disabled` fades it to 45%, and
   the one button that must *not* look dead is the one that is currently doing something. On the
   server-signed path a click waits for the node to commit — 41 seconds, measured — so for most of
   a minute the only thing distinguishing the running action from the four idle ones is this. */
button.action[aria-busy="true"] {
  opacity: 1;
  border-color: var(--warm);
  background: #fff8f4;
}

button.action[aria-busy="true"] .action-sub {
  color: var(--warm);
}

button.ghost {
  border-color: transparent;
  background: transparent;
  color: var(--muted);
  text-decoration: underline;
  padding: 4px 6px;
}

/* What the last click did. It sits under the buttons rather than in the footer because the footer
   answers a different question — "is the page connected" — and because a reader who has just
   pressed a button is looking at the button, not at the bottom of the document.

   The state is carried by the dot and by the panel's tint, never by the colour of the words: the
   one sentence a reader has to read carefully is the one that says something went wrong, and a
   paragraph in `--bad` at 12.5px is the hardest thing on the page to read. */
.action-status {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 10px 0 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fbfbfa;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

/* `display: flex` above beats the user agent's `[hidden] { display: none }`, so the hidden state
   has to be restated. Without this the empty box is always there, which reads as a broken one. */
.action-status[hidden] {
  display: none;
}

.action-status::before {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--faint);
}

.action-status[data-state="busy"] {
  border-color: #e8cbb8;
  background: #fff8f4;
  color: var(--warm);
}

.action-status[data-state="busy"]::before {
  background: var(--warm);
  animation: pulse 1.1s ease-in-out infinite;
}

.action-status[data-state="ok"] {
  border-color: #c2ddca;
  background: #f4faf6;
  color: var(--good);
}

.action-status[data-state="ok"]::before {
  background: var(--good);
}

.action-status[data-state="bad"] {
  border-color: #e6c6c6;
  background: #fdf5f5;
  color: var(--bad);
}

.action-status[data-state="bad"]::before {
  background: var(--bad);
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.2;
  }
}

/* ------------------------------------------------------------------ timeline */

.world-panel {
  max-width: 1280px;
  margin: 0 auto 24px;
}

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.count {
  color: var(--faint);
  font-size: 12.5px;
}

.roster {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
  /* The tracks below are sized for the widest values the columns can hold, which is wider than a
     narrow window. Without this the last column would simply hang off the edge of the panel. */
  overflow-x: auto;
}

.roster li {
  display: grid;
  /* Measured against the longest value each column can hold, in *both* languages, by
     `test/panel-live.test.js` — which is the check that replaced `node roster-widths.mjs`, a
     script that no longer exists. The widths are not chosen by eye: with the previous numbers the
     step column was 5em and `第 1,024 步` needs 6.24em, so **four of five Chinese rows wrapped onto
     two lines** and the list read as a wall of broken pairs. The English rows did the same thing
     at the numbers the old comment cited.

     Each column is sized for the largest value it can *reach*, not the largest one on screen
     today: `energy` grows with every feed, and `spikes` and `sightings` grow with every tick.
     A row that wraps is much worse than a column with room to spare — the whole point of this
     table is that the eye can run down a column.

     The badge column is the flexible one, and it is the only place the slack can go: it is the
     seam between what a fly *is* (index, instance, key badge) and what it *has* (step, energy,
     life, spikes, sightings, net). Sized by content like the rest, the nine tracks came to 908px
     inside a 1242px row, so every value stopped a third of a panel short of the right edge while
     the row rules ran on past them — the table read as left-shifted, and the values are the one
     thing on the page a reader compares down a column. `minmax()` rather than a bare `1fr` so the
     track keeps a floor when the panel is narrow and `.roster` is scrolling instead. */
  grid-template-columns: 2em 11em minmax(3.5em, 1fr) 6.5em 7em 9em 8em 9em 9em;
  gap: 12px;
  align-items: baseline;
  padding: 7px 8px;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 12.5px;
  cursor: pointer;
  /* `max-content` is the sum of the tracks, so on a narrow window the row keeps its columns and
     `.roster` scrolls; `width: 100%` keeps it filling the panel when the window is wide, so the
     row rules still reach the edge. */
  width: 100%;
  min-width: max-content;
}

.roster li:hover {
  background: #fafaf8;
}

.roster li[aria-current="true"] {
  background: #f0f6ff;
  box-shadow: inset 3px 0 0 var(--cool);
}

.roster .idx,
.roster .label {
  color: var(--faint);
}

.roster .net.negative {
  color: var(--warm);
}

.roster .net.positive {
  color: var(--good);
}

/* Right-aligned, because these are the columns a reader compares *down* and each track is sized for
   the widest value it can reach — so a shorter value has room to spare, and spending that room on
   the left is what puts every column's last character on one vertical rule. `--mono` is tabular, so
   the digits line up under each other too. `.label` is only ever a value with a word around it
   (`第 545 步`, `存活 · 1 代`, `20 次目击`); the identity block on the left is `.idx`, `.instance`
   and the badge, and stays left-aligned. `.timeline` already right-aligns its numeric column — this
   table was the odd one out. */
.roster .label,
.roster .energy,
.roster .spikes,
.roster .net {
  text-align: right;
}

/* The organism this server holds the key for. Watching is unrestricted; driving is not, and
   the roster is where a reader finds out which one the buttons act on. */
.roster .owned {
  justify-self: start;
  padding: 0 5px;
  border: 1px solid var(--cool);
  border-radius: 999px;
  color: var(--cool);
  font-size: 10.5px;
  line-height: 1.5;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.roster .dead {
  color: var(--bad);
}

.timeline-panel {
  max-width: 1280px;
  margin: 24px auto 0;
}

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
  border-top: 1px solid var(--line);
}

.timeline li {
  display: grid;
  grid-template-columns: 2.4em 7em minmax(0, 1fr) 5.5em 9em 6em;
  gap: 12px;
  padding: 7px 8px;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 12.5px;
  cursor: pointer;
}

.timeline li:hover {
  background: #fafaf8;
}

.timeline li[aria-current="true"] {
  background: #f0f6ff;
  box-shadow: inset 3px 0 0 var(--cool);
}

.timeline .idx,
.timeline .block,
.timeline .head {
  color: var(--faint);
}

.timeline .action {
  font-family: inherit;
  font-size: 13px;
}

.timeline .action em {
  color: var(--muted);
  font-style: normal;
}

.timeline .spikes {
  text-align: right;
}

.timeline .spikes.fired {
  color: var(--warm);
  font-weight: 600;
}

/* The page's own health, at the bottom because it is about the page rather than about anything the
   reader did. It is one notch larger and darker than the captions: it is a line of live state, and
   the version of it that was 12.5px in `--faint` was invisible in the one case it matters — when
   it is the only thing on the page saying something is wrong. */
footer {
  max-width: 1280px;
  margin: 20px auto 0;
  color: var(--muted);
  font-size: 13px;
}

footer #status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

footer #status::before {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--faint);
}

footer #status[data-state="busy"]::before {
  background: var(--warm);
  animation: pulse 1.1s ease-in-out infinite;
}

footer #status[data-state="ok"]::before {
  background: var(--good);
}

footer #status[data-state="bad"] {
  color: var(--bad);
}

footer #status[data-state="bad"]::before {
  background: var(--bad);
}

/* CCC's connector is the wallet picker: a Lit element that renders the whole modal, fixed and
   full-viewport. It is kept out of the way until the reader asks for it — `visibility` rather than
   `display`, because the element measures its own scene to size the card and a `display: none`
   element measures zero. A hidden element takes no clicks, so this also keeps it from swallowing
   the page underneath. */
ccc-connector.wallet-closed {
  visibility: hidden;
  pointer-events: none;
}

/* The connector's theme.
   `@ckb-ccc/connector` ships no stylesheet. Its styles are Lit `css` blocks that read these
   twelve custom properties, and their defaults live in `@ckb-ccc/connector-react` — which this
   page does not use, because it does not use React. With none of them defined, `background:
   var(--background)` on the modal card resolves to nothing, so the card is transparent: the page
   shows through it and the wallet names sit on top of the fly. That is what it looked like.

   The values are the light-theme defaults from CCC's connector documentation, matched to this
   page's own neutrals wherever the page already has one — a modal is a surface, and a surface
   that is `#fff` while every panel around it is `#fff` is the same decision written twice.

   Set on the element rather than on `:root`, because the connector is the only thing that reads
   them and custom properties inherit into its shadow tree from here. */
ccc-connector {
  --background: var(--panel);
  --divider: var(--line);
  --btn-primary: var(--bg);
  --btn-primary-hover: var(--line);
  --btn-secondary: var(--line);
  --btn-secondary-hover: #ccc;
  --btn-color: var(--ink);
  --btn-color-hover: var(--ink);
  --icon-primary: var(--ink);
  --icon-secondary: var(--muted);
  --tip-color: var(--muted);
  --tip-color-hover: var(--ink);
  /* The connector's own text inherits `color`, and `--btn-color` falls back to it. Set here
     rather than inherited from `body` so the modal cannot be themed by accident. */
  color: var(--ink);
}

/* ================================================================== instrument theme
 *
 * Everything above this line was written for a light page and is kept because the *layout* in it
 * was measured — column widths, the sticky stage, the measure of a caption. What follows only
 * re-dresses: surfaces, hairlines, glow, and the components whose colours were written out by
 * hand rather than read from a variable. Same selectors, later in the file, so they win.
 */

body {
  background:
    radial-gradient(1100px 620px at 10% -12%, rgba(111, 243, 255, 0.075), transparent 62%),
    radial-gradient(900px 560px at 100% 0%, rgba(255, 122, 61, 0.055), transparent 58%),
    var(--bg);
  background-attachment: fixed;
}

/* A faint instrument grid, and a vignette that pulls the eye to the middle of the ring. Both are
   one fixed layer behind everything: the page has exactly one, so it costs one composited surface
   rather than one per panel. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(124, 200, 255, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(124, 200, 255, 0.045) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(1200px 800px at 50% 0%, #000 30%, transparent 85%);
  -webkit-mask-image: radial-gradient(1200px 800px at 50% 0%, #000 30%, transparent 85%);
}

::selection {
  background: rgba(111, 243, 255, 0.28);
}

/* ---- surfaces */

.stage,
.panel {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow:
    0 1px 0 0 rgba(255, 255, 255, 0.03) inset,
    0 20px 44px -30px rgba(0, 0, 0, 0.95);
}

/* The anchor the lit edge below is positioned against. `position: relative` is set on `.panel`
   only, and that is not a stylistic choice: the stage *is* `position: sticky`, and writing
   `position: relative` on the pair above silently replaced it — the ring stopped following the
   reader down the page, which is the one thing the sticky rule exists for. `test/panel-live.test.js`
   caught it: "the ring spans -1255–-535 in a 900px window". A sticky element is already a
   positioned ancestor, so the stage needs nothing here. */
.panel {
  position: relative;
}

/* The lit edge along the top of every card: the one piece of chrome that makes a panel read as a
   panel rather than as a rounded box. `pointer-events: none` so it never eats a click on the
   heading underneath it. */
.stage::before,
.panel::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 14px;
  right: 14px;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--line-2) 22%, var(--accent) 50%, var(--line-2) 78%, transparent);
  opacity: 0.55;
}

/* ---- type */

h1 {
  letter-spacing: -0.02em;
  text-shadow: 0 0 22px rgba(111, 243, 255, 0.18);
}

h2 {
  color: #7fd9ea;
  display: flex;
  align-items: center;
  gap: 8px;
}

h2::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  background: var(--accent);
  box-shadow: var(--glow);
  transform: rotate(45deg);
}

.standfirst strong,
.caption strong {
  color: #dff3fb;
}

a {
  color: var(--accent);
  text-decoration-color: rgba(111, 243, 255, 0.35);
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-color: var(--accent);
}

code {
  font-family: var(--mono);
  font-size: 0.92em;
  color: #a9e9ff;
  background: rgba(111, 243, 255, 0.07);
  border: 1px solid rgba(111, 243, 255, 0.12);
  border-radius: 4px;
  padding: 1px 5px;
}

/* ---- the page nav, which the two prose pages share with the fly */

.page-nav {
  display: flex;
  align-items: center;
  gap: 4px;
}

.page-nav a {
  font-size: 12.5px;
  padding: 4px 9px;
  border: 1px solid transparent;
  border-radius: 7px;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}

.page-nav a:hover {
  color: var(--ink);
  border-color: var(--line);
}

.lang-switch button.lang {
  color: var(--muted);
  border-color: transparent;
  background: transparent;
}

/* The switch is the one control that sits in the masthead next to the wallet button, and it was
   still wearing the light theme's pill: `background: #fbfbfa` on a near-black page, which read as
   a white blob rather than as "EN is the language you are reading". It follows the same
   accent-tint-plus-glow as the page nav now, so the two controls in that row look like one row. */
.lang-switch button.lang.active {
  border-color: var(--line-2);
  background: var(--accent-dim);
  color: var(--ink);
  box-shadow: var(--glow);
}

.page-nav a[aria-current="page"] {
  color: var(--ink);
  border-color: var(--line-2);
  background: var(--accent-dim);
  box-shadow: var(--glow);
}

/* ---- controls */

button {
  background: rgba(19, 28, 41, 0.92);
  border-color: var(--line);
  border-radius: 8px;
  color: var(--ink);
}

button:hover:not(:disabled) {
  background: rgba(28, 42, 60, 0.96);
  border-color: var(--line-2);
  box-shadow: var(--glow);
}

button:focus-visible,
a:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}

button.primary {
  border-color: #1fc9e0;
  background: linear-gradient(180deg, #24d3ea, #0f9fb8);
  color: #032028;
  font-weight: 600;
  text-shadow: none;
}

button.primary:hover:not(:disabled) {
  border-color: #6ff3ff;
  background: linear-gradient(180deg, #46e4f7, #14b0cb);
  box-shadow: 0 0 16px rgba(111, 243, 255, 0.35);
}

button.ghost {
  color: var(--muted);
}

button.action {
  background: linear-gradient(180deg, rgba(23, 34, 49, 0.92), rgba(14, 21, 31, 0.92));
  border-left: 2px solid rgba(124, 200, 255, 0.2);
}

button.action .action-label {
  color: var(--ink);
}

button.action:hover:not(:disabled) {
  border-left-color: var(--accent);
}

button.action[aria-busy="true"] {
  border-color: var(--warm);
  border-left-color: var(--warm);
  background: rgba(255, 122, 61, 0.12);
  box-shadow: 0 0 16px rgba(255, 122, 61, 0.22);
}

/* ---- the status boxes */

.action-status {
  background: rgba(19, 28, 41, 0.7);
  color: var(--muted);
}

.action-status[data-state="busy"] {
  border-color: rgba(255, 122, 61, 0.45);
  background: rgba(255, 122, 61, 0.1);
  color: #ffb08a;
}

.action-status[data-state="ok"] {
  border-color: rgba(70, 224, 160, 0.4);
  background: rgba(70, 224, 160, 0.09);
  color: #7ff0c2;
}

.action-status[data-state="bad"] {
  border-color: rgba(255, 107, 107, 0.45);
  background: rgba(255, 107, 107, 0.1);
  color: #ff9d9d;
}

/* ---- the readouts */

.facts dd {
  color: #d3e8f4;
  text-shadow: 0 0 10px rgba(111, 243, 255, 0.12);
}

.facts dd.ok {
  color: var(--good);
}

.facts dd.bad {
  color: var(--bad);
}

.facts dd.warm {
  color: var(--warm);
}

.identity dd {
  color: #b9d4e6;
}

.identity dt,
.count {
  color: var(--faint);
}

.canvas,
.panel canvas,
.stage canvas {
  background: radial-gradient(circle at 50% 45%, rgba(24, 48, 68, 0.5), rgba(5, 8, 12, 0.85) 70%);
  border-color: var(--line);
}

/* ---- lists: a readout, not a table */

.roster li,
.timeline li {
  border-bottom: 1px solid rgba(124, 200, 255, 0.07);
  transition: background 120ms ease;
}

.roster li:hover,
.timeline li:hover {
  background: rgba(111, 243, 255, 0.05);
  cursor: pointer;
}

.roster li[aria-current="true"],
.timeline li[aria-current="true"] {
  background: rgba(111, 243, 255, 0.09);
  box-shadow: inset 3px 0 0 0 var(--accent);
}

.roster .instance,
.timeline .head,
.timeline .step {
  color: #b9d4e6;
}

.roster .net.positive {
  color: var(--good);
}

.roster .net.negative {
  color: var(--warm);
}

.roster .spikes.fired,
.timeline .spikes.fired {
  color: var(--warm);
  text-shadow: 0 0 10px rgba(255, 122, 61, 0.35);
}

/* ---- the prose pages */

main.prose {
  display: block;
  max-width: 74ch;
  margin: 0 auto;
}

.prose h2 {
  margin-top: 34px;
  font-size: 14px;
}

.prose h2:first-child {
  margin-top: 0;
}

.prose p {
  margin: 12px 0 0;
  color: #c8dae7;
}

/* Paragraphs that carry a blank line in the dictionary rather than being split into two elements:
   `pre-line` is what turns the dictionary's `\n\n` into a break without a second markup path. */
.multiline {
  white-space: pre-line;
}

.explain {
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.explain li {
  position: relative;
  padding: 8px 0 8px 22px;
  color: #c8dae7;
  border-top: 1px solid rgba(124, 200, 255, 0.07);
}

.explain li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 15px;
  width: 5px;
  height: 5px;
  background: var(--accent);
  transform: rotate(45deg);
  opacity: 0.7;
}

.facts-table {
  width: 100%;
  margin: 12px 0 0;
  border-collapse: collapse;
}

.facts-table td {
  padding: 9px 12px 9px 0;
  vertical-align: top;
  border-top: 1px solid rgba(124, 200, 255, 0.07);
  color: #c8dae7;
  font-size: 13.5px;
}

.facts-table td:first-child {
  width: 30%;
  color: var(--ink);
  white-space: nowrap;
}

.facts-table tr:hover td {
  background: rgba(111, 243, 255, 0.04);
}

footer {
  color: var(--faint);
}

/* CCC's modal is its own component and reads these; a translucent surface would let the page
   through the dialog, so it gets a solid one. */
ccc-connector {
  --background: #0b1119;
  --btn-primary: var(--accent);
  --btn-primary-hover: #9df7ff;
  --btn-color: #04222a;
  --btn-color-hover: #04222a;
  --btn-secondary: var(--line-2);
  --btn-secondary-hover: var(--accent-dim);
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}
