/* earl.ee — components
   The THIRD canonical file, beside tokens.css and platform-bar.css.

   One version, in the versioned bundle; every surface fetches it at build
   time — no copies, no sync script, no hash check. Nothing in here may be
   redefined locally: a surface that needs a
   different badge does not have a different badge, it has a bug.

   Requires tokens.css. Contains no raw values that are not geometry —
   every colour, radius, duration and type composite is a token.
   Contains no address: the four utility glyphs are data: URIs, so this file
   may be inlined into a <head> (DESIGN-SYSTEM.md, "Inlining").

   WHAT IS IN HERE
     the four status-dot forms · the one badge · cards · tables · the five
     buttons in three sizes · inputs, select, checkbox, radio, choice, toggle ·
     notices · the 28px quiet icon button of the data-surface controls, with its
     glyphs and its confirm state · the working states (spinner, button
     ring, rail, placeholder) · the on-demand help toggle · the copyable
     mono value and the inline code chip.
   WHAT IS NOT, AND WHY — see DESIGN-SYSTEM.md § "The four canonical files".
     Markup. Page layout. Anything that exists once on the platform
     (transport rail, filter field, the dock). Anything --ip-*.
     Behaviour: this file defines the states, the stack toggles them — the
     canonical behaviour of the data-surface contract (clipboard, TSV, sort,
     filter) lives in data-surface.js, the fourth canonical file.

   ─── GLYPHS ────────────────────────────────────────────────────────────
   Glyphs here are CANONICAL, and they are CSS, not assets and not ten
   copies of a path. Each one is a mask-image on a ::before; the geometry
   lives once, in this file, and the colour comes from currentColor, so a
   glyph inherits every hover and disabled state for free.

   Nothing is fetched, nothing is an <img>, nothing lands in an asset
   pipeline — "no image assets anywhere" holds. Inline SVG in a template is
   the fallback for the one case a mask cannot serve (a glyph that needs two
   colours, or its own accessible name); there it is reference, not canon.
   ──────────────────────────────────────────────────────────────────────── */

/* ══ status dot — the shape is the channel, the colour only confirms it ══
   Four forms, never five, and never a fifth colour on an existing form.
   Always beside a word (Rule 13); always carrying a title with the state. */

.dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  box-sizing: border-box;
}
.dot-ok     { background: var(--good); }                          /* filled disc */
.dot-paused { border: 1.5px solid var(--ink-50); }                /* hollow ring */
.dot-na {                                  /* dashed ring — as four ticks. */
  background:                              /* border:dashed collapses at   */
    repeating-conic-gradient(from -34deg,  /* 8px into 2-3 UA-dependent    */
      var(--ink-30) 0 68deg,               /* strokes; a conic tick ring   */
      transparent 68deg 90deg);            /* is deterministic: 4 ticks,   */
  -webkit-mask: radial-gradient(circle,    /* 22° gaps, the same in every  */
      transparent 2.15px, #000 2.35px);    /* browser.                     */
  mask: radial-gradient(circle,
      transparent 2.15px, #000 2.35px);
}
.dot-fault {                                                      /* apex DOWN — */
  width: 0;                                                       /* a fall, not */
  height: 0;                                                      /* a warning   */
  border-radius: 0;
  border-left: 4.5px solid transparent;
  border-right: 4.5px solid transparent;
  border-top: 8px solid var(--fault);
  transform: translateY(-0.5px);   /* optical centring against x-height */
}

/* ══ badge — one shape in the whole platform ══
   Solid ground, --paper text. At 12px mono a tint has too little substance
   to register. --*-wash grounds belong to notices, never here. */

.badge {
  display: inline-flex;
  align-items: center;
  border-radius: var(--radius-pill);
  padding: 6px 12px;   /* the one sub-ladder value: a pill's optical centring */
  border: 1px solid transparent;
  font: var(--text-meta);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--paper);
  background: var(--ink);
}
.badge-good   { background: var(--good); }
.badge-fault  { background: var(--fault); }
.badge-signal { background: var(--signal); }
.badge-accent { background: var(--accent); color: var(--on-accent); }

/* neutral / draft — the one exception to the solid ground */
.badge-neutral {
  background: var(--vellum);
  color: var(--ink-50);
  border-color: var(--line);
}

/* environments. live is deliberately unmarked — it gets no badge at all;
   .badge-live exists only for the rare row that must name every state. */
.badge-preview  { background: var(--env-preview); }
.badge-build    { background: var(--env-build); }
.badge-platform { background: var(--env-platform); }
.badge-live     { background: var(--env-live); color: var(--paper); }

/* ══ card ══ borders, never shadows. */

.card {
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
a.card,
.card-interactive { transition: border-color var(--dur-state) var(--ease); }
a.card:hover,
.card-interactive:hover { border-color: var(--accent); }

/* ══ table ══ */

.data-table {
  width: 100%;
  border-collapse: collapse;
  font: var(--text-small);
}
.data-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--vellum);
  border-bottom: 1px solid var(--line-strong);
  padding: var(--space-2) var(--space-lg);
  font: var(--text-meta);
  font-weight: 500;
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--ink);
  text-align: left;
  white-space: nowrap;
}
/* the affordance is on data-sort, which the markup carries permanently — not on
   aria-sort, which data-surface.js moves to the one active head. Keyed to the
   latter, no head looks sortable until the first click, and exactly one after. */
