/* melange web ui — one stylesheet, no framework, no build step.
 *
 * Colours come from melange-icons/; see theme.md for the extraction and the rules.
 * Two things there are load-bearing and easy to undo by accident:
 *   - --accent is clay, not sun. Sun (--accent-hot) is 3.96:1 against white and cannot
 *     hold text; clay is 5.72:1. Fills-with-labels use --accent, bare marks use --accent-hot.
 *   - --bad is a red from outside the icon. Using the icon's rust here would make a failed
 *     badge and a running badge differ by ~6% lightness.
 *
 * The design language is an *instrument*: melange is mission-control for a living cluster,
 * so monospace is the voice (wordmark, labels, data), the amber is heat and energy, and one
 * ambient animation — the readiness pulse — carries the sense of a system with a heartbeat.
 * Everything else is kept quiet. See the frontend-design brief. */

:root {
  color-scheme: light dark;

  /* the icon, sampled */
  --cream:    #fef5e9;
  --sand:     #d3aa7b;
  --ochre:    #be6d2f;
  --sun:      #d25c1b;
  --clay:     #ac4817;
  --espresso: #3a2d20;
  --ink:      #1c1e1b;

  --bg:         #f8ecdb;
  --panel:      var(--cream);
  --panel-2:    #f0dfc8;
  --line:       #e3cdae;
  --text:       #2a2119;
  --muted:      #7a6a57;
  --accent:     var(--clay);
  --accent-hot: var(--sun);
  --accent-fg:  var(--cream);

  --ok:    #4c7a3f;
  --warn:  #8f6111;
  --bad:   #9c1f16;
  --dead:  #7a5486;
  --idle:  #7a6a57;

  /* espresso, not black: a neutral shadow goes grey-blue over cream */
  --shadow-1: 0 8px 24px rgb(58 45 32 / 0.16);
  --shadow-card: 0 18px 56px rgb(58 45 32 / 0.30);

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Cascadia Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;

  /* --- design language tokens (derived only — no new hues) --- */
  --radius:    10px;
  --radius-lg: 16px;
  /* One height for the form controls that have to line up with each other on a single row.
     Only the CSV import form needs it so far — a `<select>` and a file picker side by side,
     which have no intrinsic height in common. */
  --field-h:   2.5rem;
  --dur:       .5s;
  --dur-fast:  .18s;
  --ease:      cubic-bezier(.22, .61, .36, 1);   /* decisive ease-out */
  --heat:      linear-gradient(90deg, var(--sand), var(--sun) 62%, var(--clay));
  --glow:      0 0 14px rgb(210 92 27 / 0.30);
  --ring:      rgb(210 92 27 / 0.55);            /* the pulse ring's start colour */

  /* --- syntax tokens (#115) ---
     No new hues: every one of these is a colour already in the palette above, picked for
     contrast against --panel-2 (the editor's and the card's background) rather than for
     resemblance to some other editor's theme. Seven distinguishable colours cover eleven
     classes, and the overlap is deliberate — a number and a [type] never sit adjacent in a
     way that needs telling apart, and weight does the rest (see .tok-cmd/.tok-fn).
     Every one of these MUST be redefined in the dark block below; a token defined only
     here renders as light-mode ink on a dark panel. `the_syntax_tokens_are_themed` pins it. */
  --tok-comment: #7a6a57;   /* muted */
  --tok-str:     #4c7a3f;   /* ok */
  --tok-kw:      #9c1f16;   /* bad */
  --tok-var:     #ac4817;   /* clay */
  --tok-cmd:     #2a2119;   /* text, bold — a command name is the primary thing on its line */
  --tok-fn:      #ac4817;   /* clay, bold */
  --tok-num:     #7a5486;   /* dead */
  --tok-type:    #7a5486;   /* dead */
  --tok-param:   #8f6111;   /* warn */
  --tok-op:      #7a6a57;   /* muted */
  --tok-punct:   #7a6a57;   /* muted */
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:         #1a1712;
    --panel:      #24201a;
    --panel-2:    #2e281f;
    --line:       #3e362a;
    --text:       #f5e9d8;
    --muted:      #a5947c;
    /* the accents lift, and the filled button flips to dark-on-bright */
    --accent:     #e8814a;
    --accent-hot: #f2955f;
    --accent-fg:  #24180f;

    --ok:   #86c46b;
    --warn: #d9a441;
    --bad:  #f0705f;
    --dead: #b98fc9;
    --idle: #a5947c;

    --shadow-1: 0 8px 24px rgb(0 0 0 / 0.45);
    --shadow-card: 0 18px 56px rgb(0 0 0 / 0.6);
    --heat:     linear-gradient(90deg, var(--ochre), var(--sun) 55%, #f2955f);
    --glow:     0 0 18px rgb(242 149 95 / 0.45);
    --ring:     rgb(242 149 95 / 0.6);

    /* the same eleven, lifted onto the dark panel — same roles, same palette */
    --tok-comment: #a5947c;
    --tok-str:     #86c46b;
    --tok-kw:      #f0705f;
    --tok-var:     #e8814a;
    --tok-cmd:     #f5e9d8;
    --tok-fn:      #e8814a;
    --tok-num:     #b98fc9;
    --tok-type:    #b98fc9;
    --tok-param:   #d9a441;
    --tok-op:      #a5947c;
    --tok-punct:   #a5947c;
  }
}

* { box-sizing: border-box; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 var(--sans);
}

/* Ambient particle-network background (decorative; see the IIFE at the foot of app.js).
 *
 * `z-index: -1` on a fixed, full-viewport canvas paints it *above* the body's background
 * fill but *below* every in-flow element — so panels and text are never obscured, and it
 * shows through the gaps and margins where the cluster has room to breathe. Ignores the
 * pointer so nothing it draws ever eats a click. Fades in once the script has a first
 * frame ready (`.on`), rather than popping; reduced-motion makes that fade instant along
 * with the rest of the page, and the script itself then holds a single still frame. */
#bg-net {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.2s var(--ease);
}
#bg-net.on { opacity: 0.5; }

.hidden { display: none !important; }
.mono { font-family: var(--mono); }
.r { text-align: right; }
.c { text-align: center; }
.spacer { flex: 1; }
.narrow { max-width: 8rem; }
.grow { flex: 1; }
.search { flex: 1 1 16rem; max-width: 24rem; }

/* The wordmark: monospace, tight, with a shell cursor. melange is a tool you drive from a
   terminal, so it signs itself in the same face your scripts are written in. */
.wordmark {
  font-family: var(--mono);
  font-weight: 600;
  letter-spacing: -0.03em;
  display: inline-flex;
  align-items: baseline;
}
.wordmark .cursor {
  width: 0.55em;
  height: 1.02em;
  margin-left: 0.12em;
  background: var(--accent-hot);
  border-radius: 1px;
  transform: translateY(0.12em);
  animation: blink 1.15s steps(1) infinite;
  box-shadow: var(--glow);
}
@keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* The page's mark is /mark-256.png — the artwork cropped to its own ring with transparent
 * corners — so it needs no clipping and must not be given any. See theme.md. */
.mark { display: block; flex: none; }

/* =========================================================================
   Connect screen
   ========================================================================= */

.connect {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 2rem;
}

.connect-card {
  width: min(32rem, 100%);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 2.25rem;
  box-shadow: var(--shadow-1);
  animation: rise var(--dur) var(--ease) both;
}

/* A soft amber corona behind the mark, so the icon reads as a low sun rather than a sticker. */
.connect-mark {
  position: relative;
  width: 72px;
  height: 72px;
  margin-bottom: 1.1rem;
}
.connect-mark::before {
  content: "";
  position: absolute;
  inset: -30%;
  border-radius: 50%;
  background: radial-gradient(circle, rgb(210 92 27 / 0.28), transparent 68%);
  animation: breathe 5s var(--ease) infinite;
}
.connect-mark .mark { position: relative; }

.connect-card h1.wordmark { margin: 0; font-size: 2rem; }

.sub { margin: 0.4rem 0 1.5rem; color: var(--muted); max-width: 30rem; }

.server-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }

.server-list li { animation: rise var(--dur) var(--ease) both; }
.server-list li:nth-child(2) { animation-delay: 0.05s; }
.server-list li:nth-child(3) { animation-delay: 0.10s; }
.server-list li:nth-child(4) { animation-delay: 0.15s; }
.server-list li:nth-child(n+5) { animation-delay: 0.2s; }

