/* Voodu WebUI — Brand Theme
 *
 * Two layers:
 *
 *   1. :root  — CSS custom properties (the source of truth). Tokens:
 *      deep navy bg, layered surface colors, brand-green accent
 *      (#34d399 dark / #059669 light, unified with the status green),
 *      blue links, traffic-light semantics. Dark-first; the light
 *      theme ships via html[data-theme="light"] below.
 *
 *   2. @theme — Tailwind 4 mapping. Every var also surfaces as a
 *      Tailwind utility (bg-voodu-surface, text-voodu-muted, etc.)
 *      so component code can use either CSS vars OR utility classes
 *      and stay coherent.
 *
 * Fonts: Geist + Geist Mono, self-hosted from public/fonts/voodu/.
 * Source files come from the `geist` npm package and are copied at
 * `pnpm install` time via the `fonts:copy` postinstall script. Both
 * are variable-axis woff2 (~70 KB each) — covers every weight 100..900
 * in a single request, no CDN dependency, no FOIT/FOUT from a third
 * party going down. `font-display: swap` keeps first paint instant
 * while the woff2 loads.
 */

@font-face {
  font-family: "Geist";
  src: url("/fonts/voodu/Geist-Variable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("/fonts/voodu/GeistMono-Variable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ── Log stream rows ──
 *
 * Layout strategy: ONE outer grid on `.log-list`, every `.log-row`
 * marked `display: contents`. Result: all cells across all rows flow
 * into the same 5-column grid, so column widths auto-fit the widest
 * content GLOBALLY (pod column doesn't zig-zag between
 * `controller.67ad` rows and `events.6cb4` rows).
 *
 * Why not per-row `display: grid`? Each row's auto-sized columns
 * would be local to that row, producing visible misalignment between
 * adjacent rows with different pod-name lengths. The outer grid lifts
 * sizing to the list level — same trick browsers use for HTML tables.
 *
 * Trade-offs of `display: contents` we accept:
 *   - The row itself has no box, so `position: relative`, `padding`,
 *     and `border-left` can't live on `.log-row`. We move:
 *       border-left → `.log-row > .log-ts` (first cell)
 *       position    → `.log-row > .log-body` (anchors the copy btn)
 *       padding-y   → each cell (consistent row height)
 *   - `row.title` tooltip doesn't fire (no box). We move titles onto
 *     `.log-body` in the JS renderer.
 *   - `:hover` still works (cursor over any child satisfies the
 *     ancestor selector), so the hover-reveal copy button is fine.
 *
 * `.log-wrap` toggled on the parent by the Wrap button: switches the
 * body cell from ellipsis to word-break. The grid template stays
 * the same.
 */

.log-list {
  display: grid;
  /* 4 columns: ts / level / pod / body. IP column intentionally
   * dropped — operator deferred it; the parsed value still lives in
   * row.dataset.search so the filter input keeps matching on IPs.
   * Re-introducing the column = one extra `auto` track here + the
   * matching header/data cells in page.rb and log_stream_controller. */
  grid-template-columns: auto auto auto minmax(0, 1fr);
  column-gap: 10px;
  row-gap: 0;
  align-items: baseline;
}

.log-row {
  display: contents;
}

.log-row > .log-ts,
.log-row > .log-level,
.log-row > .log-pod,
.log-row > .log-ip,
.log-row > .log-body {
  padding-top: 1px;
  padding-bottom: 1px;
  font-family: var(--voodu-font-mono);
  font-size: 12px;
  line-height: 1.7;
}

/* Leftmost-visible-cell stripe + padding —
 *
 * The row's left accent (pod-color stripe on data rows, transparent
 * on the header) lives on whichever cell is currently leftmost
 * AFTER applying the column-hide classes. When the operator hides
 * `TIME` via the visibility popover, `LVL` becomes leftmost and
 * needs to carry the stripe + 10px gutter; if `LVL` is also hidden,
 * `POD` takes over; if everything but PAYLOAD is hidden, the body
 * cell itself becomes leftmost.
 *
 * Without these cascading rules, the leftmost remaining cell would
 * butt straight against the container edge (no stripe, no padding)
 * — the symptom the operator hit when leaving only PAYLOAD on.
 *
 * Data rows pull the stripe color from `--row-accent`, a custom
 * property the JS row renderer (log_stream_controller.js) sets on
 * each row inline. Property inheritance works through `display:
 * contents` so the cell child reads the value off the row. Header
 * rows don't set the property — they get the transparent fallback
 * via the dedicated `.log-header` selectors below. */
.log-row > .log-ts,
.log-list.cols-hide-ts > .log-row > .log-level,
.log-list.cols-hide-ts.cols-hide-level > .log-row > .log-pod,
.log-list.cols-hide-ts.cols-hide-level.cols-hide-pod > .log-row > .log-body {
  border-left: 2px solid var(--row-accent, var(--voodu-border-2));
  padding-left: 10px;
}

/* Last cell is the positioning context for the floating copy
 * button (was on .log-row before display:contents). Padding-right
 * mirrors the old `.log-row { padding-right: 12px }`. */
.log-row > .log-body {
  position: relative;
  padding-right: 12px;
  min-width: 0;
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
}

.log-wrap .log-row > .log-body {
  white-space: pre-wrap;
  word-break: break-all;
  text-overflow: clip;
}

/* Selection scoping —
 *
 * Drag-to-select with the mouse used to grab the whole row (timestamp,
 * level chip, pod name, IP, AND payload). Pasting that into Slack/an
 * LLM dropped a wall of metadata before the actual log line. Locking
 * the metadata columns to `user-select: none` and keeping the body
 * `user-select: text` means a drag across any row only copies the
 * payload — no metadata noise, no quoting cleanup.
 *
 * The columns still render and stay clickable (level pill / pod chip
 * could grow click handlers later); they just don't participate in
 * text selection. */
.log-row .log-ts,
.log-row .log-level,
.log-row .log-pod,
.log-row .log-ip,
.log-row .log-copy,
.log-row .log-wrap-single {
  -webkit-user-select: none;
  user-select: none;
}
.log-row .log-body {
  -webkit-user-select: text;
  user-select: text;
}

.log-row .log-ts    { color: var(--voodu-muted-2); white-space: nowrap; }
.log-row .log-level {
  padding: 0 5px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.4px;
  display: inline-flex;
  align-items: center;
  height: 16px;
  align-self: center;
}
.log-row .log-pod   { white-space: nowrap; }
.log-row .log-ip    { color: var(--voodu-muted); white-space: nowrap; }

/* Message payload — dark keeps the per-level tint (`--log-tone`, set
 * inline by the JS row renderer); light forces a neutral near-black
 * (`--voodu-log-payload` = #333) so dense logs read cleanly on white.
 * The level stays legible via the LVL chip. The light rule is more
 * specific than the base, so it wins without `!important`. */
.log-row .log-msg { color: var(--log-tone, var(--voodu-text)); }
html[data-theme="light"] .log-row .log-msg { color: var(--voodu-log-payload); }

/* Log analytics (/logs/analytics) results loading overlay —
 *
 * The filter bar submits into the `logs-analytics-results` Turbo Frame.
 * While the request is in flight Turbo marks the frame `busy` (+
 * aria-busy) and keeps the previous results painted underneath. This
 * rule flashes the spinner overlay (Components::LogAnalytics::Results
 * #loading_overlay) on for exactly that window — hidden otherwise, so
 * the initial inline render (no fetch) shows no overlay. */
.la-results-loading { display: none; }
turbo-frame[busy] .la-results-loading,
turbo-frame[aria-busy="true"] .la-results-loading,
/* …and during the brief window after the rows render but before
 * logs-columns has applied the column layout (the grid is hidden until
 * `.cols-ready`). Reusing this overlay means the operator sees the
 * "Searching…" spinner there instead of a blank gap. */
turbo-frame:has(.log-list.la-list:not(.cols-ready)) .la-results-loading,
/* …and the Surrounding modal, whose overlay sits inside its own
 * `.la-cols-host` (it's not in a turbo-frame). */
.la-cols-host:has(.log-list.la-list:not(.cols-ready)) .la-results-loading {
  display: flex;
}

/* Pick the overlay label by state: "Searching…" while the query is in
 * flight, "Rendering…" during the post-render column-layout window. They
 * never overlap — a busy frame still shows the previous (cols-ready)
 * results underneath, so `:has(:not(.cols-ready))` is false then. */
.la-search-label,
.la-render-label { display: none; }
turbo-frame[busy] .la-search-label,
turbo-frame[aria-busy="true"] .la-search-label { display: inline; }
turbo-frame:has(.log-list.la-list:not(.cols-ready)) .la-render-label { display: inline; }

/* Analytics result rows reuse `.log-row { display: contents }` so their
 * cells flow into the shared `.log-list` grid (alignment + resize/hide).
 * A grid-participating cell can't also be size-contained, so these rows
 * can't carry `content-visibility:auto` (it broke the column tracks — the
 * cells collapsed into a stack). Correctness over the long-list paint
 * optimisation; revisit with row virtualisation if 20k lists ever jank. */

/* /logs/analytics results table — reuses the live-tail `.log-list` grid so
 * the column header + every row share ONE set of column tracks (perfect
 * alignment) and the `logs-columns` controller's resize/hide drive both
 * surfaces. Data rows are `.log-row.la-grid-row`: a SUBGRID spanning every
 * parent track (cells line up with the header + each other) while staying a
 * real box, so `content-visibility:auto` keeps a 20k-row scroll cheap.
 * Plain `.log-row` (the Follow live tail) stays `display:contents` — the
 * higher-specificity `.log-row.la-grid-row` only re-targets analytics rows. */
/* Data rows ARE `.log-row` (display:contents from the base rule), so the
 * cells below flow straight into the `.log-list` grid — same proven layout
 * as the live tail. la-grid-row only adds analytics behaviour. */
.log-row.la-grid-row { cursor: default; }

/* display:contents rows generate no box, so a row-level background can't
 * paint — tint the cells instead. Anchor = the surrounding-modal target;
 * hover = pointer feedback across the line. */
.la-grid-row.is-anchor > .log-ts,
.la-grid-row.is-anchor > .log-pod,
.la-grid-row.is-anchor > .log-body { background: var(--voodu-accent-dim); }

.la-grid-row:not(.is-anchor):hover > .log-ts,
.la-grid-row:not(.is-anchor):hover > .log-pod,
.la-grid-row:not(.is-anchor):hover > .log-body {
  background: color-mix(in srgb, var(--voodu-surface) 55%, transparent);
}

/* Per-row surrounding-logs chip — mirrors `.log-copy`, sits left of the
 * wrap chip (right: 52px). Analytics-only (the live tail has no
 * surrounding drill-in). The wrap + copy chips reuse the shared rules. */
.log-row .log-surrounding {
  position: absolute;
  top: 0px;
  right: 52px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--voodu-surface-3);
  border: 1px solid var(--voodu-border-2);
  box-shadow: var(--voodu-shadow-sm);
  color: var(--voodu-text-2);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.log-row:hover .log-surrounding {
  opacity: 1;
  pointer-events: auto;
}
.log-row .log-surrounding:hover {
  color: var(--voodu-text);
  background: color-mix(in srgb, var(--voodu-surface-3) 80%, var(--voodu-mix-fg));
  border-color: var(--voodu-border);
}
.log-row .log-surrounding svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

/* Per-row call-flow chip — the Logs→HEP3 bridge. Mirrors `.log-surrounding`
 * but sits leftmost (right: 76px) and only renders when the line carries a
 * SIP Call-ID AND the server has voodu-hep3. Hover lights it accent (not the
 * neutral surrounding tint) so the "jump to the SIP ladder" action reads as
 * a crossing-over, not another in-place drill. */
.log-row .log-callflow {
  position: absolute;
  top: 0px;
  right: 76px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--voodu-surface-3);
  border: 1px solid var(--voodu-border-2);
  box-shadow: var(--voodu-shadow-sm);
  color: var(--voodu-text-2);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.log-row:hover .log-callflow {
  opacity: 1;
  pointer-events: auto;
}
.log-row .log-callflow:hover {
  color: var(--voodu-accent-2);
  background: var(--voodu-accent-dim);
  border-color: var(--voodu-accent-line);
}
.log-row .log-callflow svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

/* Load more pagination lives INSIDE `.log-list`. The turbo-frame is
 * display:contents so the rows it swaps in flow into the grid tracks; the
 * trigger row spans every column. */
.log-list .la-page-frame { display: contents; }
.log-list .la-loadmore { grid-column: 1 / -1; }

/* First-paint grid template for the analytics table. The shared `.log-list`
 * default is 4 tracks (the live tail's TS/LVL/POD/BODY); analytics has only
 * 3 columns, so until the logs-columns controller connects and rewrites the
 * template, the browser lays the 3 columns into the 4-track default — a
 * visible "starts scrambled, snaps straight" flash. Pinning the fixed
 * 3-track template here means the very first paint is already aligned; the
 * controller then sets the same values (or the operator's saved widths).
 * Keep in sync with `defaultWidths` in Components::LogAnalytics::Results. */
.log-list.la-list {
  grid-template-columns: 256px 160px minmax(0, 1fr);
}

/* Hide the analytics grid until logs-columns has applied the real template
 * (the operator's saved widths, or the defaults). Without this a saved
 * resize visibly snaps from the CSS default on load. `visibility` (not
 * `display`) keeps the row area reserved so revealing doesn't reflow; the
 * controller adds `.cols-ready` synchronously on connect, so the first
 * VISIBLE paint is already at the right widths. A re-query's loading
 * overlay covers the gap; on first load the connect is ~1 frame away. */
.log-list.la-list:not(.cols-ready) {
  visibility: hidden;
}

/* The analytics MESSAGE header cell carries the action buttons (taller
 * than the plain text labels), so baseline-aligned cells would leave their
 * per-cell border-bottoms on a staircase. Stretch every header cell to the
 * row height and vertically centre its label — the bottom borders land on
 * one continuous line. Scoped to `.la-list` so the live tail (absolute
 * header buttons, already level) is untouched. */
.la-list .log-header > .log-hcell {
  align-self: stretch;
  display: flex;
  align-items: center;
}

/* TIME and POD run on fixed-width tracks (no `auto` measure across
 * thousands of rows), so their content must clip with an ellipsis instead
 * of overflowing into the next column. TIME's track is sized to fit the
 * full ISO-nano timestamp, so the ellipsis there is just a guard against
 * bleed — it shouldn't normally truncate. */
.la-list .log-ts,
.la-list .log-pod {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Column header —
 *
 * Sticky strip at the top of the log list. Same `display: contents`
 * trick as the data rows: the 5 header cells flow into the same outer
 * `.log-list` grid, so their widths track the data columns exactly
 * (no second grid, no manual alignment math). Each cell is
 * `position: sticky; top: 0` so the strip stays pinned while the
 * operator scrolls.
 *
 * Visual: low-key uppercase labels in a tight 22px strip — present
 * for orientation, never competing with the live data rows for
 * attention. Same monospace face so the labels sit on the same
 * vertical rhythm as the rows below. */
.log-row.log-header {
  display: contents;
}

.log-header > .log-hcell {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--voodu-bg-2);
  border-bottom: 1px solid var(--voodu-border);
  font-family: var(--voodu-font-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.7px;
  color: var(--voodu-muted-2);
  /* Vertical padding gives breathing room for the 20px chips inside
   * the body header cell (settings cog + copy-all). 7px top/bottom
   * + 20px chip = 34px header strip — still tight, more legible
   * than the previous 4px which crowded the chips against the
   * border-bottom. */
  padding-top: 7px;
  padding-bottom: 7px;
  line-height: 1.4;
  white-space: nowrap;
  -webkit-user-select: none;
  user-select: none;
}

/* First/last cell gutter — mirror the data rows' border-left padding
 * (.log-row > .log-ts) and body-right padding so the header aligns
 * pixel-perfect with the columns underneath. The 2px transparent
 * border on the first cell stands in for the data rows' colored
 * pod-accent stripe — keeps the leading-edge geometry identical. */
/* Same leftmost-cell cascade as for data rows, but on the header
 * row each rule applies a TRANSPARENT 2px border (geometry-only —
 * keeps the header pixel-aligned with the colored stripes below
 * without painting a visible color). */
.log-header > .log-h-ts,
.log-list.cols-hide-ts > .log-header > .log-h-level,
.log-list.cols-hide-ts.cols-hide-level > .log-header > .log-h-pod,
.log-list.cols-hide-ts.cols-hide-level.cols-hide-pod > .log-header > .log-h-body {
  border-left: 2px solid transparent;
  padding-left: 10px;
}
.log-header > .log-h-body {
  padding-right: 12px;
}

/* Resize handles on each non-body header cell. 4px wide grip on the
 * right edge with col-resize cursor; the JS controller starts the
 * drag on mousedown and rebuilds `.log-list`'s grid-template-columns
 * with the new explicit width as the mouse moves. Stays invisible
 * by default — only a faint accent line shows up on hover so the
 * header stays clean.
 *
 * No `position: relative` override needed on `.log-hcell` — the
 * `position: sticky` already applied above (header cells stick to
 * scroll top) is itself a positioned value, so absolute children
 * (resize handle, settings/copy chips on the body header) anchor
 * to the sticky cell naturally. Adding `position: relative` here
 * USED to override the sticky and silently broke the pinned
 * header during live tail scroll. */
.log-col-resize {
  position: absolute;
  top: 2px;
  bottom: 2px;
  right: -5px;
  width: 8px;
  cursor: col-resize;
  z-index: 3;
  user-select: none;
  -webkit-user-select: none;
}
.log-col-resize::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: transparent;
  transition: background 120ms ease;
}
.log-col-resize:hover::after,
.log-col-resize.is-dragging::after {
  background: var(--voodu-accent-line);
}
/* When any resize handle is dragging, freeze the whole viewport's
 * cursor + disable text selection so the operator's drag feels
 * sticky (no flickering between col-resize and text/I-beam). Class
 * is added on document.body by the controller for the drag's
 * duration. */
body.log-cols-resizing {
  cursor: col-resize !important;
  user-select: none;
  -webkit-user-select: none;
}
body.log-cols-resizing * {
  cursor: col-resize !important;
}

/* Header chips (copy-all + wrap + settings) inside the PAYLOAD
 * header cell — same chip language as `.log-copy` / `.log-wrap-
 * single` on data rows so the schema row feels coordinated with
 * the live tail.
 *
 * Layout (right-to-left, 24px stride: 20px chip + 4px gap):
 *   - Settings cog: right: 4px   → rightmost
 *   - Wrap toggle:  right: 28px  → middle
 *   - Copy all:     right: 52px  → leftmost of the trio
 *
 * Reading the trio left-to-right gives operator vocabulary in
 * decreasing destructiveness: "copy what's there → reformat layout
 * → change which columns I see." All 20×20 to match the per-row
 * chips.
 */
.log-col-settings,
.log-col-copy-all,
.log-col-wrap {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--voodu-surface-3);
  border: 1px solid var(--voodu-border-2);
  color: var(--voodu-muted);
  cursor: pointer;
  border-radius: 2px;
  transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.log-col-settings { right: 4px;  }
.log-col-wrap     { right: 28px; }
.log-col-copy-all { right: 52px; }

.log-col-settings:hover,
.log-col-copy-all:hover,
.log-col-wrap:hover {
  color: var(--voodu-text);
  background: color-mix(in srgb, var(--voodu-surface-3) 80%, var(--voodu-mix-fg));
  border-color: var(--voodu-border);
}
.log-col-settings[aria-expanded="true"],
.log-col-wrap[data-active="true"] {
  color: var(--voodu-accent-2);
  background: color-mix(in srgb, var(--voodu-accent) 25%, var(--voodu-surface-3));
  border-color: var(--voodu-accent-line);
}
.log-col-copy-all[data-copied="true"] {
  color: var(--voodu-green);
  border-color: color-mix(in srgb, var(--voodu-green) 55%, transparent);
  background: color-mix(in srgb, var(--voodu-green) 18%, var(--voodu-surface-3));
}
.log-col-settings svg,
.log-col-copy-all svg,
.log-col-wrap svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

/* Column visibility popover — anchored to the top-right of the
 * viewport (sibling of the scroll container, both inside the
 * `.relative .group` wrapper). Sits above sticky header (z:2),
 * jump-to-top (z:20) is below it so the popover wins. */
.log-cols-popover {
  position: absolute;
  top: 38px;
  right: 8px;
  z-index: 30;
  min-width: 180px;
  padding: 6px;
  background: var(--voodu-surface);
  border: 1px solid var(--voodu-border);
  box-shadow: var(--voodu-shadow-lg);
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.log-cols-popover[hidden] {
  display: none;
}
.log-cols-popover-title {
  padding: 4px 8px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--voodu-muted-2);
}
.log-cols-popover-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  font-size: 12px;
  color: var(--voodu-text);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}
.log-cols-popover-row:hover {
  background: var(--voodu-surface-2);
}
.log-cols-popover-row input[type="checkbox"] {
  accent-color: var(--voodu-accent);
  cursor: pointer;
}
.log-cols-popover-row.is-required {
  cursor: not-allowed;
  color: var(--voodu-muted-2);
}
.log-cols-popover-row.is-required:hover {
  background: transparent;
}
.log-cols-popover-row.is-required input[type="checkbox"] {
  cursor: not-allowed;
}
.log-cols-popover-label {
  flex: 1;
}
.log-cols-popover-hint {
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--voodu-muted-2);
}