.data-table th[data-sort] {
  cursor: pointer;
  user-select: none;
  padding-right: calc(var(--space-lg) + 14px);
  position: relative;
}
/* the caret: reserved space above, so nothing shifts when it appears. Inactive
   heads show it at rest only on hover, in the muted ink — an invitation, not a
   claim about the current order. */
.data-table th[data-sort]::after {
  content: "↑";
  position: absolute;
  right: var(--space-md);
  top: 50%;
  transform: translateY(-50%);
  font-size: 11px;
  line-height: 1;
  color: var(--ink-50);
  opacity: 0;
  transition: opacity var(--dur-state) var(--ease);
}
.data-table th[data-sort]:hover::after,
.data-table th[data-sort]:focus-visible::after { opacity: 1; }
.data-table th[aria-sort]::after {
  opacity: 1;
  color: var(--accent-deep);
}
.data-table th[aria-sort="descending"]::after { content: "↓"; }
@media (hover: none) {
  .data-table th[data-sort]::after { opacity: 1; }
  .data-table th[data-sort]:not([aria-sort])::after { opacity: 0.45; }
}
.data-table td {
  border-bottom: 1px solid var(--line);
  padding: var(--space-md) var(--space-lg);
  color: var(--ink-70);
  vertical-align: middle;
}
.data-table td.num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
/* a date or a time is one value: "24. Sep." broken over two lines reads as two */
.data-table td.date { white-space: nowrap; }
.data-table tbody tr { transition: background-color var(--dur-state) var(--ease); }
.data-table tbody tr:hover { background: var(--vellum); }
.data-table tbody tr.is-selected {
  background: var(--vellum);
  box-shadow: inset 2px 0 0 0 var(--accent);
}
/* A table inside a card is still a data surface, in the narrow form: the card
   already is a ground, so the head drops its vellum and keeps the line; heads
   may wrap between words and stand on the line; the columns sit 16px apart and
   the outer cells meet the card's own text edge. Row height stays. Measured on
   a 26rem card in de/en/fr: three columns with a dated year fit, with 24px apart
   French did not. */
.card .data-table th {
  background: transparent;
  padding: var(--space-2) var(--space-sm);
  white-space: normal;
  vertical-align: bottom;
}
.card .data-table th[data-sort] { padding-right: calc(var(--space-sm) + 14px); }
.card .data-table th[data-sort]::after { right: 0; top: auto; bottom: var(--space-2); transform: none; }
.card .data-table td { padding: var(--space-md) var(--space-sm); }
.card .data-table :is(th, td):first-child { padding-left: 0; }
.card .data-table th:last-child:not([data-sort]),
.card .data-table td:last-child { padding-right: 0; }
/* a badge beside a value goes on its own line under it */
.card .data-table td .badge { display: flex; width: max-content; margin-top: var(--space-xs); }

