/* ==========================================================================
   Components — cards, stats, tables, badges, controls, feedback
   ========================================================================== */

/* --------------------------------------------------------------------------
   Glass card
   -------------------------------------------------------------------------- */

.card {
  /* Single source for the card's inset. Full-bleed children (sparklines,
     table wrappers) cancel it by name, so padding can change in one place
     without their negative margins drifting out of sync. */
  --card-pad: var(--sp-6);

  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--card-pad);
  border-radius: var(--radius-lg);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  box-shadow: var(--shadow-md);
  /* Deliberately not `overflow: hidden`: an info tooltip anchored near the
     card's top edge has to escape it. Full-bleed children clip themselves
     instead — see .stat__spark. */
  transition:
    border-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}

/* Nested cards (found blocks, donation panels) sit inside a padded parent
   and would otherwise double up on inset. */
.card .card {
  --card-pad: var(--sp-5);
  box-shadow: none;
}

/* Specular top edge — what sells the glass. */
.card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--glass-highlight) 22%,
    var(--glass-highlight) 78%,
    transparent
  );
  pointer-events: none;
}

/* A restrained brand-tinted lift: border warms, shadow deepens slightly.
   No glow — the tile is data, not a call to action. */
.card--interactive:hover {
  border-color: var(--brand-border-hover);
  box-shadow: var(--shadow-lg);
  transform: translateY(-2px);
}

.card--interactive:hover .stat__icon {
  color: var(--brand-ink);
}

.card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}

.card__title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
}

.card__subtitle {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 2px;
}

/* --------------------------------------------------------------------------
   Stat tile
   -------------------------------------------------------------------------- */

.stat {
  gap: var(--sp-1);
}

.stat__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
}

.stat__icon {
  display: grid;
  place-items: center;
  /* The card tints this on hover; without a matching transition the icon
     snapped while the card itself eased. */
  transition: color var(--dur-base) var(--ease-out);
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--glass-bg-strong);
  color: var(--text-secondary);
}

.stat__icon svg {
  width: var(--icon-md);
  height: var(--icon-md);
}

.stat__icon--brand {
  background: var(--brand-dim);
  color: var(--brand-ink);
}

.stat__label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
}

/* The hero number. Tabular so digit churn never shifts layout.

   The figure and its unit are one token: "918.1 EH/s" is a reading, not a
   sentence, and the only correct number of lines for it is one. This used to
   carry `word-break: break-word`, which does not break *between* the value and
   the unit — it breaks *inside* them, and at 150px cards it rendered "918." /
   "1" and "EH/ s". `nowrap` is scoped to this group alone; the label, hint,
   delta and notes elsewhere in the card still wrap normally. Fitting the group
   is then the container query's job, below. */
.stat__value {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-display);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.stat__unit {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
  letter-spacing: 0;
  /* Part of the same unbreakable group; "EH/ s" was the other half of the bug. */
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Stat figures at the width the card actually has

   The viewport does not predict this card's width — it is not even monotonic
   in it. `.grid--stats` is `auto-fit, minmax(150px, 1fr)` below 520px, so a
   320px phone gets ONE 296px card (248px inside) while a 360px phone gets TWO
   160px cards (112px inside): the wider screen has the narrower tile. Add the
   sidebar collapsing on desktop and a media query is guessing at the wrong
   number. A container query asks the only question that matters.

   `container-type: inline-size` is already how `.stat__summary-col` handles
   its LOW/HIGH pair — same mechanism, one level out.
   -------------------------------------------------------------------------- */

.grid--stats > .stat {
  container-type: inline-size;
  container-name: stat;
}

/* Thresholds are content-box widths — that is what a container query measures,
   not the card's border box. Measured in Chromium: 219px inside a card at
   1440, 179px at 1280, then 147/127/119/112/104 across 430/390/375/360/344,
   and 248 at 320 where the grid is one-up. So 160px separates every phone
   two-up case from every desktop one, with ~13px of margin on each side.

   Two-up on a phone leaves 104-147px. The 32px figure needs ~128px for
   "918.1 EH/s" before its unit even starts. */
@container stat (max-width: 160px) {
  .stat__value {
    font-size: var(--fs-xl);
    column-gap: var(--sp-1);
  }

  .stat__unit {
    font-size: var(--fs-sm);
  }
}

/* Narrower still — 344 to 390px phones, 104-127px inside. "143.6K days" does
   not fit there even at 24px. */
@container stat (max-width: 130px) {
  .stat__value {
    font-size: var(--fs-lg);
  }

  .stat__unit {
    font-size: var(--fs-xs);
  }
}

.stat__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   Stat tile summary — two balanced columns beneath the headline figure.

   Used only where a tile has detail worth two columns; every other tile is
   untouched. The rules are real grid tracks rather than borders so they stay
   centred regardless of how long each column's text runs.
   -------------------------------------------------------------------------- */

.stat__summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--glass-border);
}