/* Column hiding —
 *
 * With rows on `display: contents`, auto-placement fills the grid
 * left-to-right per row. Removing a column means TWO things in
 * lockstep:
 *   1. Hide the data + header cells of that column via display:none
 *      (these rules), so they don't consume grid slots.
 *   2. Rebuild `.log-list` grid-template-columns to match the
 *      remaining columns (handled by JS in logs_columns_controller).
 *
 * Doing only (1) without (2) would leave an empty trailing column
 * still taking 1fr — body would render in the wrong slot and the
 * layout would visibly split. Always change both.
 *
 * Descendant (not direct-child) so the analytics Load-more rows — which
 * nest one level deeper under the `display:contents` `.la-page-frame` —
 * are hidden too. The live tail's rows are direct grandchildren, so this
 * matches them identically. */
.log-list.cols-hide-ts .log-ts,
.log-list.cols-hide-ts .log-h-ts {
  display: none;
}
.log-list.cols-hide-level .log-level,
.log-list.cols-hide-level .log-h-level {
  display: none;
}
.log-list.cols-hide-pod .log-pod,
.log-list.cols-hide-pod .log-h-pod {
  display: none;
}

/* Copy affordance —
 *
 * Floating in the top-right of the body cell, revealed on row hover.
 * Clicking writes the body's textContent (the raw payload) to the
 * clipboard, stripping the metadata columns.
 *
 * Anchored to `.log-body` (which has position: relative) rather than
 * to the row, because the row is `display: contents` and has no box.
 * Top-right of the body cell IS the top-right of the row visually
 * since body is the rightmost grid column.
 *
 * A semi-opaque surface background masks the few characters of
 * payload text behind the icon; without it a dense JSON line would
 * bleed through. */