/* A second sticky layer — a filter field, a section header, a table head — sits
   UNDER the platform bar and offsets by the height the bar publishes. Never a
   local 44px: that copy breaks the day the bar's height changes, and three
   surfaces have already scrolled their filter behind the bar. The fallback
   covers a surface rendered without the bar (an email preview, a print view).
   --pb-z is never reached for: the bar is the top layer of a page, always. */
.sticky-under-bar {
  position: sticky;
  top: var(--pb-height, 0px);
  z-index: 10;
  background: var(--paper);
}

/* A section of a multi-class page is bounded in HEIGHT, never in rows: the
   whole list is present and scrollable, so the page survives 400 services
   without withholding any of them. ~12 rows; the sticky header keeps the
   columns readable while scrolling. */
.section-scroll {
  max-height: 30rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.section-scroll thead th {
  position: sticky;
  top: 0;                    /* the scroll region, not the page — the bar is
                                already accounted for by the region's own top */
  z-index: 1;
  background: var(--paper);
}

/* the stat numeral of a tile or a summary row. Never coloured — colour the
   status beside it, or every dashboard shouts at once. */
.stat {
  font: var(--text-stat);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.stat-zero { color: var(--ink-30); }   /* a zero is good news or no news */
/* the one sanctioned coloured numeral: a non-zero count that demands action,
   inside a status matrix where a dot per cell would be noise. The colour is
   the status doing its own job on the numeral — use it only where the column
   already names the status, never on a lone tile. */
.stat-alert { color: var(--fault); }

/* ══ buttons — five kinds, three sizes, one primary per screen ══ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  height: var(--control-h);
  padding: 0 var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: none;
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-state) var(--ease),
              color var(--dur-state) var(--ease),
              border-color var(--dur-state) var(--ease),
              opacity var(--dur-state) var(--ease);
}
.btn-sm { height: 32px; padding: 0 var(--space-md); font-size: 14px; position: relative; }
/* 32px is below the 44px floor (rule 07): the hit area is grown invisibly,
   as on .icon-btn and .help-toggle — the button looks exactly as before. */
.btn-sm:not([aria-busy="true"])::after {
  content: "";
  position: absolute;
  inset: calc((32px - 44px) / 2) 0;
}
.btn-lg { height: 56px; padding: 0 var(--space-lg); font-size: 18px; }

.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); }

.btn-secondary { background: var(--ink); color: var(--paper); }
.btn-secondary:hover { background: var(--ink-hover); }

.btn-tertiary { border-color: var(--line-strong); color: var(--ink); }
.btn-tertiary:hover { border-color: var(--ink); }

.btn-quiet { color: var(--ink-50); }
.btn-quiet:hover { color: var(--ink); }

.btn-destructive { border-color: var(--fault-wash); color: var(--fault); }
.btn-destructive:hover { border-color: var(--fault); }

/* disabled: an ink step, never an opacity (Rule 02). A disabled button has
   no kind any more — it cannot be acted on, so it need not say how much it
   matters. Every kind settles into one of two rests by its anatomy: a fill
   becomes --vellum, a border becomes --line, the word becomes --ink-30. */
.btn:disabled,
.btn[aria-disabled="true"] { color: var(--ink-30); pointer-events: none; cursor: default; }
.btn-primary:is(:disabled, [aria-disabled="true"]),
.btn-secondary:is(:disabled, [aria-disabled="true"]) { background: var(--vellum); }
.btn-tertiary:is(:disabled, [aria-disabled="true"]),
.btn-destructive:is(:disabled, [aria-disabled="true"]) { border-color: var(--line); }

/* working: the label stays, the width does not change (Rule 09). aria-busy is
   the carrier — the stack sets it, this settles what it looks like. The canon
   knows no framework: a surface driven by HTMX maps its own request class onto
   the attribute in its own sheet (one line) and inherits everything below.

   The busy mark is the rail, run along the button's bottom edge — not a ring
   inside the padding. A ring has to be given room: 14px plus clearance, taken
   from a padding that sm does not have to spare, and every button pays for it
   forever so one of them can be busy for two seconds. The edge is room that
   already exists. Same segment, same duration as .rail; the button's own edge
   is the track. */