.stat__summary-rule {
  align-self: stretch;
  background: var(--glass-border);
}

.stat__summary-col {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.stat__summary-label {
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
}

.stat__summary-value {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-heading);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

/* A ratio is one value group too: "1 : 21.9M" is a single reading, and
   `overflow-wrap: anywhere` above was splitting it across three lines, one per
   token. On a phone it becomes unbreakable instead.

   Deliberately a viewport query, not a container one. The two-up cards this
   has to fix are 104-147px inside, but the same card is 248px inside one-up at
   320px and 179px on a 1280px laptop — so container width alone cannot tell a
   phone from a laptop here, and at 179px the desktop columns are 72px wide,
   where "38.39 TH/s" needs 73px and would start showing an ellipsis. Above
   520px the old wrapping behaviour is exactly as it was. */
@media (max-width: 520px) {
  .stat__summary-value {
    overflow-wrap: normal;
    white-space: nowrap;
    /* Backstop only. The columns below are 106px one-up and the full card
       width stacked; every real figure clears both with room to spare. */
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

.stat__summary-note {
  font-size: var(--fs-2xs);
  line-height: 1.45;
  color: var(--text-muted);
}

/* Direction is carried by an arrow in the text as well as by colour, so the
   comparison survives a greyscale print or a colourblind reader. */
/* Two peer readings under one label. The key sits inline so the pair costs
   two lines rather than four, and wraps beneath its key — never mid-figure —
   once the column is too narrow to hold both. */
.stat__summary-rows {
  display: grid;
  /* One shared pair of tracks for both rows. The key track is sized to the
     wider of LOW/HIGH by the browser, so both figures start on exactly the
     same edge whatever the label happens to render at — an em guess here is
     what let HIGH push its own figure out of line. */
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  gap: 2px var(--sp-2);
}

/* Two ~40px columns cannot hold "1 : 21.9M", which needs ~68px: the ratio was
   splitting across three lines, one per token. Stacking the readings gives
   each the card's full width, and at ~112px every figure fits with room over.

   This has to sit after the `.stat__summary` rules it overrides: a container
   query carries no specificity of its own, so source order decides — the same
   trap that kept the mobile figure at 32px for as long as it has been shipping.
   -------------------------------------------------------------------------- */

@container stat (max-width: 160px) {
  .stat__summary {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-3);
  }

  /* A vertical rule between stacked readings would divide nothing. */
  .stat__summary-rule {
    display: none;
  }
}

/* Scoped with :has so only the columns that carry a range become query
   containers; the other tiles' columns are left exactly as they were. */
.stat__summary-col:has(.stat__summary-rows) {
  container-type: inline-size;
}

/* Narrower than this the pair cannot sit side by side without the figure
   spilling past the divider, so the key moves above it. Both figures still
   share an edge, and the column stays shorter than the tile's first one. */
@container (max-width: 100px) {
  .stat__summary-rows {
    grid-template-columns: minmax(0, 1fr);
  }

  .stat__summary-key {
    margin-top: 2px;
  }

  .stat__summary-key:first-child {
    margin-top: 0;
  }
}

.stat__summary-key {
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  /* Both cells declare a tight line-height rather than inheriting the
     document's 1.5: a grid row is as tall as its taller cell, and the
     inherited value made the pair tall enough to drive the whole tile. */
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
}
/* Deliberately one style for both ends: a range has no senior figure. */
.stat__summary-reading {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  letter-spacing: var(--tracking-heading);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.stat__summary-value.is-good { color: var(--status-good); }
.stat__summary-value.is-bad { color: var(--status-critical); }
.stat__summary-value.is-muted { color: var(--text-secondary); }
.stat__summary-note.is-good { color: var(--status-good); }
.stat__summary-note.is-bad { color: var(--status-critical); }
.stat__summary-note.is-muted { color: var(--text-muted); }

/* --------------------------------------------------------------------------
   Paired stat tile — two figures reading as one answer.

   Nothing here changes the card's box. The tile keeps `.card`'s padding,
   radius, border, shadow and hover, and keeps `.stat__head` exactly as every
   other tile has it; only the band beneath the heading is arranged
   differently. That band takes `flex: 1` and centres its contents, so it
   absorbs whatever height the grid row already gives the card — grid stretches
   every tile in a row to the tallest — rather than asking for any of its own.

   The vertical rule is a real 1px grid track, following stat__summary above:
   a border on one half would hang off that half's edge instead of sitting
   midway, and would drift as the two halves take unequal widths.
   -------------------------------------------------------------------------- */

.stat__pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-2);
  flex: 1;
  min-height: 0;
}

.stat__pair-rule {
  align-self: stretch;
  background: var(--glass-border);
}

/* The half is a column: mark and figure on one line, caption under both.

   The caption under the *figure* alone was the first arrangement, and Chrome
   measured it wrong at every width where the sidebar is docked: at 768px the
   figure column is 39px and "MINERS" needs 53, so the word painted out across
   the divider. Given the whole half it has 75-110px and fits everywhere. */
.stat__pair-half {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

.stat__pair-line {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* Larger than .stat__icon and without its plate: at this size the mark is
   part of the reading rather than a label's bullet, and a filled tile behind
   it would compete with the figure for the eye. */
.stat__pair-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  color: var(--text-secondary);
  transition: color var(--dur-base) var(--ease-out);
}

.stat__pair-icon svg {
  width: 28px;
  height: 28px;
  stroke-width: 1.6;
}

/* One brand accent, on the mining half. Two would be decoration; none would
   leave the card reading as grey furniture. */
.stat__pair-half:first-child .stat__pair-icon {
  color: var(--brand);
}

/* The same size, weight and tabular figures as a tile's headline number: this
   card has two headlines, not one headline and a detail. */
.stat__pair-value {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-display);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Two figures share the width one used to have. Chrome puts a half at 75px
   where the sidebar is docked, which holds "999" at the full headline size and
   not "9,999" — that measures about 100px and would run through the divider.
   The size steps down by rendered length, set in pair-stat-card.js, so only
   the figures that need it pay for it. */
.stat__pair-value[data-size="md"] {
  font-size: var(--fs-xl);
}

.stat__pair-value[data-size="sm"] {
  font-size: var(--fs-lg);
}

.stat__pair-caption {
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
  white-space: nowrap;
}

/* Stack the mark over the figure once the card is too narrow to hold them side
   by side.

   A container query, on the same `stat` container the headline figures above
   already use, and for the same reason spelled out there: the viewport does
   not predict this card's width and is not monotonic in it. A 320px phone gets
   ONE 248px-wide card, where the halves fit side by side comfortably; a 375px
   phone gets TWO, 119px inside, where the caption alone needs 53px of a 51px
   half. A media query would have stacked the 320px case that did not need it
   and is the wrong question everywhere else.

   160px is the threshold the figures above already draw, and it separates the
   same two populations here: measured inside-widths are 248 at 320px one-up,
   then 104-147 across the two-up phone widths, and 179-219 on desktop.

   Every size below is set against a measured budget rather than by eye. The
   stacked half comes to 20 + 2 + 26.4 + 2 + 13.2 = 63.6px, inside the ~72px
   the replaced tile leaves under its heading at these widths. The first cut
   used a 22px mark and a full-size figure, came to 70.9px, and pushed every
   tile in the row 9.7px taller — which is what the browser pass caught. */
@container stat (max-width: 160px) {
  .stat__pair-line {
    flex-direction: column;
    gap: 2px;
  }

  .stat__pair-icon svg {
    width: 20px;
    height: 20px;
  }

  .stat__pair-value {
    font-size: var(--fs-xl);
  }

  .stat__pair-value[data-size="md"],
  .stat__pair-value[data-size="sm"] {
    font-size: var(--fs-lg);
  }

  .stat__pair-caption {
    line-height: 1.2;
  }
}

/* Inline sparkline sits flush to the tile's bottom edge, and clips itself
   to the card's bottom corners now that the card no longer clips. */
.stat__spark {
  margin: var(--sp-4) calc(var(--card-pad) * -1) calc(var(--card-pad) * -1);
  height: 48px;
  overflow: hidden;
  border-bottom-left-radius: var(--radius-lg);
  border-bottom-right-radius: var(--radius-lg);
}

.stat__spark canvas {
  width: 100% !important;
  height: 100% !important;
}

/* --------------------------------------------------------------------------
   Delta / trend indicator
   -------------------------------------------------------------------------- */

.delta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px var(--sp-2);
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}

.delta svg {
  width: var(--icon-xs);
  height: var(--icon-xs);
}

.delta--up {
  background: var(--status-good-dim);
  color: var(--status-good);
}

.delta--down {
  background: var(--status-critical-dim);
  color: var(--status-critical);
}

.delta--flat {
  background: var(--glass-bg-strong);
  color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   Badges & pills
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px var(--sp-3);
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.02em;
  white-space: nowrap;
  background: var(--glass-bg-strong);
  color: var(--text-secondary);
  border: 1px solid var(--glass-border);
}

.badge--good {
  background: var(--status-good-dim);
  color: var(--status-good);
  border-color: transparent;
}

.badge--warning {
  background: var(--status-warning-dim);
  color: var(--status-warning);
  border-color: transparent;
}

.badge--critical {
  background: var(--status-critical-dim);
  color: var(--status-critical);
  border-color: transparent;
}

.badge--brand {
  background: var(--brand-dim);
  color: var(--brand-ink);
  border-color: transparent;
}

/* Status-tinted figures for key/value rows — same palette as the badges,
   so a count and a badge for the same state always agree. */
.status-text {
  font-weight: var(--fw-bold);
}

.status-text--good {
  color: var(--status-good);
}

.status-text--warning {
  color: var(--status-warning);
}

.status-text--critical {
  color: var(--status-critical);
}

/* Status dot; pulses only while the state is "live". */
.dot {
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  border-radius: 50%;
  background: currentColor;
}

.dot--pulse {
  animation: pulse-ring 2.4s var(--ease-in-out) infinite;
}

@keyframes pulse-ring {
  0% {
    box-shadow: 0 0 0 0 currentColor;
    opacity: 1;
  }
  70% {
    box-shadow: 0 0 0 6px transparent;
    opacity: 0.85;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .dot--pulse {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  /* Height is fixed rather than derived from padding, so every control
     lands on the same baseline regardless of its font size or content. */
  min-height: var(--control-h);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.btn:hover {
  background: var(--brand-dim);
  color: var(--brand-ink);
  border-color: var(--brand-border-hover);
}

.btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.btn svg {
  width: var(--icon-md);
  height: var(--icon-md);
}

.btn--primary {
  background: var(--brand);
  border-color: transparent;
  color: var(--text-inverse);
}

.btn--primary:hover {
  background: var(--brand-hover);
  color: var(--text-inverse);
}

.btn--icon {
  padding: 0;
  width: var(--control-h);
  height: var(--control-h);
}

.btn--sm {
  min-height: var(--control-h-sm);
  padding: 0 var(--sp-3);
  font-size: var(--fs-xs);
}

.btn--sm svg {
  width: var(--icon-sm);
  height: var(--icon-sm);
}

.btn[aria-pressed="true"],
.btn[data-active="true"] {
  background: var(--brand-dim);
  color: var(--brand-ink);
  border-color: transparent;
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Spin the refresh glyph while a fetch is in flight. */
.btn[data-busy="true"] svg {
  animation: spin 900ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* --------------------------------------------------------------------------
   Segmented control (time-range filters)
   -------------------------------------------------------------------------- */

/* Total height matches --control-h: 1px border + 3px padding on each side
   leaves exactly --control-h-sm for the buttons inside. */
.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-md);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}

.segmented__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-h-sm);
  padding: 0 var(--sp-3);
  border-radius: calc(var(--radius-md) - 3px);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.segmented__btn:hover {
  color: var(--text-primary);
}

.segmented__btn[aria-pressed="true"] {
  background: var(--glass-bg-strong);
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   Form controls
   -------------------------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.field__label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
}

.field__hint {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.input,
.select {
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--sp-3);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  border: 1px solid var(--glass-border);
  color: var(--text-primary);
  font-size: var(--fs-sm);
  transition: border-color var(--dur-fast) var(--ease-out);
}

.input:focus,
.select:focus {
  border-color: var(--brand);
}

.input::placeholder {
  color: var(--text-muted);
}

/* An entry the app cannot act on. Colour reinforces, it never carries the
   message on its own: the hint below the field says what is wrong in words,
   and `aria-invalid` reports it to assistive technology. Both tokens are
   theme-provided, so this reads correctly on paper and at night. */
.input[aria-invalid='true'] {
  border-color: var(--status-critical);
}

.field__hint[data-state='error'] {
  color: var(--status-critical);
}

.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
    linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
  background-position:
    calc(100% - 18px) calc(50% + 2px),
    calc(100% - 13px) calc(50% + 2px);
  background-size: 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--sp-10);
}

/* Toggle switch */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  cursor: pointer;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.switch__track {
  width: 42px;
  height: 24px;
  flex-shrink: 0;
  padding: 3px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  transition: background-color var(--dur-base) var(--ease-out);
}

.switch__thumb {
  display: block;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text-secondary);
  transition:
    transform var(--dur-base) var(--ease-out),
    background-color var(--dur-base) var(--ease-out);
}