.log-row .log-copy {
  position: absolute;
  top: 0px;
  right: 4px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Background uses `surface-3` (#1c2330) — the brightest of the
   * surface ramp — so the chip pops clearly off the log container's
   * `bg-2` (#0c1018). The earlier `surface` (#11161f) was only ~5
   * hex points above bg-2 and read as a watermark over dense JSON.
   * Heavier shadow + slightly tighter border completes the lift. */
  background: var(--voodu-surface-3);
  border: 1px solid var(--voodu-border-2);
  box-shadow: var(--voodu-shadow-sm);
  color: var(--voodu-text-2);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.log-row:hover .log-copy {
  opacity: 1;
  pointer-events: auto;
}
.log-row .log-copy:hover {
  color: var(--voodu-text);
  background: color-mix(in srgb, var(--voodu-surface-3) 80%, var(--voodu-mix-fg));
  border-color: var(--voodu-border);
}
.log-row .log-copy[data-copied="true"] {
  color: var(--voodu-green);
  border-color: color-mix(in srgb, var(--voodu-green) 55%, transparent);
  background: color-mix(in srgb, var(--voodu-green) 18%, var(--voodu-surface-3));
  opacity: 1;
}
.log-row .log-copy svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

/* Per-row wrap toggle —
 *
 * Mirrors `.log-copy` (size, hover-reveal, accent on active) but sits
 * 24px to the LEFT (right: 28px so there's a 4px gap between the two
 * chips). Clicking flips `.log-row-wrap` on the row, which the rule
 * below overrides into pre-wrap / break-all for THIS row's body only.
 *
 * `data-active="true"` keeps the chip visible after the cursor leaves
 * — otherwise wrapping a row would strand the operator (chip hidden,
 * can't click again to un-wrap without re-finding the right row). */
.log-row .log-wrap-single {
  position: absolute;
  top: 0px;
  right: 28px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Same surface-3 + heavy-shadow recipe as .log-copy — chip pops
   * off the dark log container with no JSON text bleeding through. */
  background: var(--voodu-surface-3);
  border: 1px solid var(--voodu-border-2);
  box-shadow: var(--voodu-shadow-sm);
  color: var(--voodu-text-2);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.log-row:hover .log-wrap-single,
.log-row .log-wrap-single[data-active="true"] {
  opacity: 1;
  pointer-events: auto;
}
.log-row .log-wrap-single:hover {
  color: var(--voodu-text);
  background: color-mix(in srgb, var(--voodu-surface-3) 80%, var(--voodu-mix-fg));
  border-color: var(--voodu-border);
}
/* Active state — a translucent accent-dim used to let JSON text
 * bleed through the chip when wrap was on. Swap to a SOLID mix of
 * accent into surface-3 so the chip stays a tangible accent-tinted
 * square regardless of what payload sits underneath. */
.log-row .log-wrap-single[data-active="true"] {
  color: var(--voodu-accent-2);
  border-color: var(--voodu-accent-line);
  background: color-mix(in srgb, var(--voodu-accent) 38%, var(--voodu-surface-3));
}
.log-row .log-wrap-single svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

/* Per-row wrap override —
 *
 * Flips the body of ONE row into wrap mode while the rest of the
 * viewport stays in the current global mode. Specificity beats the
 * default `.log-row > .log-body` rule (class chain on the row) but
 * not the global `.log-wrap` (operator-driven, applied on the parent
 * list), so the global toggle still wins if both are on — harmless
 * since both produce the same wrap behaviour. */
.log-row.log-row-wrap > .log-body {
  white-space: pre-wrap;
  word-break: break-all;
  text-overflow: clip;
}

/* ── Live indicator dot ──
 *
 * Used inside the Follow toggle in the log viewer. The dot's color
 * + animation reflect whether the parent button is active (live) or
 * not — driven by the [data-active] attribute the JS already flips
 * in refreshToggleButton, so this is pure CSS state.
 *
 * Active   → green, pulsing — signals "watching live."
 * Inactive → muted, static  — signals "follow paused / off."
 *
 * Sits as a sibling rule to the chip's class swap so the same
 * `data-active` toggle drives both the chrome AND the dot, without
 * the JS having to know about a second target.
 */
.voodu-live-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 9999px;
  background: var(--voodu-muted-2);
  box-shadow: none;
  animation: none;
}

[data-active="true"] .voodu-live-dot {
  background: var(--voodu-green);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--voodu-green) 18%, transparent);
  animation: voodu-pulse 2.4s ease-in-out infinite;
}

/* ── Animations ──
 *
 *  voodu-pulse — used by StatusDot for live states (running, online,
 *  restarting). Subtle opacity breathe rather than a scale pop so it
 *  reads as "alive" without grabbing the eye.
 *
 *  voodu-spin  — used by Spinner. Standard linear rotation.
 *
 *  Exposed as Tailwind utilities via @theme below (`animate-voodu-*`).
 */
@keyframes voodu-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

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

:root {
  /* ── Backgrounds (deep → surfaces) ── */
  --voodu-bg:        #0a0d14;
  --voodu-bg-2:      #0c1018;
  --voodu-surface:   #11161f;
  --voodu-surface-2: #161c27;
  --voodu-surface-3: #1c2330;
  --voodu-surface-side: #181f2b; /* secondary sidebar (panels rail) */

  /* ── Borders ── */
  --voodu-border:    #1f2632;
  --voodu-border-2:  #29303f;

  /* ── Text ── */
  --voodu-text:    #e8e8ee;
  --voodu-text-2:  #b4b4c0;
  --voodu-muted:   #7a7a88;
  --voodu-muted-2: #5a5a66;

  /* Log payload text — dark uses the bright body text; light overrides
   * to a neutral near-black (see light block) so a wall of logs reads
   * cleanly on white. */
  --voodu-log-payload: var(--voodu-text);

  /* ── Accent: Voodu brand green (was purple — see header) ──
   * Unified with the status green: one green for "action / active /
   * healthy" (CTA, active tab/nav, selected chips, focus). */
  --voodu-accent:      #34d399;
  --voodu-accent-2:    #6ee7b7;
  --voodu-accent-dim:  #34d39922;
  --voodu-accent-line: #34d39966;

  /* ── Link: blue ──
   * Navigational anchors (pod names, breadcrumbs). Deliberately NOT
   * the accent green, so "navigate here" never reads as "healthy /
   * action" next to a green status pill. */
  --voodu-link:   #60a5fa;
  --voodu-link-2: #93c5fd;

  /* ── Semantic / status (traffic-light family) ── */
  --voodu-green:     #34d399;
  --voodu-green-dim: #34d39922;
  --voodu-amber:     #fbbf24;
  --voodu-amber-dim: #fbbf2422;
  --voodu-red:       #f87171;
  --voodu-red-dim:   #f8717122;
  --voodu-blue:      #60a5fa;

  /* ── Chart palette (extends the semantic colors above) ──
   *
   * Every observable metric gets its OWN color — operators
   * cross-page should be able to glance at a sparkline tint
   * and know which signal it is without reading the label.
   *
   * RULE: --voodu-red is reserved for errors / failures.
   * Never use it for a happy-path metric.
   *
   * Assignments live in app/services/*_data.rb (search for
   * "var(--voodu-" — one color per metric, single source of
   * truth in the chart_specs lists).
   */
  --voodu-teal:    #2dd4bf; /* Disk usage */
  --voodu-cyan:    #22d3ee; /* Block I/O */
  --voodu-indigo:  #818cf8; /* Net Tx (outbound) */
  --voodu-orange:  #fb923c; /* HTTP Requests */
  --voodu-pink:    #ec4899; /* HTTP Bytes Out */
  --voodu-purple:  #7c5cff; /* CPU — was the old --voodu-accent hue; decoupled
                               so chrome can go brand-green while CPU keeps a
                               distinct chart color (1 metric = 1 color). */

  /* HTTP latency tail family — sister tones to amber (p95).
   * Gold reads as "moderate tail" (p90), violet as "rare extreme"
   * (p99). Both visually distinct from amber so a p95 chart never
   * gets mistaken for a p99 one in cross-page glances. */
  --voodu-gold:    #eab308; /* HTTP p90 Latency */
  --voodu-violet:  #a855f7; /* HTTP p99 Latency */

  /* HTTP status families.
   * sky = informational / redirect (3xx) — bluer than --voodu-blue
   * which is Memory, so the two never read as the same signal.
   * rose = client error (4xx) — in the red family per the RED-FOR-
   * ERRORS rule above, but distinguishably warmer than --voodu-red
   * (5xx), signalling "less severe than a server failure". */
  --voodu-sky:     #38bdf8; /* HTTP 3xx */
  --voodu-rose:    #fb7185; /* HTTP 4xx */

  /* ── Radius ──
   *
   * Voodu leans square — but not razor-sharp. A subtle 2px softens
   * every rectangle (cards, buttons, inputs, pills, kbd chips, tabs,
   * bars) without going "consumer-app" round; large surfaces get 4px.
   * Only the geometrically circular pieces (StatusDot, Avatar, dot
   * indicators) get `rounded-full`, Tailwind's native utility,
   * unaffected by these tokens.
   *
   * Applies to both themes. Bump a token here for softer corners —
   * never override at the call site.
   */
  --voodu-r-sm: 2px;
  --voodu-r-md: 2px;
  --voodu-r-lg: 4px;

  /* ── Interaction (theme-flippable: dark mixes toward white, light toward black) ── */
  --voodu-hover:     #ffffff08; /* subtle row / menu hover overlay */
  --voodu-mix-fg:    #ffffff;   /* color the chip hovers mix toward */
  --voodu-on-accent: #ffffff;   /* text on accent-filled buttons */

  /* ── Shadow ── */
  --voodu-shadow-inset: 0 1px 0 0 rgba(255, 255, 255, 0.025) inset;
  --voodu-shadow-pop:   0 8px 24px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--voodu-border);
  --voodu-shadow-sm:    0 2px 5px rgba(0, 0, 0, 0.55);
  --voodu-shadow-lg:    0 8px 24px rgba(0, 0, 0, 0.55);
  --voodu-shadow-drawer: -12px 0 32px rgba(0, 0, 0, 0.55);
  /* Header lift — the (always-dark) topbar casts a soft shadow onto the
   * content below so it reads as a layer above, not glued to the page. */
  --voodu-shadow-header: 0 4px 12px rgba(0, 0, 0, 0.45);

  /* ── Fonts ── */
  --voodu-font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --voodu-font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;

  /* ── Base sizing ── */
  --voodu-font-size-base: 13px;
  --voodu-line-height:    1.45;
}