.btn[aria-busy="true"] {
  position: relative;
  overflow: hidden;
  pointer-events: none;
}
.btn[aria-busy="true"] > .btn-label { opacity: 0.55; }
.btn[aria-busy="true"]::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 30%;
  height: 2px;
  background: currentColor;
  animation: earl-rail var(--dur-rail) var(--ease) infinite;
}

/* glyph on a labelled button — same masks as .icon-btn, 14px to sit with the
   text. The .btn gap already spaces it; no margin of its own. */
.btn[data-glyph]::before {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  background: currentColor;
  -webkit-mask: var(--glyph) center / 14px 14px no-repeat;
          mask: var(--glyph) center / 14px 14px no-repeat;
}

/* ══ working states — spinner, rail, placeholder ══
   Rule 09: under 300ms nothing · under ~5s spinner · beyond, rail with a step
   line · content not yet arrived, placeholder. Reserved height, never a
   shimmer, never a spinner over stale rows. */

@keyframes earl-spin { to { transform: rotate(360deg); } }
@keyframes earl-rail {
  from { transform: translateX(-100%); }
  to   { transform: translateX(333%); }
}

.spinner {
  display: inline-block;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--line-strong);
  border-top-color: var(--ink-50);            /* the 90° arc */
  animation: earl-spin var(--dur-spin) linear infinite;
}

.rail {
  position: relative;
  overflow: hidden;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--line);
}
.rail::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 30%;
  border-radius: var(--radius-pill);
  background: var(--ink-50);
  animation: earl-rail var(--dur-rail) var(--ease) infinite;
}
/* the step line is not decoration — it is the primary channel (Rule 09). */
.rail-step { font: var(--text-small); color: var(--ink-50); }

.placeholder {
  background: var(--vellum);
  border-radius: var(--radius-input);   /* no shimmer, no pulse, no animation */
}

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
    border-color: var(--line-strong);
    border-top-color: var(--line-strong);
  }
  .btn[aria-busy="true"]::after { animation: none; }
  .rail::after { animation: none; }
}

/* ══ fields ══ */

.input,
.select,
.textarea {
  width: 100%;
  padding: var(--space-2) var(--space-md);
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  color: var(--ink);
  font: var(--text-body);
  transition: border-color var(--dur-state) var(--ease),
              box-shadow var(--dur-state) var(--ease);
}
/* One line of field is one control row: the height is set, the padding is
   only horizontal. Vertical padding plus --text-body made it ~53px, so a
   layout reserving --control-h (e.g. the empty board) jumped. The textarea
   grows with its text and keeps the vertical padding. */
.input,
.select { height: var(--control-h); padding-block: 0; }
.textarea { min-height: var(--control-h); }
.input::placeholder,
.textarea::placeholder { color: var(--ink-30); }
.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}
.input[aria-invalid="true"] { border-color: var(--fault); }
.field-error { color: var(--fault); font: var(--text-small); }
.field-hint  { color: var(--ink-50); font: var(--text-small); }

/* a field that carries a machine value — a hostname, a path, a SHA — is mono
   (Rule 02). 14px, because mono runs optically larger than the sans at 16. */
.input-mono,
.textarea.input-mono { font-family: var(--font-mono); font-size: 14px; }

/* ══ help on demand — the ? beside a label ══
   A <details>: native, script-free, survives every swap. Closed by default;
   always-visible prose is the thing this exists to prevent. The toggle has
   no body of its own: label and toggle share a .help-row, and the .help-text
   is the row's next sibling, so it runs the full width under the row instead
   of starting where the ? stands.
     <div class="help-row"><label>…</label>
       <details class="help-toggle"><summary aria-label="…">?</summary></details></div>
     <p class="help-text">…</p> */

.help-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
}