.switch input:checked + .switch__track {
  background: var(--brand);
}

.switch input:checked + .switch__track .switch__thumb {
  transform: translateX(18px);
  background: var(--text-inverse);
}

.switch input:focus-visible + .switch__track {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Tables
   -------------------------------------------------------------------------- */

/* Wide tables scroll inside their own container — the page never does.
   The wrapper is focusable so a keyboard user can scroll it without a
   pointer. */
.table-wrap {
  overflow-x: auto;
  margin: 0 calc(var(--card-pad) * -1) calc(var(--card-pad) * -1);
  padding: 0 var(--card-pad) var(--card-pad);
}

.table-wrap:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
  border-radius: var(--radius-md);
}

.table {
  min-width: 560px;
  font-size: var(--fs-sm);
}

.table th {
  position: sticky;
  top: 0;
  padding: var(--sp-3);
  text-align: left;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
  white-space: nowrap;
  background: var(--surface-2);
  border-bottom: 1px solid var(--glass-border);
}

.table th[data-sortable] {
  cursor: pointer;
  user-select: none;
}

.table th[data-sortable]:hover {
  color: var(--text-primary);
}

.table th[aria-sort]::after {
  content: "";
  display: inline-block;
  margin-left: var(--sp-2);
  border: 4px solid transparent;
  vertical-align: middle;
}