/* ── Light theme ──
 *
 * Overrides the dark base above when html[data-theme="light"]. The
 * no-flash inline script (application.html.erb) resolves
 * localStorage > prefers-color-scheme and sets the attribute before
 * first paint; theme_controller.js flips it on toggle. Dark stays the
 * :root default, so data-theme="dark" falls straight through to it.
 *
 * Same brand hues — accent purple is unchanged; only lightness is
 * remapped (deep navy → off-white surfaces, light text → near-black)
 * and the low-contrast semantic / chart tones are darkened so they
 * stay legible on white. mix-fg flips to near-black so the chip hovers
 * darken instead of lighten; shadows go soft slate.
 */
html[data-theme="light"] {
  /* Supabase-style surface hierarchy: neutral near-white CHROME
   * (bg/bg-2) under WHITE panels (surface). The lift now comes from
   * the bg↔surface contrast, NOT the border — so cards read as
   * distinct without heavy separators. Earlier bg==surface==#fff made
   * a card exist only by its border. Neutral grey, no blue tint. */
  --voodu-bg:        #f7f8fa;
  --voodu-bg-2:      #f3f4f6;
  --voodu-surface:   #ffffff;
  --voodu-surface-2: #f6f7f9;
  --voodu-surface-3: #eef0f3;
  --voodu-surface-side: #fafafa; /* secondary sidebar (panels rail) */

  /* Borders are soft hairlines — the bg↔surface contrast carries the
   * hierarchy (Supabase model), so separators only need to whisper.
   * Heavier lines made tables/cards feel boxed-in and "grey". */
  --voodu-border:    #e8eaee;
  --voodu-border-2:  #d9dbe1;

  /* Neutral text ramp (de-blued from the previous slate): near-black
   * primary, intentional greys for secondary/muted. */
  --voodu-text:    #16191f;
  --voodu-text-2:  #3d434d;
  --voodu-muted:   #5c636e;
  --voodu-muted-2: #868d99;
  /* Log body text — near-black on white, matching CloudWatch's crisp
   * black @message column (a slate-grey here read as washed-out next to
   * it). Scoped to log payload only; app chrome (--voodu-text) stays
   * the softer #1b212b so headings/labels don't turn harsh. */
  --voodu-log-payload: #0e1116;

  /* Accent green for light — same hue as --voodu-green below (the
   * unified brand/status green); accent-2 darker for hover + text. */
  --voodu-accent:      #059669;
  --voodu-accent-2:    #047857;
  --voodu-accent-dim:  #05966918;
  --voodu-accent-line: #05966955;

  /* Link blue — darker for legibility on the near-white chrome. */
  --voodu-link:   #0b6bcb;
  --voodu-link-2: #084d96;

  --voodu-green:     #059669;
  --voodu-green-dim: #05966918;
  --voodu-amber:     #d97706;
  --voodu-amber-dim: #d9770618;
  --voodu-red:       #dc2626;
  --voodu-red-dim:   #dc262618;
  --voodu-blue:      #2563eb;

  /* Chart palette — same hues, darkened for white-bg contrast. */
  --voodu-teal:   #0d9488;
  --voodu-cyan:   #0891b2;
  --voodu-indigo: #6366f1;
  --voodu-orange: #ea580c;
  --voodu-pink:   #db2777;
  --voodu-gold:   #ca8a04;
  --voodu-violet: #9333ea;
  --voodu-sky:    #0284c7;
  --voodu-rose:   #e11d48;
  --voodu-purple: #7c5cff; /* CPU — kept identical to dark (reads fine on white) */

  /* Interaction + shadows for light. */
  --voodu-hover:        #00000008;
  --voodu-mix-fg:       #0a0d14;
  --voodu-on-accent:    #ffffff;
  --voodu-shadow-inset: 0 1px 0 0 rgba(255, 255, 255, 0.6) inset;
  --voodu-shadow-pop:   0 10px 28px rgba(15, 23, 42, 0.12), 0 0 0 1px var(--voodu-border);
  --voodu-shadow-sm:    0 1px 3px rgba(15, 23, 42, 0.12);
  --voodu-shadow-lg:    0 10px 28px rgba(15, 23, 42, 0.14);
  --voodu-shadow-drawer: -10px 0 30px rgba(15, 23, 42, 0.1);
  /* Slate lift for the dark topbar over the light content. A crisp 1-2px
   * layer reads as a defined edge; the softer spread gives depth. Needs
   * more punch than a dark-on-dark shadow to register on the light bg. */
  --voodu-shadow-header: 0 1px 2px rgba(15, 23, 42, 0.14), 0 8px 22px -2px rgba(15, 23, 42, 0.24);
}