.help-toggle > summary {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  color: var(--ink-50);
  font: var(--text-meta);
  line-height: 1;
  cursor: pointer;
  list-style: none;
  user-select: none;
  transition: border-color var(--dur-state) var(--ease),
              color var(--dur-state) var(--ease),
              background-color var(--dur-state) var(--ease);
}
.help-toggle > summary::-webkit-details-marker { display: none; }
/* 18px is far below the 44px floor — the target grows, the mark does not. */
.help-toggle > summary::after { content: ""; position: absolute; inset: -13px; }
.help-toggle > summary:hover { border-color: var(--ink); color: var(--ink); }
.help-toggle > summary:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.help-toggle[open] > summary {
  /* solid ink, paper glyph — the open state must read at a distance: the
     filled mark is what ties the text under the row to its toggle. */
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.help-text {
  max-width: 62ch;
  margin-top: var(--space-sm);
  color: var(--ink-70);
  font: var(--text-small);
}
.help-row:not(:has(.help-toggle[open])) + .help-text { display: none; }

/* bottom sheet — the phone's float anchored to the bottom edge. The grip is a
   mark, not the way out: every sheet also carries its close word. */
.sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  display: grid;
  gap: var(--space-2);
  padding: var(--space-md);
  background: var(--raised);
  border-top: 1px solid var(--line-strong);
  border-radius: var(--radius-card) var(--radius-card) 0 0;
  box-shadow: var(--shadow-lift-up);
}
.sheet-grip {
  justify-self: center;
  width: 36px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--line-strong);
}

/* select caret — a CSS triangle, never a character: ▾ falls outside the
   shipped subsets and lands on a system face at an unpredictable width. */
.select-wrap { position: relative; display: block; }
.select { appearance: none; padding-right: var(--space-lg); }
.select-wrap::after {
  content: "";
  position: absolute;
  right: var(--space-md);
  top: 50%;
  width: 0;
  height: 0;
  margin-top: -2px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--ink-50);
  pointer-events: none;
}
/* The small select: the footer's language picker. Quiet like the links beside
   it — no border, the links' type and colour, the caret in --ink-30. The
   control keeps the 44px row (rule 07); only the drawing is small. */
.select-wrap:has(> .select-sm) { display: inline-block; }
.select.select-sm {
  width: auto;
  padding: 0 var(--space-md) 0 0;
  background: none;
  border-color: transparent;
  color: var(--ink-50);
  font: var(--text-small);
  line-height: 1;
}
.select.select-sm:hover { color: var(--ink); }
.select-wrap:has(> .select-sm)::after {
  right: 0;
  border-top-width: 4px;
  border-top-color: var(--ink-30);
}

.checkbox {
  appearance: none;
  flex: none;
  width: 19px;
  height: 19px;
  margin: 0;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--paper);
  cursor: pointer;
  transition: background-color var(--dur-state) var(--ease),
              border-color var(--dur-state) var(--ease);
}
.checkbox:checked {
  background: var(--ink);
  border-color: var(--ink);
}
.checkbox:checked::before {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  background: var(--accent-night);
  -webkit-mask: var(--glyph-check) center / 13px 13px no-repeat;
          mask: var(--glyph-check) center / 13px 13px no-repeat;
}

/* radio — the checkbox's twin, round. Checked: an ink ring around an
   --accent-night point, drawn with an inset shadow, no pseudo-element.
   Disabled keeps the value readable and drops the pointer. */
.radio {
  appearance: none;
  flex: none;
  width: 19px;
  height: 19px;
  margin: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--paper);
  cursor: pointer;
  transition: background-color var(--dur-state) var(--ease),
              border-color var(--dur-state) var(--ease);
}
.radio:checked {
  background: var(--accent-night);
  border-color: var(--ink);
  box-shadow: inset 0 0 0 5px var(--ink);
}
.radio:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.radio:checked:focus-visible { box-shadow: inset 0 0 0 5px var(--ink), var(--focus-ring); }
.radio:disabled { border-color: var(--line); cursor: default; }
.radio:checked:disabled {
  border-color: var(--ink-50);
  box-shadow: inset 0 0 0 5px var(--ink-50);
}