.server-list button {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.server-list button:hover {
  border-color: var(--line);
  border-left-color: var(--accent);
  transform: translateX(2px);
}

/* A status lamp in a fixed left column — green ready, red down, amber not-ready.
   Being a dedicated element (not a ::before on the variable-width text) it holds one
   column, so every row's lamp lines up vertically however the status reads. */
.server-list .lamp {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--idle);
  flex: none;
}
.server-list .lamp.ok       { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
.server-list .lamp.down     { background: var(--bad); }
.server-list .lamp.notready { background: var(--warn); }

.server-list .url { font-family: var(--mono); font-size: 0.85rem; }
.server-list .state {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

/* =========================================================================
   Command bar (the persistent hero: a live instrument panel)
   ========================================================================= */

/* Three zones: brand + readiness pulse (left), nav (centre), session controls (right). The
   centre column is `auto` between two equal `1fr` gutters, so the tabs are centred against
   the *bar* — never nudged off-centre by whatever sits beside them. */
.command {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  padding: 0 1.25rem;
  min-height: 3.5rem;
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 5;
  animation: drop var(--dur) var(--ease) both;
}

/* Left zone. `min-width: 0` lets it shrink rather than shove the centre off. */
.lead {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-width: 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.05rem;
}

/* The pulse ring, emitted by the cluster's leader dot (see `.lead-dot`). Kept as its own
   keyframe because the leader dot loops it continuously. */
@keyframes ring {
  from { box-shadow: 0 0 0 0 var(--ring); }
  to   { box-shadow: 0 0 0 11px rgb(210 92 27 / 0); }
}

/* Right zone, hard against the edge. `min-width: 0` so a long username can't shove the centre. */
/* Right zone: an account chip that opens a dropdown. `position: relative` anchors the menu;
   `justify-self: end` keeps it hard against the bar's right edge. */
.conn { justify-self: end; position: relative; display: flex; align-items: center; min-width: 0; }

.account-trigger {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.28rem 0.5rem 0.28rem 0.35rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--text);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.account-trigger:hover,
.account-trigger[aria-expanded="true"] {
  background: var(--panel-2);
  border-color: var(--line);
}
.avatar {
  display: grid;
  place-items: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-fg);
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  flex: none;
}
.account-name {
  font-family: var(--mono);
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-name:empty { display: none; }
.caret {
  color: var(--muted);
  font-size: 0.7rem;
  transition: transform var(--dur-fast) var(--ease);
}
.account-trigger[aria-expanded="true"] .caret { transform: rotate(180deg); }

/* The dropdown. Anchored below the chip, right-aligned; `.hidden` closes it. */
.account-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 13rem;
  padding: 0.35rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  z-index: 20;
  animation: drop var(--dur-fast) var(--ease) both;
}
.account-head {
  padding: 0.45rem 0.6rem 0.6rem;
  margin-bottom: 0.35rem;
  border-bottom: 1px solid var(--line);
}
.account-who {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--text);
}
.account-who:empty { display: none; }
.account-node {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  margin-top: 0.2rem;
}
.menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.6rem;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.menu-item:hover { background: var(--panel-2); }
.menu-item.danger { color: var(--bad); }
.menu-item.danger:hover { background: color-mix(in srgb, var(--bad) 15%, transparent); }

/* =========================================================================
   Tabs
   ========================================================================= */

/* Lives inside the command bar now, not on its own row. Stretches to the bar's full height
   so the active-tab underline lands on the bar's bottom edge, where a tab underline belongs. */
.tabs {
  display: flex;
  gap: 0.15rem;
  /* The centre grid cell already centres this against the bar; stretch just gives the tabs
     the bar's full height so the active underline lands on its bottom edge. */
  justify-self: center;
  align-self: stretch;
}
.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Vertical padding gives the tab height when it is on its own row (mobile); on desktop it
     stretches to the command bar's full height and the text just centres. */
  padding: 0.5rem 0.9rem;
  background: none;
  border: 0;
  color: var(--muted);
  font: inherit;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease);
}
.tab::after {
  content: "";
  position: absolute;
  /* bottom: 0, not -1px: sitting flush with the bar's bottom keeps the full 2px underline
     visible under `overflow-y: hidden` above, instead of being clipped to 1px. */
  left: 0.6rem; right: 0.6rem; bottom: 0;
  height: 2px;
  background: var(--heat);
  border-radius: 2px 2px 0 0;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--dur-fast) var(--ease);
}
.tab:hover { color: var(--text); }
.tab.on { color: var(--text); font-weight: 650; }
.tab.on::after { transform: scaleX(1); }

main {
  padding: 1.5rem 1.25rem 3rem;
  max-width: 1440px;
  margin: 0 auto;
}

/* Each view fades up as it becomes current. Toggling display restarts this, so it plays on
   every tab switch but never on a background auto-refresh (which does not touch display). */
.view:not(.hidden) { animation: rise var(--dur) var(--ease) both; }

/* =========================================================================
   Controls
   ========================================================================= */

.bar { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; }
.auto {
  color: var(--muted);
  display: flex; align-items: center; gap: 0.4rem;
  font-family: var(--mono); font-size: 0.78rem;
  letter-spacing: 0.04em; text-transform: uppercase;
}

select, input, textarea {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font: inherit;
  padding: 0.45rem 0.6rem;
  transition: border-color var(--dur-fast) var(--ease);
}
select:hover, input:hover, textarea:hover { border-color: var(--muted); }
textarea { font-family: var(--mono); font-size: 0.85rem; resize: vertical; width: 100%; line-height: 1.55; }

button.primary {
  background: var(--accent); color: var(--accent-fg);
  border: 0; border-radius: 8px; padding: 0.5rem 1rem;
  font: inherit; font-weight: 600; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
button.primary:hover { background: var(--accent-hot); box-shadow: var(--glow); transform: translateY(-1px); }
button.primary:active { transform: translateY(0); }
button.primary.big { padding: 0.65rem 1.6rem; align-self: end; }

button.ghost {
  background: var(--panel); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 0.5rem 0.95rem; font: inherit; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
button.ghost:hover:not(:disabled) { border-color: var(--accent); transform: translateY(-1px); }
button.ghost:disabled { opacity: 0.45; cursor: default; }

button.danger {
  background: none; color: var(--bad);
  border: 1px solid var(--bad); border-radius: 8px;
  padding: 0.4rem 0.85rem; font: inherit; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
button.danger:hover { background: var(--bad); color: var(--panel); }

/* =========================================================================
   Tables
   ========================================================================= */

.table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--panel);
  box-shadow: var(--shadow-1);
}
.table-wrap { overflow-x: auto; }

table { width: 100%; border-collapse: collapse; }

th {
  text-align: left;
  font-family: var(--mono);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 600;
  padding: 0.65rem 0.85rem;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  position: sticky;
  top: 0;
}

td {
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr.click { cursor: pointer; transition: background var(--dur-fast) var(--ease); }
tbody tr.click:hover { background: var(--panel-2); }
/* A hairline heat marker slides in on hover — the row you are about to open. */
tbody tr.click td:first-child { position: relative; }
tbody tr.click td:first-child::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px; background: var(--heat);
  transform: scaleY(0); transform-origin: center;
  transition: transform var(--dur-fast) var(--ease);
}
tbody tr.click:hover td:first-child::before { transform: scaleY(1); }

td.id { font-family: var(--mono); font-size: 0.82rem; }
td.dim { color: var(--muted); }

.empty { padding: 2.75rem; text-align: center; color: var(--muted); }

/* =========================================================================
   Badges
   ========================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.12rem 0.55rem;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  border: 1px solid currentColor;
  background: color-mix(in srgb, currentColor 12%, transparent);
}
.badge::before {
  content: "";
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor;
}
/* The "this run sets this parameter" tick in the script card's Parameters table.
   Hidden rather than absent, so a tick appearing does not shift the column. */
.tick {
  color: var(--ok);
  font-weight: 700;
  visibility: hidden;
}
.tick.on { visibility: visible; }

.badge.pending   { color: var(--idle); }
.badge.running   { color: var(--accent); }
.badge.succeeded { color: var(--ok); }
.badge.failed    { color: var(--bad); }
.badge.timed_out { color: var(--warn); }
.badge.cancelled { color: var(--dead); }

.rt {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

.stuck-flag { color: var(--warn); margin-left: 0.4rem; }
.waiting-flag { margin-left: 0.4rem; opacity: 0.75; font-size: 0.85em; }

.more { display: flex; align-items: center; gap: 0.85rem; margin-top: 1.1rem; }
.note { color: var(--muted); font-size: 0.85rem; }

/* =========================================================================
   Cluster — the signature: a field of node cells
   ========================================================================= */

.leader-line {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1.25rem;
  font-family: var(--mono);
  font-size: 0.9rem;
  color: var(--muted);
}
.leader-line strong { color: var(--text); }
/* The leader dot: a living lamp that emits a ring on a slow, steady heartbeat while the
   cluster has a leader, and sits red and still when it does not. This is the one ambient
   animation. */
.lead-dot {
  position: relative;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent-hot); box-shadow: var(--glow);
  flex: none;
}
.lead-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  animation: ring 1.8s var(--ease) infinite;
}
.lead-dot.bad { background: var(--bad); box-shadow: none; }
.lead-dot.bad::after { animation: none; }