.table th[aria-sort="ascending"]::after {
  border-bottom-color: var(--brand);
  margin-bottom: 4px;
}

.table th[aria-sort="descending"]::after {
  border-top-color: var(--brand);
  margin-top: 4px;
}

.table td {
  padding: var(--sp-3);
  border-bottom: 1px solid var(--divider);
  color: var(--text-secondary);
  white-space: nowrap;
}

.table tbody tr {
  transition: background-color var(--dur-fast) var(--ease-out);
}

.table tbody tr:hover {
  background: var(--glass-bg-strong);
}

.table tbody tr:hover td {
  color: var(--text-primary);
}

.table th[data-sortable]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.table tbody tr:last-child td {
  border-bottom: none;
}

.table .is-primary {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
}

.table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   The four-column worker table on a phone

   `.table` carries `min-width: 560px`, which is right for eight columns of
   block history — those have to scroll and there is no honest way around it.
   The dashboard's worker table is four columns and does not: at 320px it was
   560px inside a 294px card, so 314px of it sat off-screen behind a scroll
   nobody discovers, taking the hashrate and last-share figures with it.

   The status pill was the largest avoidable cost — 76px of the 560, of which
   the word "Active" is 60. It becomes its dot; `statusBadge` puts the state on
   `aria-label` and `title` so nothing but the pixels is lost.

   `table-layout: fixed` is what lets the three figure columns state their own
   widths and hand the remainder to the worker name. Measured at 13px: the
   widest realistic hashrate is ~78px and the widest relative time ~52px, so
   6rem and 4.5rem hold both with their padding. At 320px that leaves 102px for
   the name — enough for the short form, with an ellipsis behind it.
   -------------------------------------------------------------------------- */