/* ── Force-dark scope (.voodu-dark) ──
 *
 * AWS-console pattern: the topbar stays dark regardless of the page
 * theme, so it reads as a fixed chrome bar above the themed sidebar +
 * content (nice contrast in light mode). We re-pin the dark palette on
 * any `.voodu-dark` subtree, but ONLY under light — in dark mode the
 * :root defaults already apply, so this rule is unneeded there.
 *
 * Specificity matters: `html[data-theme="light"] .voodu-dark` (0,2,1)
 * must beat the light override `html[data-theme="light"]` (0,1,1), or
 * the light values would win inside the topbar. Keep the values in
 * lockstep with the :root dark block above. */
html[data-theme="light"] .voodu-dark {
  --voodu-bg:        #0a0d14;
  --voodu-bg-2:      #0c1018;
  --voodu-surface:   #11161f;
  --voodu-surface-2: #161c27;
  --voodu-surface-3: #1c2330;
  --voodu-surface-side: #181f2b; /* secondary sidebar (panels rail) */

  --voodu-border:    #1f2632;
  --voodu-border-2:  #29303f;

  --voodu-text:    #e8e8ee;
  --voodu-text-2:  #b4b4c0;
  --voodu-muted:   #7a7a88;
  --voodu-muted-2: #5a5a66;
  --voodu-log-payload: #e8e8ee;

  --voodu-accent:      #34d399;
  --voodu-accent-2:    #6ee7b7;
  --voodu-accent-dim:  #34d39922;
  --voodu-accent-line: #34d39966;

  --voodu-link:   #60a5fa;
  --voodu-link-2: #93c5fd;

  --voodu-green:     #34d399;
  --voodu-green-dim: #34d39922;
  --voodu-amber:     #fbbf24;
  --voodu-amber-dim: #fbbf2422;
  --voodu-red:       #f87171;
  --voodu-red-dim:   #f8717122;
  --voodu-blue:      #60a5fa;

  --voodu-hover:     #ffffff08;
  --voodu-mix-fg:    #ffffff;

  /* Re-map the Tailwind @theme tokens too. The utilities (bg-voodu-*,
   * text-voodu-*, border-voodu-*) read --color-voodu-*, which is
   * declared only on :root — so its `var(--voodu-*)` was resolved
   * against :root's LIGHT values and wouldn't pick up the dark
   * overrides above. Re-declaring it here re-resolves against this
   * subtree's dark --voodu-* (set just above), so the utilities go
   * dark too. Without this the topbar stayed white in light mode. */
  --color-voodu-bg:        var(--voodu-bg);
  --color-voodu-bg-2:      var(--voodu-bg-2);
  --color-voodu-surface:   var(--voodu-surface);
  --color-voodu-surface-2: var(--voodu-surface-2);
  --color-voodu-surface-3: var(--voodu-surface-3);
  --color-voodu-surface-side: var(--voodu-surface-side);
  --color-voodu-border:    var(--voodu-border);
  --color-voodu-border-2:  var(--voodu-border-2);
  --color-voodu-text:        var(--voodu-text);
  --color-voodu-text-2:      var(--voodu-text-2);
  --color-voodu-muted:       var(--voodu-muted);
  --color-voodu-muted-2:     var(--voodu-muted-2);
  --color-voodu-log-payload: var(--voodu-log-payload);
  --color-voodu-accent:      var(--voodu-accent);
  --color-voodu-accent-2:    var(--voodu-accent-2);
  --color-voodu-accent-dim:  var(--voodu-accent-dim);
  --color-voodu-accent-line: var(--voodu-accent-line);
  --color-voodu-link:        var(--voodu-link);
  --color-voodu-link-2:      var(--voodu-link-2);
  --color-voodu-hover:     var(--voodu-hover);
  --color-voodu-green:     var(--voodu-green);
  --color-voodu-green-dim: var(--voodu-green-dim);
  --color-voodu-amber:     var(--voodu-amber);
  --color-voodu-amber-dim: var(--voodu-amber-dim);
  --color-voodu-red:       var(--voodu-red);
  --color-voodu-red-dim:   var(--voodu-red-dim);
  --color-voodu-blue:      var(--voodu-blue);
}