.node-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 1rem;
}

/* No entrance animation here: node cells are rebuilt on every 2s refresh, so a reveal
   would re-run and flicker each tick. The meter's width still eases (that transition is a
   change, not an entrance), which is the movement worth keeping. */
.node {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.1rem 1.15rem 1.2rem;
  overflow: hidden;
  box-shadow: var(--shadow-1);
}
/* The leader wears a warm edge and a soft top-glow — Raft's coordinator, marked. */
.node.leader { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
.node.leader::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--heat);
}
.node.draining { opacity: 0.86; }
/* Unreachable: a red edge and a dimmed, desaturated card, so a down node reads as down at a
   glance in the field rather than looking like a healthy idle one. */
.node.down {
  border-color: color-mix(in srgb, var(--bad) 60%, var(--line));
  opacity: 0.72;
  filter: saturate(0.5);
}
.node.down::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--bad);
}

.node-head {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 0.9rem;
}
.node-id {
  font-family: var(--mono);
  font-weight: 650;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}
.crown {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.crown::before {
  content: "";
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-hot);
  box-shadow: var(--glow);
}

/* The load meter: in_flight / slots, filled with heat. The fill eases when load shifts, so
   a node warming up is something you *see* move. This is the node's real scheduling metric. */
.meter {
  height: 12px;
  border-radius: 999px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  overflow: hidden;
  margin-bottom: 0.6rem;
}
.meter-fill {
  height: 100%;
  min-width: 0;
  background: var(--heat);
  border-radius: 999px;
  transition: width var(--dur) var(--ease);
}
.meter-fill.over {
  background: repeating-linear-gradient(45deg, var(--clay), var(--clay) 6px, var(--sun) 6px, var(--sun) 12px);
}