@media (max-width: 520px) {
  .table--compact {
    min-width: 0;
    width: 100%;
    table-layout: fixed;
  }

  .table--compact th,
  .table--compact td {
    padding: var(--sp-2) 6px;
  }

  /* No left padding on the first column or right on the last: the card's own
     padding already provides it, and 12px of it is a worker name. */
  .table--compact th:first-child,
  .table--compact td:first-child {
    padding-left: 0;
  }

  .table--compact th:last-child,
  .table--compact td:last-child {
    padding-right: 0;
  }

  /* Worker: whatever the other three leave. */
  .table--compact td:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .table--compact .worker-name,
  .table--compact .worker-name__label {
    display: block;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Status: the dot and its cell padding, nothing more. */
  .table--compact th:nth-child(2),
  .table--compact td:nth-child(2) {
    width: 1.5rem;
  }

  .table--compact .badge {
    padding: 0;
    background: none;
    border-color: transparent;
    /* The dot inherits `currentColor`, so the variant colours still separate
       active from idle from offline. */
  }

  .table--compact .badge__label {
    display: none;
  }

  /* Hashrate and last share: right-aligned, one line, tabular. */
  .table--compact th:nth-child(3),
  .table--compact td:nth-child(3) {
    width: 6rem;
  }

  /* Last share is usually "3m ago", but past 30 days `formatRelativeTime`
     switches to a date — "Jul 31, 2026" is ~80px at 13px, and that is the
     figure this column has to hold, not the short form. */
  .table--compact th:nth-child(4),
  .table--compact td:nth-child(4) {
    width: 5.25rem;
  }

  /* "Hashrate (5m)" does not fit 6rem on one line. Wrapped rather than
     ellipsised — a clipped header reads as a bug, and the row below is what
     the width is being spent on. */
  .table--compact thead th {
    white-space: normal;
    line-height: 1.25;
  }

  /* "Status" does not fit a 24px column at any line count. Zeroed rather than
     removed: the header is still in the accessibility tree naming the column
     for the dots beneath it. */
  .table--compact thead th:nth-child(2) {
    font-size: 0;
  }
}

/* --- The narrowest phones ------------------------------------------------
   Below 360px the card has 246px of content. Four columns need 67px for the
   longest worker name, 19px for the dot, 78px for the widest hashrate and
   92px for a last-share date — 256px, ten short. Something has to give, and
   the name is the one thing that must not: at 42px every row read "AX…".

   So the fourth column folds under the first. The figure moves; it does not
   disappear, and it keeps its own absolute-time tooltip. Above 360px there is
   room for all four and the table stays a table.
   ------------------------------------------------------------------------- */

@media (max-width: 359px) {
  .table--compact th:nth-child(4),
  .table--compact td:nth-child(4) {
    display: none;
  }

  .table--compact th:nth-child(3),
  .table--compact td:nth-child(3) {
    width: 5.25rem;
    padding-right: 0;
  }

  .table--compact .worker-name {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
  }

  .table--compact .worker-name__last {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
  }
}

/* --------------------------------------------------------------------------
   Activity feed
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Bitcoin news

   Occupies the grid slot the activity feed used to. The row scale below is a
   height budget, not decoration: the row's height is set by the Block History
   chart beside it, so four stories with two-line headlines have to come in
   under ~68px each — dividers included — at the 316px the tile narrows to.
   -------------------------------------------------------------------------- */

/* At 316px the subtitle wraps to two lines; the standard head margin would
   spend the space the fourth story needs. Scoped so no other card moves. */
.card--news .card__head {
  margin-bottom: var(--sp-2);
}

/* At 316px this subtitle wraps to two lines. Tightening its leading here —
   and nowhere else — is what pays for the breathing room between stories. */
.card--news .card__subtitle {
  line-height: 1.35;
}

.news__viewall {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  /* Generous hit area pulled back by an equal negative margin, so the target
     grows without the label moving off the head's right edge. */
  padding: 7px 10px;
  margin: -7px -10px 0 0;
  border-radius: var(--radius-sm);
  background: none;
  border: 0;
  /* Sentence case and unspaced: uppercase + tracking made this read louder
     than the card title it sits beside. It is an affordance, not a heading. */
  font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  cursor: pointer;
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.news__viewall svg {
  width: 11px;
  height: 11px;
}

.news__viewall:hover,
.news__viewall:focus-visible {
  color: var(--brand-hover);
  background: var(--glass-highlight);
}

.news {
  display: flex;
  flex-direction: column;
  /* Takes the height the card has spare without ever forcing more. */
  flex: 1 1 auto;
  min-height: 0;
  /* Rows bleed past the text so a hovered row reads as a full-width band. */
  margin-inline: calc(var(--sp-2) * -1);
}

.news__item {
  --news-accent: var(--text-secondary);

  display: grid;
  /* A fixed first column, not `auto`: every category label, headline and
     metadata row then starts on the same x, so the eye scans straight down. */
  grid-template-columns: 30px minmax(0, 1fr) 14px;
  align-items: center;
  column-gap: var(--sp-3);
  padding: 6px var(--sp-2);
  border-radius: var(--radius-sm);
  color: inherit;
  text-decoration: none;
  transition:
    background-color 170ms var(--ease-out),
    border-color 170ms var(--ease-out);
}

.news__item[data-accent="mining"]     { --news-accent: var(--news-mining); }
.news__item[data-accent="market"]     { --news-accent: var(--news-market); }
.news__item[data-accent="regulation"] { --news-accent: var(--news-regulation); }
.news__item[data-accent="tech"]       { --news-accent: var(--news-tech); }
.news__item[data-accent="security"]   { --news-accent: var(--news-security); }
.news__item[data-accent="bitcoin"]    { --news-accent: var(--news-bitcoin); }

/* Equal-share rows — but only in the two-column dashboard layout, which is the
   one place this card has a definite height to share out. The Block History
   chart beside it is what sets the row height there.

   Below 1101px `.grid--split` collapses to a single column, nothing is beside
   the card, and the grid row sizes to its content. `flex-basis: 0` with
   `min-height: 0` means the items then contribute *nothing* to the container's
   intrinsic height: the list shrank to four lots of padding — 51px total, rows
   of 12-13px against ~57px of content — so each row painted 44px over the next
   one and the last two spilled out through the bottom of the card.

   The same rule applied unconditionally had already broken the full news page,
   where the list is likewise free to be as tall as its content. Both failures
   are one mistake, so the rule is stated once: equal-share needs a container
   whose height is already decided. */
@media (min-width: 1101px) {
  .news--balanced .news__item {
    flex: 1 1 0;
    min-height: 0;
  }
}

/* Subtle rules between stories, never above the first or below the last. */
.news__item + .news__item {
  border-top: 1px solid rgba(255, 255, 255, 0.035);
}

/* A lift, not a recess: --surface-inset is a dark overlay and would sink the
   row on this already-dark card. */
.news__item:hover,
.news__item:focus-visible {
  background: var(--glass-bg-strong);
}

/* The rounded-square category plate. The tint is derived from the accent so
   one custom property drives glyph, label and plate together; the flat
   fallback is what a browser without color-mix() gets. */
.news__plate {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 8px;
  /* line-height: 0 keeps the glyph off the text baseline, so it sits on the
     plate's true centre rather than a few tenths low. */
  line-height: 0;
  background: var(--glass-bg-strong);
  background: color-mix(in srgb, var(--news-accent) 12%, transparent);
  color: var(--news-accent);
  transition: background-color 170ms var(--ease-out);
}

.news__plate svg {
  width: 16px;
  height: 16px;
}

.news__item:hover .news__plate {
  background: color-mix(in srgb, var(--news-accent) 22%, transparent);
}

.news__body {
  display: block;
  min-width: 0;
}

.news__category {
  display: block;
  font-size: 9px;
  font-weight: var(--fw-semibold);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--news-accent);
}

.news__headline {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin-top: 1px;
  font-size: 12px;
  font-weight: var(--fw-semibold);
  line-height: 1.4;
  letter-spacing: var(--tracking-heading);
  color: var(--text-primary);
  transition: color 170ms var(--ease-out);
}

.news__item:hover .news__headline {
  color: #fff;
}

.news__meta {
  display: flex;
  align-items: baseline;
  gap: 5px;
  margin-top: 1px;
  font-size: 9.5px;
  line-height: 1.15;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
}

.news__source {
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
}

.news__dot {
  color: var(--text-muted);
  opacity: 0.55;
}

/* Same muted tier as the source: together they are one secondary line under
   the headline, not two competing figures. */
.news__time {
  color: inherit;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

/* Present but recessive until the row is hovered. */
.news__open {
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  margin-top: 1px;
  flex-shrink: 0;
  color: var(--brand-ink);
  opacity: 0;
  transition:
    opacity 170ms var(--ease-out),
    color 170ms var(--ease-out);
}

.news__open svg {
  width: 12px;
  height: 12px;
}

.news__item:hover .news__open,
.news__item:focus-visible .news__open {
  opacity: 1;
}


/* --------------------------------------------------------------------------
   The news tile in the single-column layout

   Everything above is written against a height budget: at 316px wide the card
   has to fit four two-line stories into whatever the chart beside it leaves,
   which is what pays for the 9px category and the 6px row padding. In one
   column that budget does not exist — the card grows to its content — so the
   tile spends the freed space on legibility instead of borrowing it back.
   -------------------------------------------------------------------------- */

@media (max-width: 1100px) {
  .news__item {
    /* Stated rather than left to the initial value, so no future `flex` on
       the list can start shrinking rows below their content again. */
    flex: 0 0 auto;
    /* 8px top and bottom, so consecutive story blocks are 16px apart. */
    padding: 8px var(--sp-2);
    /* The plate belongs beside the category label, not halfway down a
       two-line headline. */
    align-items: start;
  }

  /* Optical: the plate's 30px box sits a touch high against 10px uppercase. */
  .news__plate {
    margin-top: 1px;
  }

  .news__category {
    font-size: 10px;
  }

  .news__headline {
    margin-top: 3px;
    font-size: 13.5px;
  }

  .news__meta {
    margin-top: 3px;
    font-size: 11px;
  }

  /* The head was tightened to buy the fourth story room it no longer has to
     borrow. */
  .card--news .card__head {
    margin-bottom: var(--sp-4);
  }
}

/* Where there is no pointer to hover with, a hover-only affordance is one the
   reader never sees. The icon keeps its own grid column either way, so showing
   it takes nothing from the headline. */
@media (hover: none) {
  .news__open {
    opacity: 0.4;
  }
}

/* Centred in the space the rows would have filled — quiet, not broken. */
.news__empty {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  min-height: 0;
  text-align: center;
  color: var(--text-muted);
}

.news__empty-icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--glass-bg-strong);
  border: 1px solid var(--glass-border);
  color: var(--text-muted);
}