/* ══ choice — a few values whose consequences are compared ══
   <fieldset class="choice"><legend>label + optional help toggle</legend>
   then one <label class="choice-option"> per value: .radio, the short word,
   and an optional .choice-help sentence. The help toggle in the legend has
   no body of its own: opened, every option shows its sentence under its
   word. Script-free, like the toggle itself. */
.choice {
  display: grid;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.choice > legend {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  padding: 0;
}
.choice-option {
  display: grid;
  grid-template-columns: 19px minmax(0, 1fr);
  column-gap: var(--space-2);
  align-items: start;
  min-height: 44px;
  padding-block: var(--space-sm);
  box-sizing: border-box;
  cursor: pointer;
}
.choice-option > .radio { margin-top: 4px; } /* centred on the first line of --text-body */
.choice-option:has(> .radio:disabled) { cursor: default; }
.choice-help {
  grid-column: 2;
  display: none;
  max-width: 62ch;
  color: var(--ink-70);
  font: var(--text-small);
}
.choice:has(.help-toggle[open]) .choice-help { display: block; }

.toggle {
  appearance: none;
  flex: none;
  position: relative;
  width: 38px;
  height: 22px;
  margin: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--line-strong);
  cursor: pointer;
  transition: background-color var(--dur-state) var(--ease);
}
.toggle::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-pill);
  background: #FFFFFF;
  transition: transform var(--dur-state) var(--ease);
}
.toggle:checked { background: var(--accent); }
.toggle:checked::after { transform: translateX(16px); }
.toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.toggle:disabled { background: var(--line); cursor: default; }
.toggle:checked:disabled { background: var(--ink-50); }

/* ══ notice ══ full-strength border, wash ground, never a pastel border. */

.notice {
  border-radius: var(--radius-input);
  border: 1px solid var(--ink-30);
  background: var(--vellum);
  padding: var(--space-md) var(--space-4);
  color: var(--ink-70);
  font: var(--text-small);
}
.notice > strong,
.notice-lead { display: block; color: var(--ink); font-weight: 600; }
.notice-signal { border-color: var(--signal); background: var(--signal-wash); }
.notice-good   { border-color: var(--good);   background: var(--good-wash); }
.notice-fault  { border-color: var(--fault);  background: var(--fault-wash); }

/* ══ avatar ══ a label, never a portrait. */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-pill);
  background: var(--vellum);
  border: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-50);
  text-transform: uppercase;
  transition: border-color var(--dur-state) var(--ease),
              color var(--dur-state) var(--ease);
}
a.avatar:hover,
button.avatar:hover { border-color: var(--ink); color: var(--ink); }

/* the filter container data-surface.js presumes (input.closest(".filter")):
   input left, live count right, clear button inside the field's right edge.
   Owned here so fifteen surfaces stop rebuilding its border and focus ring. */
.filter {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  height: 36px;
  padding: 0 var(--space-sm) 0 var(--space-2);
  background: var(--raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  transition: border-color var(--dur-state) var(--ease);
}
.filter:focus-within {
  border-color: var(--signal);
  box-shadow: var(--focus-ring);
}
.filter input {
  flex: 1;
  min-width: 0;
  appearance: none; /* Safari draws input[type="search"] as its own widget otherwise;
                       background/border resets alone don't reach it */
  /* mono at 16px per the filter-field spec — the corpus is service names, SHAs,
     domains and paths, and the field should look like what goes into it */
  font: 400 16px/1.5 var(--font-mono);
  color: var(--ink);
  background: none;
  border: none;
  outline: none;
}
/* Safari's built-in ✕ and inset decoration — ours is the .icon-btn beside the count */
.filter input::-webkit-search-cancel-button,
.filter input::-webkit-search-decoration { -webkit-appearance: none; display: none; }
.filter input::placeholder { color: var(--ink-30); }
/* The readout carries a bare [data-filter-count]; the field carries
   [data-filter-count="#id"] to name it (markup contract). The bare attribute
   selector matched both, and the field lost its 16px for --text-meta
   (board 0.41.3, 2026-10-17). The field is excluded here. */
.filter [data-filter-count]:not(input) { font: var(--text-meta); color: var(--ink-50); white-space: nowrap; }

/* the head of a data region — the row the document calls normative, now with a
   rule to its name: title left, tool group right, one baseline. */
.region-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-2);
}
.region-head .region-title { font: var(--text-label); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--ink-70); }
.region-head .region-tools { display: flex; align-items: center; gap: var(--space-xs); }