.node-load {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.load-num {
  font-family: var(--mono);
  font-size: 1.5rem;
  font-weight: 650;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.load-den { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }

.node-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem 1rem;
  margin: 0 0 0.95rem;
}
.node-stats > div { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.node-stats dt {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.node-stats dd {
  margin: 0;
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }

/* The data table's cross-column filter clauses (#118): existing ones as removable chips,
   with the box to add another on the same line. A per-column box cannot say
   `a = 1 OR b = 2`, which is the whole reason this exists beside them. */
/* A table's declared columns, as one chip each. The previous form was a comma-joined run
   ("id int key, username text null, …") which read as prose and scanned as none: the names,
   the types and the flags were all the same weight and the same colour. Here the name leads,
   the type is quiet beside it, and only the *exceptions* are tagged. */
.schema { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.col-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-2);
  font-size: 0.78rem;
  line-height: 1.5;
}
.col-chip b { font-weight: 600; color: var(--text); }
.col-type { font-family: var(--mono); font-size: 0.72rem; color: var(--muted); }
/* A flag, not a value: uppercased and tracked out so it cannot be misread as data. */
.col-tag {
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.col-tag.is-key { color: var(--accent); }
.col-tag.is-bad { color: var(--bad); }

.clauses { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin: 0 0 0.6rem; }
.clauses input {
  flex: 1 1 16rem;
  min-width: 10rem;
  padding: 0.3rem 0.5rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  font-family: var(--mono);
  font-size: 0.75rem;
}
.clauses input:focus { outline: none; border-color: var(--accent); }

/* A node's admin actions (Remove) sit at the foot of its cell, right-aligned and quiet —
   membership changes are deliberate, not a primary action of the view. */
.node-actions { display: flex; justify-content: flex-end; margin-top: 0.9rem; }
.chip {
  font-family: var(--mono);
  font-size: 0.72rem;
  padding: 0.15rem 0.55rem;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
}
.chip.none { color: var(--muted); font-style: italic; }
/* A chip that navigates: a real <button>, reset to the pill's look, with a hover cue. */
button.chip { appearance: none; -webkit-appearance: none; }
.chip.link { cursor: pointer; transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
.chip.link:hover { border-color: var(--accent); color: var(--accent); }
.chip .badge { margin-left: 0.4rem; vertical-align: middle; }
/* The remove (×) on a builder chip. */
.chip-x {
  margin-left: 0.45rem; padding: 0; border: none; background: none;
  color: var(--muted); cursor: pointer; font-size: 1em; line-height: 1;
}
.chip-x:hover { color: var(--bad); }

/* The schedule pipeline builder: a search box with a floating result list, and the chosen
   stages shown as removable chips above it. */
.pipeline-builder { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.55rem; }
/* Several controls belonging to one `.pb-label` — the builder itself is a column, so without
   this they stack one per line and a three-part choice reads as three separate settings. */
.pb-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.pb-controls > input { flex: 1 1 11rem; min-width: 0; }
.pb-label { font-size: 0.85rem; color: var(--text); }
.pb-label .opt { color: var(--muted); font-weight: 400; }
.pipeline-search { position: relative; }
.pipeline-search input { width: 100%; box-sizing: border-box; }
.typeahead {
  position: absolute; left: 0; right: 0; top: calc(100% + 3px); z-index: 30;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
  max-height: 15rem; overflow-y: auto;
}
.typeahead-item {
  display: flex; align-items: baseline; gap: 0.6rem; width: 100%;
  padding: 0.45rem 0.7rem; border: none; background: none; cursor: pointer;
  text-align: left; color: var(--text);
}
.typeahead-item:hover { background: var(--panel-2); }
.typeahead-item .ti-name { font-weight: 500; }
.typeahead-item .ti-id { font-size: 0.72rem; margin-left: auto; }
.typeahead-empty { padding: 0.5rem 0.7rem; color: var(--muted); font-size: 0.85rem; }

/* The per-parameter run form: a labelled input for each detected script parameter. */
.param-form { display: flex; flex-direction: column; gap: 0.5rem; }
.param-row { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; }
.param-label { display: flex; flex-direction: column; flex: 0 0 12rem; }
.param-label .mono { color: var(--accent); }
.param-meta { font-size: 0.72rem; }
.param-input { flex: 1; min-width: 12rem; box-sizing: border-box; }
.param-switch { display: flex; align-items: center; gap: 0.4rem; flex: 1; color: var(--muted); font-size: 0.85rem; }
.param-switch input { width: auto; }
/* The dependency-chain rows in a job card: a label and a wrapping row of job chips. */
.chain-row { display: flex; align-items: baseline; gap: 0.6rem; margin: 0.35rem 0; flex-wrap: wrap; }
.chain-label { min-width: 3.2rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; }
.chain-row .chips { flex: 1; }
.chip.ok   { color: var(--ok);   border-color: currentColor; background: color-mix(in srgb, var(--ok) 12%, transparent); }
.chip.warn { color: var(--warn); border-color: currentColor; background: color-mix(in srgb, var(--warn) 12%, transparent); }
.chip.bad  { color: var(--bad);  border-color: currentColor; background: color-mix(in srgb, var(--bad) 12%, transparent); }

/* A schedule's pipeline: collapsed to a "N stages" disclosure pill in the table, expanding
   to a compact numbered list that reads top-to-bottom instead of wrapping into a mess of
   inline `id → id → id`. */
details.chain-fold > summary {
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel-2);
  color: var(--muted);
  font-family: var(--mono); font-size: 0.72rem;
  user-select: none; list-style: none;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
details.chain-fold > summary::-webkit-details-marker { display: none; }
details.chain-fold > summary::before {
  content: "▸"; color: var(--accent);
  transition: transform var(--dur-fast) var(--ease);
}
details.chain-fold[open] > summary::before { transform: rotate(90deg); }
details.chain-fold > summary:hover { border-color: var(--accent); color: var(--accent); }
details.chain-fold .chain { margin-top: 0.4rem; }

.chain { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.chain li { display: flex; align-items: center; gap: 0.45rem; }
.stage-n {
  flex: none;
  width: 1.35rem; height: 1.35rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--muted);
  font-family: var(--mono); font-size: 0.68rem;
}

/* =========================================================================
   Forms
   ========================================================================= */

.form { display: grid; gap: 1.1rem; }

/* An in-view create/edit form (Users, Libraries, Scripts) sits directly above its table;
   give it the same bottom gap the filter bar has so the form and the table beneath it do not
   touch. Scoped to `.view >` so the dialog forms (login, add-node) inside a modal card are
   unaffected. */
.view > .form { margin-bottom: 1.25rem; }
/* The same problem one level in: the data table card's "add row" form sits directly above
   the filter/join boxes, and it is not inside a `.view`, so the rule above never reached it.
   Without this its Add row / Cancel buttons touch the query box underneath them. */
#newrow-form { margin-bottom: 1.25rem; }

/* Block, not grid: the caption (label text + its `optional`/`required` hint) then flows on
   one line as inline text, and the control drops below it. A grid put the hint on its own
   row, which made a hinted field taller than an un-hinted one beside it and knocked their
   controls out of alignment. */
.form label { display: block; font-weight: 600; }
.form label > select,
.form label > input,
.form label > textarea { display: block; margin-top: 0.4rem; }
.form label > input,
.form label > textarea { width: 100%; }
.form label > select { width: auto; }        /* a dropdown sizes to its options */
.form label > input.narrow { width: 8rem; }   /* small numeric fields stay small */

/* The script and library create/edit forms sit above their registry list, so each needs a
   boundary of its own — a card with a border and space beneath it — or it reads as part of
   the table below. */
#sc-form,
#lib-form {
  padding: 1.35rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: var(--shadow-1);
  margin-bottom: 1.75rem;
  animation: rise var(--dur-fast) var(--ease) both;
}
.row { display: flex; gap: 1rem; align-items: start; flex-wrap: wrap; }
/* Buttons in a form row sit on the same baseline as the inputs beside them (the row aligns
   to `start`, so without this a button floats to the top while the fields drop below their
   captions). This keeps Save/Cancel — and Submit — level with the controls. */
.form .row > button { align-self: end; }
/* Side-by-side textarea fields (Arguments / Secrets / Libraries): a longer hint wraps to
   more lines than its neighbours, making one caption taller than the rest. Aligning the row
   to the top then lines the *titles* up but drops the taller field's box below the others;
   aligning to the bottom does the reverse. So lay each of these fields out as a column
   stretched to the row's full height, with its title pinned to the top and its box pinned to
   the bottom — titles line up with titles and boxes with boxes, however the hints wrap. */
.row > label:has(> textarea) {
  display: flex;
  flex-direction: column;
  align-self: stretch;
}
.row > label:has(> textarea) > textarea { margin-top: auto; }

/* A short list of email addresses is not a script: one line, comma-separated, sized to two
   or three addresses rather than growing to fill the row. A single-line input rather than a
   textarea on purpose — every other control on the notification row is one line tall, and a
   two-row textarea beside them left the checkbox and the dropdown floating at the top of a
   taller row with nothing lining up. */
.form label > input.addresses { width: min(20rem, 100%); }

/* A row of small controls (the limits, the notification group) closes up: the default 1rem
   column gap is sized for full-width fields and leaves a line of narrow ones looking
   scattered across the form. Column gap only — the row gap stays generous, because on a
   narrow window these wrap onto two lines and captions must not run into the controls
   above them. */
.row.tight { column-gap: 0.75rem; row-gap: 1rem; }

/* A captioned field is two lines — caption, then control — and a checkbox is one. Left to
   `align-items: start` the checkbox floated up level with the *captions*, so a row mixing
   the two read as a jumble: "Report progress" sitting alongside the word "Timeout" rather
   than alongside the box you type the timeout into.

   So drop it to the control line. Now a row reads the way it looks like it should: captions
   across the top, controls across the bottom, the checkbox being simply another control
   whose label happens to sit beside it instead of above it. The bottom padding is what
   centres its line against the taller boxes rather than resting it on their floor —
   `0.45rem` of input padding plus a border, halved. */
.form .row > label.check {
  align-self: end;
  padding-bottom: 0.5rem;
}
/* …and anchored to the right edge: `margin-left: auto` on the first button pushes it and any
   button after it (Cancel) hard against the form's right side, instead of trailing the
   narrow Timeout/grace fields into the middle. */
.form .row > button:first-of-type { margin-left: auto; }

/* The run options: four sections you open, side by side under the editor.
 *
 * Four layouts were built before this one and each failed in its own direction — stacked
 * full-width labels (a narrow strip of controls down the left of a wide page), a side rail
 * beside the editor (takes width from the thing being written), two flex rows paired across
 * the line (they wrap at different points, so nothing lines up), a four-column grid
 * (everything lined up and spread — an 8rem box adrift in a 19rem cell). They were all
 * attempts to make a dozen controls look good *while showing all of them at once*.
 *
 * The dozen controls are the problem. Almost every run uses none of them: a script, and
 * submit. So they are folded away by section, and each closed bar carries its own state —
 * "2 nodes", "timeout 60 s" — which is what makes folding honest rather than hiding. Open one
 * and it grows in place; the others keep their height, because the grid aligns to `start`.
 *
 * `auto-fit` from 15rem: four bars across a wide window, two on a laptop, one on a phone. The
 * bars are equal width by construction, which is the tidiness the packed line could never
 * quite have — its controls are genuinely different widths. */
.opt-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 0.75rem;
  align-items: start;
}

.opt-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  min-width: 0;
  transition: border-color var(--dur-fast) var(--ease);
}
/* An open section carries a hairline of the accent down its left edge, so which one you are
   editing is legible from across the row without the box shouting. */
.opt-card[open] { border-left: 2px solid var(--accent); }

/* The same summary language as `details.fold` — a bar with a chevron, mono and quiet — at the
   smaller scale a form control wants, and with the state pinned to its right end. */
.opt-card > summary {
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.55rem 0.7rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text);
  user-select: none;
  list-style: none;
  border-radius: var(--radius);
}
.opt-card > summary::-webkit-details-marker { display: none; }
.opt-card > summary::before {
  content: "▸";
  color: var(--accent);
  transition: transform var(--dur-fast) var(--ease);
}
.opt-card[open] > summary::before { transform: rotate(90deg); }
.opt-card > summary:hover { color: var(--accent); }
.opt-card > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The state of a closed section, in its own quiet voice: what is set, never what the section
   is for. An unset section says so ("none", "any node", "defaults") rather than going blank,
   because a bar with nothing on its right reads as "not loaded yet". */
.opt-state {
  margin-left: auto;
  text-align: right;
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.opt-card[open] .opt-state { opacity: 0; }

.opt-body {
  padding: 0.85rem 0.7rem;
  border-top: 1px solid var(--line);
}
/* Fields inside an open section: packed, wrapping, one baseline. A section is narrow, so its
   controls sit at `.row.tight`'s gap and wrap rather than being spread to fill it. */
.opt-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.9rem 0.85rem;
}
.opt-fields > label.grow { flex: 1; min-width: 11rem; }
.form .opt-fields label > input.addresses { width: 100%; }
.form .opt-fields > label.check { padding-bottom: 0.5rem; }
/* Muted inline text — a column's type, an empty-state line, "none — cannot be notified".
   This used to *also* be a hover-popup hint on every form caption; that is gone. A caption
   hint is shown for as long as the label is hovered or holds focus, which turned into a
   bubble floating over the form the whole time anyone was filling it in, and it was worst on
   the labels wrapping a code editor (#115) — a target you keep the pointer and the focus in
   for as long as you are writing. Do not reintroduce it. */
.opt { font-weight: 400; color: var(--muted); font-size: 0.8rem; }

.result {
  padding: 0.9rem 1.1rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  background: var(--panel);
}
.result.ok  { border-left-color: var(--ok); }
.result.err { border-left-color: var(--bad); }