.news__empty-icon svg {
  width: 20px;
  height: 20px;
}

.news__empty-text {
  font-size: var(--fs-sm);
  max-width: 30ch;
}

.feed {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  /* Bleeds slightly past the text so a hovered row reads as a full-width
     band rather than a floating rectangle. */
  margin-inline: calc(var(--sp-2) * -1);
  /* Grows into whatever height the row gives its card, rather than stopping
     at a fixed cap and leaving a gap beneath itself. It does not *force*
     height — an empty feed stays as compact as its empty state. */
  flex: 1 1 auto;
  min-height: 0;
  max-height: 400px;
  overflow-y: auto;
}

.feed__item {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-2);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-fast) var(--ease-out);
}

.feed__item:hover {
  background: var(--glass-bg);
}

.feed__icon {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  margin-top: 1px;
  border-radius: var(--radius-sm);
  background: var(--glass-bg-strong);
  color: var(--text-secondary);
}

.feed__icon svg {
  width: var(--icon-sm);
  height: var(--icon-sm);
}

.feed__icon--good {
  background: var(--status-good-dim);
  color: var(--status-good);
}

.feed__icon--brand {
  background: var(--brand-dim);
  color: var(--brand-ink);
}

.feed__icon--critical {
  background: var(--status-critical-dim);
  color: var(--status-critical);
}