/* ── Tailwind @theme mapping ──
 * Surfaces every CSS var as a Tailwind utility token. Pattern: a CSS
 * var named `--voodu-foo` becomes `--color-voodu-foo` here, which
 * Tailwind 4 picks up to generate `bg-voodu-foo`, `text-voodu-foo`,
 * `border-voodu-foo`, etc. Keep the lists in lockstep with :root above.
 */
@theme {
  /* Backgrounds */
  --color-voodu-bg:        var(--voodu-bg);
  --color-voodu-bg-2:      var(--voodu-bg-2);
  --color-voodu-surface:   var(--voodu-surface);
  --color-voodu-surface-2: var(--voodu-surface-2);
  --color-voodu-surface-3: var(--voodu-surface-3);
  --color-voodu-surface-side: var(--voodu-surface-side);

  /* Borders */
  --color-voodu-border:   var(--voodu-border);
  --color-voodu-border-2: var(--voodu-border-2);

  /* Text */
  --color-voodu-text:        var(--voodu-text);
  --color-voodu-text-2:      var(--voodu-text-2);
  --color-voodu-muted:       var(--voodu-muted);
  --color-voodu-muted-2:     var(--voodu-muted-2);
  --color-voodu-log-payload: var(--voodu-log-payload);

  /* Accent */
  --color-voodu-accent:      var(--voodu-accent);
  --color-voodu-accent-2:    var(--voodu-accent-2);
  --color-voodu-accent-dim:  var(--voodu-accent-dim);
  --color-voodu-accent-line: var(--voodu-accent-line);

  /* Link */
  --color-voodu-link:   var(--voodu-link);
  --color-voodu-link-2: var(--voodu-link-2);

  /* Interaction */
  --color-voodu-hover:     var(--voodu-hover);
  --color-voodu-on-accent: var(--voodu-on-accent);

  /* Semantic */
  --color-voodu-green:     var(--voodu-green);
  --color-voodu-green-dim: var(--voodu-green-dim);
  --color-voodu-amber:     var(--voodu-amber);
  --color-voodu-amber-dim: var(--voodu-amber-dim);
  --color-voodu-red:       var(--voodu-red);
  --color-voodu-red-dim:   var(--voodu-red-dim);
  --color-voodu-blue:      var(--voodu-blue);

  /* Radius */
  --radius-voodu-sm: var(--voodu-r-sm);
  --radius-voodu-md: var(--voodu-r-md);
  --radius-voodu-lg: var(--voodu-r-lg);

  /* Shadow */
  --shadow-voodu-header: var(--voodu-shadow-header);

  /* Fonts */
  --font-voodu-sans: var(--voodu-font-sans);
  --font-voodu-mono: var(--voodu-font-mono);

  /* Animations */
  --animate-voodu-pulse: voodu-pulse 2.4s ease-in-out infinite;
  --animate-voodu-spin:  voodu-spin 0.9s linear infinite;

  /* ── Custom breakpoints (match the inspiration's media queries) ──
   *
   * The dashboard chrome flips at two specific widths the design
   * was sized around:
   *
   *   vmd = 1100px   below: drawer sidebar + card list of pods.
   *                  at/above: in-flow sidebar + pods table.
   *
   *   vlg = 1280px   below: topbar drops region/uptime chips and
   *                  the Cmd-K hint to keep the bar single-line.
   *                  at/above: full topbar with all chips.
   *
   * Tailwind 4 picks these up automatically — `vmd:flex`,
   * `hidden vmd:flex`, etc. — without touching the default
   * `md`/`lg`/`xl` prefixes (which the styleguide still uses).
   *
   * Names are intentionally hyphen-free; Tailwind 4 doesn't reliably
   * emit utilities for breakpoint keys that contain a hyphen.
   */
  --breakpoint-vmd: 1100px;
  --breakpoint-vlg: 1280px;
}