/* ══ data-surface controls — the copy/download twin ══
   Top-right of every data region, same corner every time, in this order:
   copy, download, close. */

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: var(--radius-control);
  background: none;
  color: var(--ink-30);
  cursor: pointer;
  opacity: 0.6;
  transition: opacity var(--dur-state) var(--ease),
              background-color var(--dur-state) var(--ease),
              color var(--dur-state) var(--ease);
}
.icon-btn:hover { background: var(--vellum); color: var(--ink); opacity: 1; }
.icon-btn:focus-visible { opacity: 1; }

/* 28px is below the 44px floor, so the hit area is grown invisibly rather
   than the button — the control stays quiet, the target does not. */
.icon-btn::after {
  content: "";
  position: absolute;
  inset: -8px;
}
.icon-btn { position: relative; }

/* visible at rest, full strength on region hover, and always full on touch:
   a control that appears on hover does not exist on a tablet. */
.data-region:hover .icon-btn,
.data-region:focus-within .icon-btn { opacity: 1; }
@media (hover: none) {
  .icon-btn { opacity: 1; }
}

.icon-btn::before {
  content: "";
  width: 16px;
  height: 16px;
  background: currentColor;
  -webkit-mask: var(--glyph) center / 16px 16px no-repeat;
          mask: var(--glyph) center / 16px 16px no-repeat;
}
/* the mapping is on the attribute, not on .icon-btn, so a labelled .btn picks
   up the same geometry from the same names. */
[data-glyph="copy"]     { --glyph: var(--glyph-copy); }
[data-glyph="download"] { --glyph: var(--glyph-download); }
[data-glyph="close"]    { --glyph: var(--glyph-close); }
[data-glyph="clear"]    { --glyph: var(--glyph-close); }
[data-glyph="more"]     { --glyph: var(--glyph-more); }
[data-glyph="expand"]   { --glyph: var(--glyph-expand); }
[data-glyph="collapse"] { --glyph: var(--glyph-collapse); }

/* a toggle that holds a state — the full view of an area (my, Full view) — is
   the same 28px button, pressed: solid ink, paper glyph, full strength, the
   same truth as .help-toggle[open]. The surface sets aria-pressed and swaps
   data-glyph (expand → collapse) and the accessible name; it never paints. */
.icon-btn[aria-pressed="true"],
.icon-btn[aria-pressed="true"]:hover {
  background: var(--ink);
  color: var(--paper);
  opacity: 1;
}

/* confirmation happens in place for 1.2s: no toast, no layout shift. The
   stack sets the attribute and clears it; the appearance is settled here.
   Two channels, because one was not enough to notice in passing: the glyph
   becomes the check, and the button takes a --good-wash ground. */
.icon-btn[data-confirmed="true"] {
  color: var(--good);
  background: var(--good-wash);
  opacity: 1;
  --glyph: var(--glyph-check);
}
/* the confirm ground outranks hover — the pointer is usually still on it. */
.icon-btn[data-confirmed="true"]:hover {
  background: var(--good-wash);
  color: var(--good);
}
/* the mirror of the confirm: the copy or download threw. Same two channels,
   same 1.2s in-place window, the fault colours — and the value stays on the
   screen, so the by-hand route is still open. */
.icon-btn[data-failed="true"] {
  color: var(--fault);
  background: var(--fault-wash);
  opacity: 1;
  --glyph: var(--glyph-close);
}
.icon-btn[data-failed="true"]:hover {
  background: var(--fault-wash);
  color: var(--fault);
}
/* the terminal itself — the hook the night twins below key on. Lines wrap:
   a horizontal scrollbar hides the end of an address. */
