/* 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, 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;
}
.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 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: 44px;
  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; }
.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 { opacity: 0.86; }

.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); }

.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }

/* 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%;
  min-height: 44px;
  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);
}
.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 row that holds
   label and toggle is markup, and stays per stack. */

.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 survive distance: on a
     phone the help is a bottom sheet at the other end of the screen, and
     the filled mark is what ties sheet and toggle together. */
  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);
}

/* 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;
}

.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;
}

.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); }

/* ══ 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); }
.filter [data-filter-count] { 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 four 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); }

/* 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;
}

/* ── 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-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>');
}