/* A CSV import's validation report (#117). Every problem is listed, not just the first — the
   import is all-or-nothing, so a report that stopped early would mean one round trip per
   mistake. It scrolls rather than growing without bound: a file with a wrong column reports
   a problem on every line of it, and that must not push the buttons off the screen. */
.csv-errors {
  margin-top: 0.5rem;
  max-height: 16rem;
  overflow-y: auto;
  border-top: 1px solid var(--line);
}
.csv-error {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.82rem;
}
.csv-error:last-child { border-bottom: 0; }
.csv-error .mono { font-family: var(--mono); font-size: 0.78rem; white-space: nowrap; }
.csv-error .dim { color: var(--muted); }

/* The import form puts its two fields and both buttons on one row, so all four have to share
   a bottom edge. A file input and a `<select>` do not agree on an intrinsic height — the
   picker is set in a smaller mono face, so it comes out shorter — and the buttons align to
   the *row's* bottom, which means they line up with the taller of the two and float free of
   the other. Pinning both controls to one height is what makes the row a row. */
#csv-form .row > label > select { height: var(--field-h); }

/* The file picker, so it sits like the other fields rather than like a 1996 form control.
   Fixed to the same height as the `<select>` beside it, because they do not agree on an
   intrinsic one — the picker is set in a smaller mono face, so it comes out a different size,
   and the buttons on that row align to the row's bottom and so line up with whichever is
   taller.
   The browser's own button is **stretched to fill** that height rather than centred within
   it. Centring was the obvious thing and does not work: a file input's button and label are
   shadow content, and `display: flex` / `align-items` on the input are not reliably honoured
   for them, so the button sits on the line's baseline near the bottom of a taller box. Making
   it the full height removes the question instead of asking it, and the filename beside it is
   centred by a `line-height` that matches the box — which needs no cooperation from the
   shadow tree at all. */
input[type="file"] {
  --field-inner: calc(var(--field-h) - 2px); /* the border box, less its two borders */
  width: 100%;
  height: var(--field-h);
  padding: 0;
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: var(--field-inner);
  cursor: pointer;
  overflow: hidden; /* a long filename is clipped by the pill, never pushed out of it */
}
input[type="file"]::file-selector-button {
  height: var(--field-inner);
  margin: 0 0.7rem 0 0;
  padding: 0 0.9rem;
  border: 0;
  border-right: 1px solid var(--line);
  /* One less than the pill's 8px, so it nests inside the border rather than crossing it. */
  border-radius: 7px 0 0 7px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  vertical-align: top;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
input[type="file"]::file-selector-button:hover { color: var(--accent); }
.result .why { color: var(--muted); margin-top: 0.4rem; font-size: 0.85rem; }

/* =========================================================================
   Detail cards (centered pop-outs — jobs and scripts alike; the old side
   drawer is gone)
   ========================================================================= */

/* The wrapper is its own overlay, exactly like the dialogs below: fade the whole
   thing, translate/scale only the card. */
.card-wrap {
  position: fixed;
  inset: 0;
  z-index: 30; /* below the confirm dialog (40), which a card can open */
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: rgb(28 30 27 / 0.5);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.card-wrap.show { opacity: 1; }

.card {
  width: min(58rem, 100%);
  max-height: min(88vh, 100%);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden; /* the head is squared; the card's corners must clip it */
  box-shadow: var(--shadow-card);
  transform: translateY(10px) scale(0.98);
  transition: transform var(--dur-fast) var(--ease);
}
.card-wrap.show .card { transform: none; }

.card-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1.1rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
}
#j-id, #c-id { font-size: 0.82rem; }

.card-scroll { overflow-y: auto; padding: 1.25rem; }

.kv { display: grid; grid-template-columns: 9rem 1fr; gap: 0.45rem 1rem; margin-bottom: 1.4rem; }
/* The metadata splits into two columns on a wide card so it fills the width instead of
   hugging the left edge. Each column is its own `.kv` grid; the pair shares one bottom
   margin. `.kv-wide` is the full-width row of long list fields below them. */
.kv-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2.5rem; margin-bottom: 1.4rem; }
.kv-cols .kv { margin-bottom: 0; }
.kv dt {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  align-self: center;
}
.kv dd { margin: 0; }

.stuck {
  padding: 0.85rem 1.1rem;
  border: 1px solid var(--warn);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  margin-bottom: 1.4rem;
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}
.stuck h4 { margin: 0 0 0.4rem; color: var(--warn); font-size: 0.85rem; }

h4.section {
  margin: 1.4rem 0 0.6rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}
h4.section:first-child { margin-top: 0; }

/* "● live" beside the Output heading while a job is still running (#94). */
.live {
  color: var(--ok);
  margin-left: 0.5em;
  letter-spacing: 0;
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* A checkbox row in a form (e.g. "Report progress"): the box beside its label, laid out
   horizontally rather than the stacked label/control the other fields use.

   The `display: flex` is load-bearing and was missing: without it the three flex properties
   below are inert, `.form label`'s `display: block` wins, and `.form label > input` leaves
   the checkbox a block — so the box sat on its own line *above* its caption instead of
   beside it. `label.node-pick` and `label.lib-detect` below both spell out
   `display: inline-block` on their inputs for exactly this reason; this row was the one
   that missed it. */
label.check {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}
label.check input { display: inline-block; width: auto; margin: 0; }

/* The "run on" node picker: one tickable pill per cluster member.
   A wrapping row of pills rather than a <select multiple>, because the choice is small
   (a cluster is nodes, not thousands of rows), ticking is unambiguous where ctrl-click
   is not, and each entry needs room for the address and runtimes that are what anyone
   actually recognises — a bare "3" is not a node anybody remembers. */
.node-picker { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.35rem; }
/* Qualified `label.node-pick`, not `.node-pick`, for the same reason `label.check` above
   is: `.form label` sets `display: block` and `.form label > input` stretches the control
   to full width, and both outrank a bare class selector. Two classes' worth of
   specificity is what it takes to be a row of pills inside a form. */
label.node-pick {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-2);
  font-weight: 400;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease);
}
label.node-pick:hover { border-color: var(--accent); }
label.node-pick input { display: inline-block; width: auto; margin: 0; }
.node-pick-id { font-family: var(--mono); font-size: 0.8rem; font-weight: 600; }
/* Ticked reads as chosen at a glance, without waiting for the eye to find the checkbox. */
label.node-pick:has(input:checked) { border-color: var(--accent); background: var(--panel); }
label.node-pick:has(input:checked) .node-pick-id { color: var(--accent); }

/* The library picker: what the script gets, plus a way to find more.
   Deliberately *not* the node picker's shape, which is a tick-box per member. A cluster is
   a knowable handful and every one of them is a live consideration; the library registry is
   unbounded, and all but a couple of entries are irrelevant to any given script. So the
   attached ones are chips (a short, exact answer to "what will this job get") and the rest
   live behind a search — which keeps the control the same size whether ten libraries are
   registered or a thousand. */
.lib-chosen { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: 0.35rem; }
.lib-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.25rem 0.3rem 0.25rem 0.6rem;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--panel);
}
.lib-chip-name { font-family: var(--mono); font-size: 0.8rem; font-weight: 600; color: var(--accent); }
/* The × is the only destructive control in the picker, so it is quiet until pointed at. */
.lib-drop {
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
  border-radius: 4px;
}
.lib-drop:hover { color: var(--bad); background: color-mix(in srgb, var(--bad) 12%, transparent); }
/* "imported" — why this one is here without being asked for. Quiet: it annotates the chip,
   it is not a second label competing with the name. */
.lib-auto {
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.85;
}

/* The search, and its menu. Positioned relative so the menu can overlay what follows
   instead of shoving the rest of the form down every time it opens. */
.lib-add { position: relative; margin-top: 0.5rem; max-width: 30rem; }
input.lib-search { width: 100%; }
input.lib-search:disabled { opacity: 0.6; cursor: default; }
.lib-menu {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  margin-top: 0.25rem;
  max-height: 17rem;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: var(--shadow-1);
}
.lib-opt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 0;
  background: none;
  padding: 0.4rem 0.6rem;
  cursor: pointer;
}
/* One highlight for both the pointer and the arrow keys, so the two never disagree about
   which row Enter would take. */