/* ── Hover tooltip ──────────────────────────────────────────────────
 *
 * A CSS-only tooltip: add `data-tooltip="…"` to any element and a
 * styled bubble appears on hover AND keyboard focus (focus-visible),
 * with no JS and no native-`title` delay. Keep an `aria-label` for
 * screen readers — `data-tooltip` is the VISUAL layer only.
 *
 * Positioned BELOW the element (the icon buttons that use it sit at
 * the top of a panel, where an above-tooltip would clip). nowrap so
 * short labels stay one line; pointer-events:none so the bubble never
 * eats the hover.
 */
[data-tooltip] {
  position: relative;
}

[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(-2px);
  z-index: 60;
  white-space: nowrap;
  padding: 4px 7px;
  font-size: 11px;
  line-height: 1;
  font-weight: 500;
  color: var(--voodu-text);
  background: var(--voodu-surface-2);
  border: 1px solid var(--voodu-border-2);
  border-radius: 5px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, transform 120ms ease;
}

[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ── JSON code editor (textarea + highlighted overlay) ──────────────
 *
 * Route B from the body-template editor decision: keep a REAL <textarea>
 * (native caret, selection, undo, form submission) and paint a
 * syntax-highlighted layer behind it. The two layers MUST share the same
 * font metrics, padding and wrapping or the colored text drifts out from
 * under the typed text — hence the shared rule block below.
 *
 * No library: ~1KB of our own highlighter (json_editor_controller.js)
 * covers JSON + our {{token}} / | filter markers, the only grammar we
 * need. Prism would drag in N languages we'd never use.
 */
.voodu-code {
  position: relative;
}

.voodu-code__hl,
.voodu-code__input,
.voodu-code__gutter {
  margin: 0;
  border: 0;
  font-family: var(--voodu-font-mono);
  font-size: 12px;
  line-height: 1.6;
  letter-spacing: normal;
  tab-size: 2;
}

/* Soft-wrap (long lines wrap instead of scrolling horizontally — far
 * nicer to edit). Left padding clears the gutter. The highlight's RIGHT
 * padding is nudged in JS to match the textarea's scrollbar width so the
 * two layers wrap at the exact same column. */
.voodu-code__hl,
.voodu-code__input {
  padding: 8px 12px 8px 46px;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
}

/* Highlighted layer: behind, fills the box, never interactive. One block
 * per logical line so each line's wrapped height can be measured to
 * position the matching gutter number. */
.voodu-code__hl {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* Purely decorative — it mirrors the textarea's glyphs behind it. It must
   * never take a selection of its own: a document-level select-all (or any
   * stray range) would otherwise paint the global ::selection over this layer,
   * a green block layered on top of the colored tokens that reads as a broken
   * highlight. Only the real textarea's selection should ever show. */
  user-select: none;
  color: var(--voodu-text-2);
  background: transparent;
}

.voodu-code__line {
  display: block;
  min-height: 1.6em;
}

/* Line-number gutter — pinned left, scrolls vertically with the text
 * (JS-synced), never horizontally. Same metrics as the text layers so
 * row N lines up. Sits above the highlight, below the textarea. */
.voodu-code__gutter {
  position: absolute;
  inset: 0 auto 0 0;
  width: 38px;
  padding: 8px 6px 8px 0;
  overflow: hidden;
  text-align: right;
  color: var(--voodu-muted-2);
  background: var(--voodu-surface-2);
  border-right: 1px solid var(--voodu-border);
  pointer-events: none;
  user-select: none;
}

/* One cell per logical line; its height is set in JS to the wrapped
 * height of the matching line, so the number sits at the line's top row
 * and wrapped continuation rows get blank gutter space. */
.voodu-code__gnum {
  display: block;
  line-height: 1.6;
  overflow: hidden;
}

/* Textarea overlays it: transparent glyphs (colors show through from
 * below), but a visible caret and native selection. */
.voodu-code__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  resize: none;
  overflow: hidden auto;
  color: transparent;
  background: transparent;
  caret-color: var(--voodu-text);
  outline: none;
}