.terminal {
  background: var(--ink);
  border-radius: var(--radius-card);
  padding: var(--space-4);
  font: 400 14px/1.85 var(--font-mono);
  color: var(--night-ink);
  position: relative;
}
.terminal pre { margin: 0; font: inherit; white-space: pre-wrap; overflow-wrap: anywhere; }
/* the same control on a night ground — a terminal is a data region too (Rule
   8), and --ink-30 on --ink is invisible. Context, not a class: every .icon-btn
   inside a .terminal takes the night twins, so a surface never tints it by hand. */
.terminal .icon-btn { color: var(--night-muted); }
.terminal .icon-btn:hover { background: var(--night-raised); color: var(--night-ink); }
.terminal .icon-btn[data-confirmed="true"],
.terminal .icon-btn[data-confirmed="true"]:hover { color: var(--night-good); background: var(--night-raised); }
.terminal .icon-btn[data-failed="true"],
.terminal .icon-btn[data-failed="true"]:hover { color: var(--night-fault); background: var(--night-raised); }
.copyable[data-failed="true"] { color: var(--fault); text-decoration-color: transparent; }
.copy-confirm {
  font: var(--text-meta);
  color: var(--good);
  white-space: nowrap;
}

/* a lone mono value is itself a data region: the value IS the control, no
   button beside it. Same confirm mechanic as .icon-btn, set by the stack. */
.copyable {
  font-family: var(--font-mono);
  color: var(--ink-70);
  cursor: pointer;
  transition: color var(--dur-state) var(--ease),
              text-decoration-color var(--dur-state) var(--ease);
  text-decoration: underline dotted transparent;
  text-underline-offset: 3px;
}
.copyable:hover { color: var(--ink); text-decoration-color: var(--line-strong); }
.copyable[data-confirmed="true"] { color: var(--good); text-decoration-color: transparent; }

/* a machine value inside running prose — a domain, a path, a flag, a SHA. */
.code-chip {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--vellum);
  border-radius: var(--radius-control);
  padding: 2px var(--space-xs);
  word-break: break-all;
}

/* a calendar's shortcode (BZ, SYM, DE+) — an identity, not a value in prose.
   Atomic: it never breaks, not at a "+", not at a narrow column. That is the
   whole difference from .code-chip, which must break so a URL cannot escape.
   On a chip in lists, panels and page headers; bare before the time in a
   month cell and on an event block. */
.cal-code {
  display: inline-block;
  flex: none;
  box-sizing: border-box;
  min-width: 34px;
  padding: 4px 8px;
  font: var(--weight-medium) 11px/1 var(--font-mono);
  letter-spacing: 0.08em;
  text-align: center;
  color: var(--ink-50);
  background: var(--vellum);
  border-radius: 4px;
  white-space: nowrap;
  word-break: keep-all;
  overflow-wrap: normal;
}
.cal-code-bare {
  display: inline;
  min-width: 0;
  padding: 0;
  background: none;
  border-radius: 0;
  text-align: start;
}

/* ── the glyph geometry, once, for the whole platform ────────────────────
   Line glyphs at 16px, 1.4 stroke, round caps. Monochrome by construction:
   a mask has one colour, which is the point — it is always currentColor. */
:root {
  --glyph-copy: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="5.75" y="5.75" width="7.5" height="7.5" rx="1.5"/><path d="M10.25 3.4V3A1 1 0 0 0 9.25 2h-6A1 1 0 0 0 2.25 3v6a1 1 0 0 0 1 1h.4"/></svg>');
  --glyph-download: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5v7.6M4.9 7.2 8 10.3l3.1-3.1M2.6 13.4h10.8"/></svg>');
  --glyph-check: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 8.6 6.4 11.8 12.8 4.6"/></svg>');
  --glyph-more: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="black"><circle cx="3" cy="8" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="13" cy="8" r="1.5"/></svg>');
  --glyph-close: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.4" stroke-linecap="round"><path d="M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6"/></svg>');
  /* expand: four corners pushed out to the frame — the page takes the window.
     collapse: the same four corners turned in. One pair, read as a pair. */
  --glyph-expand: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10"/></svg>');
  --glyph-collapse: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5"/></svg>');
}