.lib-opt:hover, .lib-opt.on { background: var(--panel-2); }
.lib-opt-name { font-family: var(--mono); font-size: 0.8rem; font-weight: 600; color: var(--text); }
.lib-opt-desc {
  font-size: 0.76rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
/* What is being held back, and why nothing matched — the two things a truncated list must
   say out loud, or it reads as the whole registry. */
.lib-opt-more {
  padding: 0.4rem 0.6rem;
  font-size: 0.76rem;
  color: var(--muted);
  border-top: 1px solid var(--line);
}
.lib-menu .lib-opt-more:only-child { border-top: 0; }

.lib-foot {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.5rem;
}
label.lib-detect {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.4rem;
  font-weight: 400;
  font-size: 0.82rem;
  color: var(--muted);
  cursor: pointer;
  width: fit-content;
}
label.lib-detect input { display: inline-block; width: auto; margin: 0; }
/* An import the cluster cannot honour. Warn, not error: the script is submittable and will
   run — it is the *import* that will fail, quietly, which is the whole reason this is said
   out loud rather than left to a job that reports success having done nothing. */
.lib-warn {
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--warn);
  border-left: 2px solid var(--warn);
  padding-left: 0.6rem;
}
.lib-warn code { font-family: var(--mono); font-size: 0.95em; }

/* A pinned job in the jobs table: the node is a choice, not a placement. */
.pin-flag { margin-left: 0.35rem; font-size: 0.8em; opacity: 0.8; }

/* A live progress bar for an instrumented job (#-progress). */
.progress { margin: 0 0 0.6rem; }
.progress-track {
  height: 0.5rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  overflow: hidden;
}
.progress-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 0.3s ease;
}
/* A status-only Write-MelangeProgress has no percentage: sweep an indeterminate bar. */
.progress-fill.indeterminate {
  width: 35%;
  animation: indeterminate 1.4s ease-in-out infinite;
}
@keyframes indeterminate {
  0% { margin-left: -35%; }
  100% { margin-left: 100%; }
}
.progress-label {
  margin-top: 0.3rem;
  font-size: 0.8rem;
  color: var(--muted);
  font-family: var(--mono);
}

pre {
  margin: 0 0 0.5rem;
  padding: 0.95rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}
pre.err { border-color: var(--bad); border-left: 3px solid var(--bad); }

/* =========================================================================
   Syntax highlighting (#115)
   ========================================================================= */

/* The eleven token classes. `tokensToHtml` in app.js emits these and nothing else — the
   class name is never built from source text, so this list is the complete vocabulary.
   Anything the lexer does not classify is left bare and inherits the body colour. */
.tok-comment { color: var(--tok-comment); font-style: italic; }
.tok-str     { color: var(--tok-str); }
.tok-kw      { color: var(--tok-kw); font-weight: 600; }
.tok-var     { color: var(--tok-var); }
.tok-cmd     { color: var(--tok-cmd); font-weight: 600; }
.tok-fn      { color: var(--tok-fn); font-weight: 600; }
.tok-num     { color: var(--tok-num); }
.tok-type    { color: var(--tok-type); }
.tok-param   { color: var(--tok-param); }
.tok-op      { color: var(--tok-op); }
.tok-punct   { color: var(--tok-punct); }

/* A highlighted source block in a detail card. Deliberately a *class* rather than a change
   to the `pre` rule above: job output, `pre.err` and the docs all rely on that rule's
   `pre-wrap` + `word-break`, which wraps prose sensibly and would mangle code. Code scrolls
   sideways instead, matching the editor, and that is also what makes the line numbers below
   line up — with soft wrap, a visual row is not a logical line. */
pre.code {
  white-space: pre;
  word-break: normal;
  tab-size: 4;
  counter-reset: ln;
}
/* Numbers come from a CSS counter, not from JS: nothing to measure, nothing to keep in step
   with the text, and the numbers cannot drift from the lines they label.
   The `.ln` spans stay **inline** and the real newlines stay in the markup — so `white-space:
   pre` still breaks the lines, and copying the block copies actual line breaks rather than
   one run-on string. A block-level line span plus a literal newline would double-space it. */
pre.code .ln::before {
  counter-increment: ln;
  content: counter(ln);
  display: inline-block;
  width: 3rem;
  padding-right: 0.6rem;
  text-align: right;
  color: var(--muted);
  opacity: 0.65;
  user-select: none;
  -webkit-user-select: none;
}

/* The copy button rides the fold's summary bar, so it costs no vertical space. */
.fold-head { display: flex; align-items: center; gap: 0.5rem; }
button.copy {
  margin-left: auto;
  background: none;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
button.copy:hover { border-color: var(--accent); color: var(--accent); }
button.copy.done { border-color: var(--ok); color: var(--ok); }

/* =========================================================================
   The code editor (#115)
   ========================================================================= */

/* A transparent <textarea> sitting exactly on top of a highlighted copy of its own text.
   The textarea is the real control and never stops being one — .value, the input event,
   native undo, IME, selection and screen-reader semantics all still belong to the browser.
   We only paint underneath it.
   Everything here exists to make the two layers agree glyph-for-glyph. The rules that look
   redundant are not: see the eight numbered pitfalls in app.js's editor section. */
.editor {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  overflow: hidden;          /* with `resize` below, this is what clips to the new height */
  resize: vertical;          /* the wrapper resizes, not the textarea — they must not detach */
  min-height: 6rem;
  /* The form's labels are `font-weight: 600`, and the editor is inside one — without this
     the whole script renders bold and .tok-kw/.tok-cmd have nothing left to say. */
  font-weight: 400;

  /* The editor's metrics, in one place. Both text layers, the gutter and the initial height
     are all derived from these four, so nothing can be changed in half. `--ed-rows` is set
     per editor from the textarea's own `rows` attribute; the rest are the type. */
  --ed-font: 0.85rem;
  --ed-line: 1.55;
  --ed-pad: 0.5rem;
  --ed-rows: 12;
  transition: border-color var(--dur-fast) var(--ease);
}
/* The spacing the textarea used to get from `.form label > textarea`, now that it is a
   grandchild of the label rather than a child.
   These live on `.editor` itself and NOT on `.form label > .editor`, and that is not a
   style preference — a descendant selector there outranks every `.editor.…` modifier below
   it, so the more specific rule silently won three fights it should have lost: a
   `display: block` undid the flex column (and then the wrapper resized while the text
   inside it did not), and `width`/`margin` overrode fullscreen's insets. Keep editor
   layout on single-class selectors so the modifiers can do their job. */
.editor { margin-top: 0.4rem; width: 100%; }
.editor:hover { border-color: var(--muted); }
.editor:focus-within { border-color: var(--accent); }

/* The scrolling area. The gutter is a sibling of the box rather than padding inside it, so
   it can stay put while the text scrolls sideways under it.

   Its height is the editor's *initial* height, computed from the textarea's own `rows` — so
   the existing rows=12 / rows=14 in index.html still decide it, with nothing measured. That
   matters more than it looks: both forms are `hidden` when the editors are built, so a
   measured height would be zero and every editor would come up two pixels tall. `flex-grow`
   is what lets it fill a wrapper that *has* been given an explicit height, by a resize drag
   or a restored one. */
.ed-main {
  display: flex;
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  height: calc(var(--ed-rows) * var(--ed-font) * var(--ed-line) + var(--ed-pad) * 2);
}

.ed-gutter {
  flex: 0 0 auto;
  width: 3.2rem;
  padding: var(--ed-pad) 0.5rem var(--ed-pad) 0;
  background: var(--panel-2);
  border-right: 1px solid var(--line);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--ed-font);
  line-height: var(--ed-line);
  text-align: right;
  white-space: pre;
  overflow: hidden;
  user-select: none;
  opacity: 0.7;
}

.ed-box { position: relative; flex: 1 1 auto; min-width: 0; }

/* Layers 1-3, stacked. All three share the font metrics and the padding below, and they take
   them from the same four custom properties the height above is computed from — one source of
   truth, so the layers cannot drift from each other or from the box that holds them. A single
   divergence here (a different tab-size, a px line-height on one) is a visible misalignment. */
.ed-box > .ed-hl,
.ed-box > textarea,
.ed-box > .ed-current {
  font-family: var(--mono);
  font-size: var(--ed-font);
  line-height: var(--ed-line);
  tab-size: 4;
}

.ed-box > .ed-hl,
.ed-box > textarea { padding: var(--ed-pad) 0.6rem; }