.feed__body {
  min-width: 0;
  flex: 1;
}

.feed__text {
  font-size: var(--fs-sm);
  color: var(--text-primary);
}

.feed__meta {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

/* New rows slide in rather than pop. */
.feed__item--enter {
  animation: feed-in var(--dur-slow) var(--ease-out);
}

@keyframes feed-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Legend (identity is never color-alone — swatch + label)
   -------------------------------------------------------------------------- */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}

.legend__item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--text-secondary);
}

.legend__swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   Loading & empty states
   -------------------------------------------------------------------------- */

.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.05) 25%,
    rgba(255, 255, 255, 0.10) 37%,
    rgba(255, 255, 255, 0.05) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  color: transparent !important;
}

@keyframes shimmer {
  from {
    background-position: 100% 50%;
  }
  to {
    background-position: 0 50%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
}

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-10) var(--sp-5);
  text-align: center;
  color: var(--text-muted);
}

/* The glyph sits on its own plate so an empty panel reads as deliberate
   rather than as something that failed to load. */
.empty > div:first-child:not([class]) {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--sp-1);
  border-radius: var(--radius-lg);
  background: var(--glass-bg-strong);
  border: 1px solid var(--glass-border);
  color: var(--text-secondary);
}

.empty svg {
  width: var(--icon-xl);
  height: var(--icon-xl);
  opacity: 0.85;
}