.voodu-code__input::placeholder {
  color: var(--voodu-muted-2);
}

/* Selection wash for the code editor. A DIRECT rgba — never color-mix(): the
 * build (LightningCSS) can't fold a color-mix() over a CSS var at compile time,
 * so it emits an OPAQUE `var(--voodu-accent)` fallback, and that bright-green
 * fallback was painting SOLID green blocks over the code whenever you selected
 * text. A plain rgba has no fallback split, and a NEUTRAL slate never smears the
 * green string tokens (green-on-green read as broken). Works on both the dark
 * and light editor surface. */
.voodu-code__input::selection {
  background: rgba(120, 140, 172, 0.45);
}

/* Brief red flag when Format hits invalid JSON. */
.voodu-code--invalid {
  border-color: var(--voodu-red) !important;
}

/* Token colors — one signal per token, muted punctuation, our {{vars}}
 * in accent so operators spot the substitution points at a glance. */
.voodu-code .tok-key  { color: var(--voodu-blue); }
.voodu-code .tok-str  { color: var(--voodu-green); }
.voodu-code .tok-num  { color: var(--voodu-amber); }
.voodu-code .tok-lit  { color: var(--voodu-orange); }
.voodu-code .tok-punc { color: var(--voodu-muted); }
.voodu-code .tok-var  { color: var(--voodu-accent-2); font-weight: 600; }
/* Regex literals in the log-query DSL — distinct hue from quoted strings
 * so `/re/` and "text" read apart at a glance. */
.voodu-code .tok-re   { color: var(--voodu-pink); }
/* Pipeline structure (the `filter` command + the `|` pipe) — purple, bold,
 * so the CloudWatch-style stage boundaries pop above the clause keywords. */
.voodu-code .tok-cmd  { color: var(--voodu-purple); font-weight: 600; }

/* Query-editor variant (analytics filter): no line-number gutter, so the
 * text layers start at the normal left padding instead of clearing 46px. */
.voodu-code--query .voodu-code__hl,
.voodu-code--query .voodu-code__input {
  padding-left: 12px;
}