/* The highlight layer is a *clipping box*, not a second scroller — its <code> child is moved
   with a transform from the textarea's scroll event. A second scrollable element desyncs at
   the extremes and janks on momentum scrolling. */
.ed-hl {
  position: absolute;
  inset: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  overflow: hidden;
  white-space: pre;
  word-break: normal;
  color: var(--text);
  pointer-events: none;
}
.ed-hl > code {
  display: block;
  font: inherit;
  color: inherit;
  white-space: pre;
  will-change: transform;
}

/* The current-line band, under the text and over the panel. */
.ed-current {
  position: absolute;
  left: 0;
  right: 0;
  height: 1.55em;
  background: var(--panel-2);
  opacity: 0.55;
  pointer-events: none;
  display: none;
}
.editor:focus-within .ed-current { display: block; }

/* The real control. Transparent text, visible caret, visible selection. Both `color` and
   `-webkit-text-fill-color` are needed — Safari honours only the latter. */
.ed-box > textarea {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;     /* .ed-main is definite, so this is too — see the note there */
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  resize: none;             /* the wrapper resizes; a textarea that resized alone would detach */
  overflow: auto;
  white-space: pre;
  overflow-wrap: normal;
  color: transparent;
  -webkit-text-fill-color: transparent;
  caret-color: var(--text);
}
.ed-box > textarea:hover { border: 0; }
.ed-box > textarea:focus { outline: none; }   /* the wrapper carries the focus ring instead */
.editor:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Selection has to be translucent: it paints over the coloured text underneath. */
.ed-box > textarea::selection { background: var(--ring); color: transparent; -webkit-text-fill-color: transparent; }

/* Over the size cap the highlight layer is dropped and the textarea paints its own text.
   A 500 KB paste must degrade, not freeze. */
.editor.plain .ed-hl { display: none; }
.editor.plain .ed-box > textarea { color: var(--text); -webkit-text-fill-color: var(--text); }

.ed-status {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 0 0 auto;
  padding: 0.3rem 0.6rem;
  background: var(--panel-2);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
}
.ed-pos { flex: 0 0 auto; }
.ed-hint { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ed-hint.warn { color: var(--warn); }
.ed-keys { flex: 0 0 auto; opacity: 0.7; }

/* A clickable syntax error in a save result — jumps the editor to the line it names. */
.err-jump {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-left: 3px solid var(--bad);
  padding: 0.3rem 0.6rem;
  margin: 0 0 0.2rem;
  color: inherit;
  font-family: var(--mono);
  font-size: 0.8rem;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.err-jump:hover { background: var(--panel-2); }

/* A collapsible section (the script body in the script card). The summary is a visible
   *control* — a bordered bar with its own chevron — not a quiet heading: the first
   person shown one styled as a heading could not find it. */
details.fold summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 1.4rem 0 0.6rem;
  padding: 0.55rem 0.8rem;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text);
  user-select: none;
  list-style: none; /* the chevron below replaces the browser's marker */
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
details.fold summary::-webkit-details-marker { display: none; }
details.fold summary::before {
  content: "▸";
  color: var(--accent);
  transition: transform var(--dur-fast) var(--ease);
}
details.fold[open] summary::before { transform: rotate(90deg); }
details.fold summary:hover { border-color: var(--accent); color: var(--accent); }

/* A schedule fold is lighter than a section fold: normal-case text, tight stacking, and a
   summary that wraps rather than overflowing sideways when a row runs long. */
details.fold.sched summary {
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.8rem;
  margin: 0.4rem 0;
  flex-wrap: wrap;
}
details.fold.sched summary .badge { margin: 0; }
details.fold.sched .sched-body { padding: 0.1rem 0.2rem 0.6rem 1.7rem; }
details.fold.sched .chip-arrow { align-self: center; }

.card-scroll code { font-family: var(--mono); font-size: 0.82rem; color: var(--accent); }

/* =========================================================================
   Toast
   ========================================================================= */

.toast {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  padding: 0.8rem 1.2rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  /* Above every overlay — the card (30) and the confirm/login modal (40) — so a toast
     raised *from* one (a bad cron entered in the script card) is not blurred out behind it. */
  z-index: 50;
  max-width: min(42rem, 90vw);
  animation: toast-in 0.32s var(--ease);
}
.toast.err { border-left-color: var(--bad); }

/* =========================================================================
   Confirmation dialog (themed — replaces the browser's confirm())
   ========================================================================= */

.modal {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: rgb(28 30 27 / 0.55);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.modal.show { opacity: 1; }

.modal-card {
  width: min(28rem, 100%);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: 1.5rem;
  transform: translateY(8px) scale(0.98);
  transition: transform var(--dur-fast) var(--ease);
}
.modal.show .modal-card { transform: none; }

.modal-msg { margin: 0 0 1.4rem; line-height: 1.55; }

/* The prompt input, shown only for promptDialog(). Matches the form fields' look. */
.modal-input {
  width: 100%;
  margin: 0 0 1.4rem;
  padding: 0.55rem 0.7rem;
  background: var(--bg);
  /* `--text`, not `--fg`: there is no `--fg` in this stylesheet, so the declaration was
     invalid and the colour silently fell back to whatever the modal inherited. */
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
}
.modal-input:focus { outline: none; border-color: var(--accent); }

/* The optional extra yes/no, shown only for confirmWithOption(). A label wrapping its own
   box, so the text is part of the hit area. */
.modal-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 1.4rem;
  color: var(--muted);
  cursor: pointer;
}
.modal-check input { margin: 0; accent-color: var(--accent); }

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.6rem;
}

/* =========================================================================
   Keyframes
   ========================================================================= */

@keyframes rise   { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes drop   { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes breathe{ 0%, 100% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1; transform: scale(1.06); } }
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
/* the toast is centred via the keyframe's translate; hold it there at rest too */
.toast { transform: translateX(-50%); }

/* =========================================================================
   Responsive
   ========================================================================= */

@media (max-width: 800px) {
  /* Below the width where the three zones comfortably share one row, the bar goes to two
     rows rather than three: the brand and the account menu keep each other company on the
     first — both are chrome, both are small, and that row exists for the brand anyway — and
     only the tabs, the one zone that actually wants the width, drop to a line of their own.
     Stacking all three was the obvious version and made the header a third of a narrow
     screen tall before any content appeared. It stays a grid (the desktop `display` is not
     overridden), because two rows with a shared first row is what grid areas are for. */
  .command {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "lead conn"
      "tabs tabs";
    align-items: center;
    row-gap: 0.35rem;
    column-gap: 1rem;
    padding: 0.5rem 1rem;
  }
  .lead { grid-area: lead; gap: 0.75rem; }
  /* `align-self: stretch` is what lands the active tab's underline on the bar's bottom edge
     on desktop; here the tabs are their own row, so they take their height from their own
     padding and the underline sits under the row. */
  .tabs { grid-area: tabs; justify-self: stretch; align-self: auto; justify-content: center; }
  .conn { grid-area: conn; justify-self: end; justify-content: flex-end; }
  .node-grid { grid-template-columns: 1fr; }
  .kv { grid-template-columns: 7rem 1fr; }
  .kv-cols { grid-template-columns: 1fr; gap: 0; }
  .kv-cols .kv:not(:last-child) { margin-bottom: 1.4rem; }
}

/* =========================================================================
   Docs — the reference manual, rendered from DOCS.md. A prose column, not a
   table view: capped for readability and left-aligned rather than centred in
   the wide main area, so a long line never runs the width of the screen.
   ========================================================================= */

.docs {
  max-width: 48rem;
  /* Centre the prose column in the wide main area rather than letting it hug the left. */
  margin-inline: auto;
  line-height: 1.65;
  /* --text, not --ink: --ink is a fixed near-black that is *not* remapped for dark mode,
     so it would render the manual invisibly dark on the dark background. */
  color: var(--text);
}
.docs > :first-child { margin-top: 0; }

/* Headings clear the sticky command bar when reached by an anchor jump. */
.docs h1, .docs h2, .docs h3, .docs h4 {
  scroll-margin-top: 4.5rem;
  line-height: 1.25;
}
.docs h1 { font-size: 1.8rem; margin: 0 0 1rem; }
.docs h2 {
  font-size: 1.35rem;
  margin: 2.4rem 0 0.9rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--line);
}
.docs h3 { font-size: 1.1rem; margin: 1.8rem 0 0.6rem; }
.docs h4 { font-size: 0.98rem; margin: 1.4rem 0 0.5rem; }