.empty__title {
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
}

.empty__text {
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
  max-width: 46ch;
}

/* An empty state standing in for a table row must not inherit the cell
   padding on top of its own. */
.table td > .empty {
  padding-block: var(--sp-8);
}

/* --------------------------------------------------------------------------
   Toasts
   -------------------------------------------------------------------------- */

.toasts {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-width: min(92vw, 380px);
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-md);
  background: var(--surface-overlay);
  border: 1px solid var(--glass-border);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm);
  animation: toast-in var(--dur-base) var(--ease-out);
}

.toast[data-leaving="true"] {
  animation: toast-out var(--dur-base) var(--ease-out) forwards;
}

.toast__icon {
  flex-shrink: 0;
  margin-top: 2px;
}

.toast__icon svg {
  width: var(--icon-md);
  height: var(--icon-md);
}

.toast--good .toast__icon {
  color: var(--status-good);
}

.toast--warning .toast__icon {
  color: var(--status-warning);
}

.toast--critical .toast__icon {
  color: var(--status-critical);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.98);
  }
}

@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* --------------------------------------------------------------------------
   Data-source banner
   -------------------------------------------------------------------------- */

.banner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-md);
  font-size: var(--fs-sm);
  background: var(--status-warning-dim);
  border: 1px solid rgba(201, 133, 0, 0.3);
  color: var(--text-secondary);
}

.banner svg {
  width: var(--icon-md);
  height: var(--icon-md);
  flex-shrink: 0;
  color: var(--status-warning);
}

.banner__body {
  flex: 1;
  min-width: 0;
}

.banner strong {
  color: var(--text-primary);
}