.docs p { margin: 0 0 0.9rem; }
.docs a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.docs a:hover { color: var(--accent-hot); }
/* A relative link to a sibling repo file this UI does not serve (README.md, done.md):
   shown as text, but not a live link — there is nowhere for it to go here. */
.docs a.dead { color: inherit; text-decoration: none; cursor: default; }

.docs ul, .docs ol { margin: 0 0 0.9rem; padding-left: 1.4rem; }
.docs li { margin: 0.25rem 0; }
.docs li > ul, .docs li > ol { margin: 0.25rem 0; }

/* Inline code — the same clay the card bodies use, on a faint plate so it reads as a token. */
.docs code {
  font-family: var(--mono);
  font-size: 0.86em;
  color: var(--accent);
  background: var(--panel-2);
  padding: 0.1em 0.35em;
  border-radius: 5px;
  /* The manual is full of long unbroken identifiers and --flag names; without this a
     single one overflows the 48rem column and drags the whole page into a horizontal
     scroll. `anywhere` lets the token break mid-word rather than widening the line. */
  overflow-wrap: anywhere;
}
/* Inside a fenced block the plate would double up; the <pre> already carries it. */
.docs pre { margin: 0 0 1rem; }
.docs pre code { color: inherit; background: none; padding: 0; font-size: inherit; }

.docs blockquote {
  margin: 0 0 1rem;
  padding: 0.1rem 1rem;
  border-left: 3px solid var(--accent);
  background: var(--panel-2);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--muted);
}
.docs blockquote p:last-child { margin-bottom: 0.6rem; }
.docs blockquote p:first-child { margin-top: 0.6rem; }

/* Tables always fill the column and wrap their text rather than scrolling.
   `display: block` (needed for overflow-x) would shrink the table box to its
   content, so a narrow table's header bar stopped short of the full width and a
   wide one scrolled sideways. A real table at width: 100% keeps the header
   spanning the whole row; fixed layout + word breaking makes long cells wrap. */
.docs table {
  width: 100%;
  margin: 0 0 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  border-collapse: collapse;
  table-layout: fixed;
  font-size: 0.9rem;
}
.docs th, .docs td {
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  /* The bare `td`/`th` selectors force `white-space: nowrap` for the dashboard
     tables; without overriding it here every prose cell refuses to wrap and runs
     off the page, which no amount of word-break can undo. */
  white-space: normal;
  overflow-wrap: break-word;
  word-break: break-word;
}
.docs thead th { background: var(--panel-2); font-weight: 600; }
.docs tbody tr:last-child td { border-bottom: none; }

.docs hr { border: none; border-top: 1px solid var(--line); margin: 2rem 0; }

/* =========================================================================
   Reduced motion — honour it. The pulse stops ringing, the cursor stops
   blinking, reveals become instant. Nothing that carried meaning is lost:
   the dot's colour still says ready, the meter's width still says load.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .wordmark .cursor { opacity: 1; }
}

/* --- Data tables (#114) ------------------------------------------------------------
 *
 * The grid is the one place in this UI that is edited in place rather than through a
 * form, so it needs two things nothing else here does: a cell that looks typeable, and a
 * filter row that reads as part of the header rather than as data.
 */

/* A table's card carries a grid as wide as its schema, so it is allowed more room than a
   script or library card, which hold prose. */
.card.wide {
  width: min(84rem, 100%);
}

/* The schema editor on the create form: one row per column, fields sized to their job. */
.col-editor {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* The rows live in their own container, so the editor's gap separates the caption, the list
   and the "Add column" button — and nothing at all between one column row and the next. This
   is the gap that was missing: without it a table with three columns read as one dense block
   of controls with no telling where a row ended. */
#tbl-columns {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* Column gap matches `.row.tight`, which is the house size for a line of narrow controls —
   the default 1rem is meant for full-width fields and leaves these looking scattered, while
   anything under it runs the name box into the type dropdown. 

   Row gap is *smaller* than the gap between rows, and that is the point: on a narrow
   window a single column's controls wrap onto a second line, and the wrapped part has to
   read as a continuation of its own row rather than as a new one. Anything equal to or
   larger than the gap between rows would make it look like a row of its own. */
.col-row {
  align-items: center;
  column-gap: 0.75rem;
  row-gap: 0.4rem;
}

.col-row .col-name {
  flex: 1 1 12rem;
  min-width: 8rem;
}

.col-row .col-type {
  flex: 0 0 auto;
}

/* A checkbox or radio beside its own word, rather than stacked like the form's labels. */
label.inline {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
  font-size: 0.85rem;
  color: var(--muted);
}

label.inline input {
  width: auto;
  margin: 0;
}

/* The remove button sits at the end of the row rather than jammed against the last toggle. */
.col-row .col-rm {
  margin-left: auto;
}

/* The count strip above the grid. Deliberately quiet: it is context, and when the number
   is a bounded estimate it says so in the same breath. */
.agg {
  margin: 0.5rem 0;
  font-size: 0.85rem;
}

/* Centred counts, header and cells together.
   The header half is the part that matters: `th` is left-aligned globally, so a `td.num`
   that set only its own alignment put the number at one end of the column and the word
   naming it at the other — which reads as a misaligned table rather than as a deliberate
   numeric column. Both selectors are here so the two can never drift again; anything using
   `.num` must put it on the `<th>` as well (the Jobs table does the same with `.r`).
   `tabular-nums` still keeps the digits themselves on a fixed pitch. */
td.num,
th.num {
  text-align: center;
  font-variant-numeric: tabular-nums;
}

table.grid td.cell {
  cursor: text;
  max-width: 24rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

table.grid td.cell:hover,
table.grid td.cell:focus-visible {
  background: var(--panel-2);
}

/* The in-place editor fills its cell exactly, so committing an edit does not make the
   column jump. */
table.grid input.cell-edit {
  width: 100%;
  margin: 0;
  padding: 0.15rem 0.3rem;
  font: inherit;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm, 4px);
  background: var(--panel);
  /* `--text`, not `--fg`: there is no `--fg` here, and with no fallback the declaration is
     invalid at computed-value time, so the colour silently fell back to whatever it
     inherited. The same slip is noted on `.modal-input`. */
  color: var(--text);
}

/* The per-column filter boxes live in the header, so they read as part of it. */
table.grid tr.filters td {
  padding: 0.2rem 0.35rem;
  background: var(--panel-2);
}

table.grid tr.filters input {
  width: 100%;
  margin: 0;
  padding: 0.15rem 0.3rem;
  font-size: 0.8rem;
}

/* The data grid scrolls in its own pane, so its header can stay put (#122).
 *
 * `th` has been `position: sticky` all along and it did nothing here, for a reason worth
 * writing down: a sticky element sticks to its nearest *scrolling* ancestor, and
 * `.table-wrap`'s `overflow-x: auto` makes it exactly that — a box that scrolls sideways and
 * never vertically, so `top: 0` had nothing to stick against while the card scrolled behind
 * it. Giving the pane a height of its own is what makes the vertical scroll happen *inside*
 * the thing the header is sticking to.
 *
 * Two rows stick, not one: the column names, and the filter boxes under them. Reaching a
 * hundred rows down for the box that narrows the column you are reading is the whole
 * complaint. */
.table-wrap.rows-pane {
  overflow: auto;
  max-height: min(60vh, 40rem);
  /* The card behind it does not start scrolling when this pane hits its end. */
  overscroll-behavior: contain;
}

.rows-pane thead th { z-index: 2; }

.rows-pane table.grid tr.filters td {
  position: sticky;
  /* Under the column names. The height is measured and set by `stickRowHeader`; the fallback
     is only what applies for the instant before it, or if the card is not laid out yet. */
  top: var(--head-h, 2.1rem);
  z-index: 1;
}

/* Borders on a collapsed-border table belong to the table, not the cell, so they scroll away
   from a sticky row and leave it floating on the rows beneath. An inset shadow is drawn by the
   cell itself and travels with it. */
.rows-pane thead th,
.rows-pane table.grid tr.filters td {
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 var(--line);
}

table.grid td.row-actions {
  width: 2rem;
  text-align: right;
}

table.grid td.row-actions button {
  padding: 0 0.4rem;
  line-height: 1.4;
}
