/**
 * Стили интерфейса тенанта.
 *
 * Общее с админкой вынесено в web-ui-core (/static/shared/css/app-core.css)
 * и подключается раньше этого файла.
 */

/* Discovered-asset tree (workspace) — nested, collapsible, with guide lines */
.ws-tree-root,
.ws-children {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ws-children {
  margin-left: 0.5rem;
  padding-left: 0.75rem;
}

.ws-node > .ws-row {
  position: relative;
  padding: 0.05rem 0;
}

/* Vertical guide line: drawn per child so it never dangles past the last one.
   A non-last child spans its full height (joining the next sibling below its
   subtree); the last child stops at its connection tick, so a node with a
   single child no longer trails a line down into its sibling. */
.ws-children > .ws-node {
  position: relative;
}
.ws-children > .ws-node::before {
  content: "";
  position: absolute;
  left: -0.75rem;
  top: 0;
  bottom: 0;
  border-left: 1px solid var(--bs-border-color);
}
.ws-children > .ws-node:last-child::before {
  bottom: auto;
  height: 0.85rem;
}

/* Horizontal tick joining a child row to the parent's vertical guide line. */
.ws-children > .ws-node > .ws-row::before {
  content: "";
  position: absolute;
  left: -0.75rem;
  top: 0.85rem;
  width: 0.55rem;
  border-top: 1px solid var(--bs-border-color);
}

.ws-node.collapsed > .ws-children {
  display: none;
}

.ws-toggle .bi {
  transition: transform 0.15s;
  font-size: 0.75rem;
}

.ws-node.collapsed > .ws-row .ws-toggle .bi {
  transform: rotate(-90deg);
}

/* Leaf rows align with rows that have a caret. */
.ws-leaf-spacer {
  display: inline-block;
  width: 1rem;
}

/* Selected asset rows (run picker) are highlighted. */
.ws-node.ws-selected > .ws-row {
  background-color: rgba(var(--bs-primary-rgb), 0.12);
  border-radius: 0.25rem;
}

/* Sidebar scenario switcher: the current scenario is highlighted and not a
   link (rendered as a <span>), so it reads as "you are here". */
.ws-scenario-nav .nav-link.active {
  background-color: rgba(var(--bs-primary-rgb), 0.15);
  border-radius: 0.25rem;
  cursor: default;
}

/* Stage graph: a full-width map. The viewport is a fixed-size window; the inner
   canvas (dagre-laid-out HTML node cards + an SVG edge layer) is translated and
   scaled inside it, so the DAG can grow without forcing a scroll. */
.ws-stage-graph {
  /* Цвет рёбер. Рисовались цветом рамки — в тёмной теме это #26292e на фоне
     #0b0c0e: линия толщиной 1.5px сливалась с полотном, и карта читалась как
     россыпь карточек без связей. Своя переменная, а не общая рамка: рамкам
     карточек добавочная яркость не нужна, а связи — главное на этой картинке. */
  --ws-graph-edge: var(--bs-border-color);
  position: relative;
  overflow: hidden;
  /* 17rem squeezed a whole pipeline into ~270px, so the graph auto-zoomed to
     illegible. The drag handle below still resizes it; this is just a default
     that fits a real scenario without fighting it. */
  height: var(--ws-graph-h, 26rem);
  cursor: grab;
  touch-action: none;
}
[data-bs-theme="dark"] .ws-stage-graph {
  --ws-graph-edge: #6b7280;
}
/* Drag handle under the graph: grows/shrinks the graph viewport height. */
.ws-graph-hhandle {
  height: 0.6rem;
  margin: 0.25rem 0 0;
  position: relative;
  cursor: row-resize;
  border-radius: 3px;
}
.ws-graph-hhandle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 45%;
  right: 45%;
  height: 2px;
  transform: translateY(-50%);
  background: var(--bs-border-color);
  border-radius: 2px;
}
.ws-graph-hhandle:hover::before,
.ws-graph-hhandle.ws-ah-drag::before {
  background: var(--bs-primary);
}
.ws-stage-graph.ws-grabbing {
  cursor: grabbing;
}
.ws-graph-canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
/* Zoom controls, pinned to a corner of the viewport (above the canvas). */
.ws-graph-ctl {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  z-index: 3;
}
.ws-graph-ctl .btn {
  width: 1.9rem;
  padding: 0.1rem 0;
  line-height: 1.2;
  /* btn-light is always white, which glares against the dark theme — pin the
     colours to theme variables so the controls blend into the surface. */
  background-color: var(--bs-body-bg);
  border-color: var(--bs-border-color);
  color: var(--bs-body-color);
}
.ws-graph-ctl .btn:hover {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-body-color);
}
.ws-graph-edges {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
}
.ws-graph-edge {
  fill: none;
  stroke: var(--ws-graph-edge);
  stroke-width: 1.5;
  marker-end: url(#wsArrow);
}
/* Feedback edge: the data goes back to a level the pipeline already passed
   (tlsx turns a port back into a domain to check). Dashed, so a line running
   right-to-left reads as intentional rather than as a routing mistake. */
.ws-graph-edge-back {
  stroke-dasharray: 5 4;
}
.ws-graph-edges #wsArrow path {
  fill: var(--ws-graph-edge);
}
/* One stage = one row inside its input type's node. Relative, not absolute:
   the row is laid out by the type node, and only the progress bar is pinned. */
.ws-gnode {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.4rem 0.25rem;
  font-size: 0.78rem;
  border-top: 1px solid var(--bs-border-color);
  cursor: default;
}
.ws-gnode:first-child {
  border-top: 0;
}
.ws-gnode:hover {
  background: var(--bs-tertiary-bg);
}
.ws-gnode > .ws-gnode-cb {
  flex: 0 0 auto;
  width: 0.8rem;
  height: 0.8rem;
}
/* Produces the type it consumes (js-files-scan: url → url). A line back into
   the node it left says nothing; the badge does. */
.ws-gnode-self {
  color: var(--bs-secondary-color);
  font-size: 0.7rem;
}
.ws-gnode-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  flex: 0 0 auto;
}
.ws-gnode-name {
  font-weight: 600;
  white-space: nowrap;
  margin-right: auto;
}
.ws-gnode-stats {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.7rem;
  white-space: nowrap;
}
.ws-gnode-stat i {
  font-size: 0.7rem;
}
/* A hairline under the row, not a rule across it: forty full-width bars turn
   into forty separators and the rows stop reading as one list. */
.ws-gnode-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  overflow: hidden;
  opacity: 0.7;
}
.ws-gnode-bar-fill {
  height: 100%;
  width: 0;
  transition: width 0.3s ease;
}
/* Six buttons on forty rows is chrome, not information: they appear on the row
   the pointer (or the keyboard) is actually on. */
.ws-gnode-btns {
  margin-left: 0.3rem;
  opacity: 0;
  transition: opacity 0.1s;
}
.ws-gnode:hover .ws-gnode-btns,
.ws-gnode-btns:focus-within {
  opacity: 1;
}
.ws-gnode-btns .btn {
  --bs-btn-font-size: 0.65rem;
}
.ws-gnode-off {
  opacity: 0.45;
}
/* Cross-card focus highlight (amber) — distinct from the blue compose-select.
   Applied to related stage nodes / asset rows / run rows on hover. */
.ws-gnode.ws-hi {
  box-shadow: 0 0 0 2px var(--bs-warning);
}
.ws-node.ws-hi > .ws-row,
.ws-run-row.ws-hi {
  background-color: rgba(var(--bs-warning-rgb), 0.22);
  border-radius: 0.25rem;
}
/* Hovering an asset lights up its type's node on the graph (replaces the
   removed type/"via" badges). */
.ws-gtype.ws-hi {
  box-shadow: 0 0 0 2px var(--bs-warning);
}
.ws-run-row {
  cursor: pointer;
}
/* The run currently driving the Reports card. */
.ws-run-row.ws-run-selected {
  background-color: rgba(var(--bs-info-rgb), 0.18);
  border-radius: 0.25rem;
  box-shadow: inset 3px 0 0 var(--bs-info);
}
/* A stage that failed in the run whose errors are shown. */
.ws-gnode.ws-gnode-error {
  border-color: var(--bs-danger) !important;
  box-shadow: 0 0 0 2px rgba(var(--bs-danger-rgb), 0.4);
}
/* History filter chips (click to remove a stage from the filter). */
.ws-hist-chip {
  cursor: pointer;
  line-height: 1;
}

/* ── Type node ───────────────────────────────────────────────────────────
   One asset type, named once, with the stages that consume it as rows. It used
   to be drawn twice — a "layer" card keyed by the stage's input type AND a
   bridging-type hub on the edges between them — which is why every arrow ran
   into a box repeating the label it had just passed. */
.ws-gtype {
  position: absolute;
  z-index: 1;
  min-width: 11rem;
  max-width: 20rem;
  margin: 0;
  font-size: 0.8rem;
  background: var(--bs-body-bg);
}
.ws-gtype:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}
.ws-gtype-head {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
}
.ws-gtype-toggle {
  color: var(--bs-secondary-color);
  line-height: 1;
  text-decoration: none;
}
.ws-gtype-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ws-gtype-nstages {
  color: var(--bs-secondary-color);
  font-size: 0.7rem;
  margin-right: auto;
}
.ws-gtype-count {
  font-size: 0.65rem;
}
/* Nothing in the scenario produces this type — it comes in as a target. */
/* ▶ точки входа — 11 px, поэтому нужен контраст обычного текста (4,5:1).
   --bs-success (#198754) на фоне страницы давал 4,23:1 (аудит UX-3, U13). */
.ws-gtype-entry-mark {
  color: #146c43;
  font-size: 0.7rem;
}
[data-bs-theme="dark"] .ws-gtype-entry-mark {
  color: #3ddc97;
}
.ws-gtype-entry {
  border-color: var(--bs-success-border-subtle);
}
/* Nothing consumes this type: it is the result, not a step. Quieter and with
   no rows to open. */
.ws-gtype-terminal {
  background: var(--bs-tertiary-bg);
}
.ws-gtype-focus {
  opacity: 0;
  transition: opacity 0.1s;
  --bs-btn-font-size: 0.65rem;
}
.ws-gtype:hover .ws-gtype-focus,
.ws-gtype-focus:focus {
  opacity: 1;
}
.ws-gtype-sum {
  display: flex;
  gap: 0.4rem;
  padding: 0 0.4rem 0.2rem;
  font-size: 0.68rem;
  color: var(--bs-secondary-color);
}
.ws-gtype-sum:empty {
  display: none;
}
.ws-gtype-bar {
  height: 2px;
  overflow: hidden;
  opacity: 0.7;
}
.ws-gtype-rows {
  border-top: 1px solid var(--bs-border-color);
}

/* A stage nothing can feed: no seed asset of its input type and no stage in
   the scenario produces one. Dimmed so it reads as "cannot run", not "idle". */
.ws-gnode.ws-gnode-unreachable {
  opacity: 0.55;
  font-style: italic;
}

/* Hovering a type answers "where does this come from and where does it go" by
   dimming everything off its path — the one thing no edge routing can do. */
.ws-stage-graph.ws-graph-dim .ws-gtype {
  opacity: 0.25;
}
.ws-stage-graph.ws-graph-dim .ws-graph-edge {
  opacity: 0.12;
}
.ws-stage-graph.ws-graph-dim .ws-gtype.ws-path {
  opacity: 1;
}
.ws-stage-graph.ws-graph-dim .ws-graph-edge.ws-path {
  opacity: 1;
  stroke: var(--bs-primary);
}

/* Legend, behind the ? in the control cluster: nobody is born knowing what a
   dashed arrow or a ▶ chip means. */
.ws-graph-legend {
  position: absolute;
  top: 0.4rem;
  right: 2.8rem;
  z-index: 3;
  max-width: 18rem;
  padding: 0.4rem 0.55rem;
  font-size: 0.72rem;
}
.ws-graph-legend-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.1rem 0;
}
.ws-graph-legend-mark {
  flex: 0 0 1.1rem;
  height: 0.7rem;
  border-radius: 2px;
}
.ws-lg-type {
  border: 1px solid var(--bs-border-color);
  background: var(--bs-body-bg);
}
.ws-lg-entry {
  border: 1px solid var(--bs-success-border-subtle);
  background: var(--bs-success-bg-subtle);
}
.ws-lg-terminal {
  border: 1px solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
}
.ws-lg-self {
  border-top: 2px dotted var(--bs-secondary-color);
  height: 0;
}
.ws-lg-back {
  border-top: 2px dashed var(--ws-graph-edge);
  height: 0;
}

/* Per-row actions stay quiet until the row is hovered. */
.ws-actions {
  opacity: 0;
  transition: opacity 0.1s;
}

.ws-row:hover .ws-actions,
.ws-actions:focus-within {
  opacity: 1;
}

/* Reports: a horizontal, wrapping row of fixed-ish-width cards (full-width
   section) instead of a tall vertical scroll. */
/* Ряд плиток — сетка, а не флекс-обёртка: колонки одинаковой ширины, строки
   одинаковой высоты. Флексом плитка с длинным описанием растягивала соседей и
   кнопки в них уезжали вниз на пустое место. */
.ws-report-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 0.5rem;
  align-items: stretch;
}
.ws-report-card {
  height: 100%;
}
/* Описание — ровно две строки: дальше многоточие, полный текст в подсказке. */
.ws-report-desc {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.2em;
}
/* Место под цифру держится, даже когда её нет: без него плитки без сводки
   были на строку короче соседних. */
.ws-report-figure {
  min-height: 1.5rem;
}
/* Обзор сценария — та же плитка, просто закреплена первой и чуть выделена.
   Раньше он был отдельной кнопкой в шапке карточки, которая вела на ту же
   сводку, что лежит в этом же списке. */
.ws-report-overview {
  border-color: var(--bs-primary) !important;
}
.ws-report-kind {
  font-size: 0.6rem;
  text-transform: lowercase;
}
/* Развёрнутый обзор во всю ширину секции: в колонке шириной с плитку его
   таблицы и диаграммы нечитаемы. Пусто — значит свёрнут. */
.ws-report-embed-host:empty {
  display: none;
}
.ws-report-embed {
  padding: 0.5rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--vs-radius);
  background: var(--bs-body-bg);
  overflow: auto;
  /* Потолок высоты: отчёт разворачивается ВНУТРИ карточки сценария, и без него
     длинный список выталкивал всё остальное на километр вниз. */
  max-height: 80vh;
}

/* Asset-tree + History: a resizable two-pane split. The vertical handle drags
   the width split; the horizontal handle (below the row) drags the shared
   height. Each pane is a flex column whose body fills to the row bottom, so the
   two frames always bottom-align regardless of header height. Sizes are set as
   inline custom properties by app.js (.ws-ah module) and persisted. */
.ws-ah {
  display: flex;
  align-items: stretch;
  height: var(--ws-ah-h, 30rem);
  min-height: 12rem;
}
.ws-ah-pane {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}
.ws-ah-assets {
  /* fixed basis = the handle position; no grow, so the pane width follows the
     drag precisely. History (below) flex-grows into whatever space is left. */
  flex: 0 0 var(--ws-ah-split, 64%);
}
.ws-ah-history {
  flex: 1 1 0;
  min-width: 12rem;
}
.ws-ah-head {
  flex: 0 0 auto;
  min-height: 2.25rem;
}
.ws-ah-body {
  flex: 1 1 auto;
  min-height: 0;
}
.ws-ah-vhandle {
  flex: 0 0 0.5rem;
  align-self: stretch;
  margin: 0 0.25rem;
  position: relative;
  cursor: col-resize;
  border-radius: 3px;
}
.ws-ah-hhandle {
  height: 0.6rem;
  margin: 0.4rem 0 0.25rem;
  position: relative;
  cursor: row-resize;
  border-radius: 3px;
}
.ws-ah-vhandle::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 25%;
  bottom: 25%;
  width: 2px;
  transform: translateX(-50%);
  background: var(--bs-border-color);
  border-radius: 2px;
}
.ws-ah-hhandle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 45%;
  right: 45%;
  height: 2px;
  transform: translateY(-50%);
  background: var(--bs-border-color);
  border-radius: 2px;
}
.ws-ah-vhandle:hover::before,
.ws-ah-hhandle:hover::before,
.ws-ah-vhandle.ws-ah-drag::before,
.ws-ah-hhandle.ws-ah-drag::before {
  background: var(--bs-primary);
}
body.ws-ah-resizing {
  user-select: none;
}
/* Поля шапки панели активов: на широком экране прежняя фиксированная ширина, на
   узком — сжимаются вместо того, чтобы вылезать за край страницы. */
.ws-target-input {
  width: 15rem;
  max-width: 100%;
}
/* Поле цели — textarea в одну строку (чтобы вставленный список не склеивался);
   растёт до пяти строк, когда в нём список. */
textarea.ws-target-input {
  resize: vertical;
  min-height: calc(1.5em + 0.5rem + 2px);
  max-height: 8rem;
}
.ws-file-input {
  width: 10rem;
  max-width: 100%;
}
/* Выбор типа цели: «авто» по умолчанию, ручной выбор — когда значение читается
   двояко. Сжимается вместе с полем цели, чтобы строка не вылезала за край. */
.ws-target-type {
  width: 9rem;
  max-width: 100%;
}

/* Stack on narrow screens; the drag handles only make sense side-by-side. */
@media (max-width: 768px) {
  .ws-ah {
    flex-direction: column;
    height: auto;
  }
  /* Обе панели занимают высоту своего содержимого. Без этого сброса история
     схлопывалась в НОЛЬ пикселей: у .ws-ah-history задано flex:1 1 0, а в
     колонке с height:auto распределять нечего — база 0 так и остаётся нулём. */
  .ws-ah-assets,
  .ws-ah-history {
    flex: 0 0 auto;
    min-width: 0;
  }
  .ws-ah-vhandle,
  .ws-ah-hhandle {
    display: none;
  }
  .ws-ah-body {
    height: 22rem;
  }
  /* Панели идут одна под другой — нужен зазор, иначе рамки слипаются. */
  .ws-ah-history {
    margin-top: 1rem;
  }
}

/* Ничто в рабочей области не должно расширять страницу по горизонтали: на
   390 px измеренная ширина прокрутки была 562 px, и вся страница ездила вбок. */
@media (max-width: 576px) {
  .ws-card .card-header,
  .ws-card .card-body {
    overflow-x: hidden;
  }
}

/* ---- Asset-tree selection header + summary (Phase 1 UX) ---- */
/* Sticky header inside the scrollable tree pane: the Run button + live selection
   summary stay pinned while the asset tree scrolls under them. */
.ws-tree-head {
  position: sticky;
  top: 0;
  z-index: 3;
}
/* Selected/total descendant badge shown on every parent row. Tabular digits so
   the count doesn't jitter; colour reflects how much of the subtree is picked. */
.ws-desc-count {
  font-variant-numeric: tabular-nums;
  font-size: 0.7rem;
}
.ws-desc-count.ws-desc-none {
  background: var(--bs-secondary-bg);
  color: var(--bs-secondary-color);
}
.ws-desc-count.ws-desc-partial {
  background: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
  border-color: var(--bs-warning-border-subtle) !important;
}
.ws-desc-count.ws-desc-full {
  background: var(--bs-success-bg-subtle);
  color: var(--bs-success-text-emphasis);
  border-color: var(--bs-success-border-subtle) !important;
}
/* Human-readable "what will be scanned" chips under the header. */
.ws-chip {
  font-weight: 500;
}
.ws-chip-type {
  background: var(--bs-primary-bg-subtle);
  color: var(--bs-primary-text-emphasis);
}
.ws-chip-indiv {
  background: var(--bs-secondary-bg-subtle);
  color: var(--bs-secondary-text-emphasis);
}
.ws-chip-run {
  background: var(--bs-info-bg-subtle);
  color: var(--bs-info-text-emphasis);
}
.ws-chip-excluded {
  background: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
}

/* ---- Отчёт в панели предпросмотра конструктора ---- */
/* Plotly рисует в тот размер, который у контейнера уже есть; у пустого div-а
   высота нулевая, и график ложился поверх соседних блоков — таблица и две
   диаграммы оказывались друг на друге. Те же правила есть на самой странице
   отчёта (report_full_page.mustache): здесь они нужны панели предпросмотра,
   которая вставляет тот же фрагмент внутрь страницы конструктора. */
.rep-chart,
.plotly-chart {
  display: block;
  width: 100%;
  min-height: 320px;
  margin: 16px 0;
  clear: both;
}

.rep-doc > * {
  clear: both;
}

.rep-table {
  display: table;
  width: 100%;
}

/* ---- Конструктор отчётов: узкий экран ---- */
/* Поле имени копии не держит фиксированные 14rem на телефоне: вместе с текстом
   «только чтение» и кнопкой оно растягивало страницу до 504 px при экране 390. */
.rb-clone-name {
  width: 14rem;
  max-width: 100%;
}

.rb-clone-name .form-control {
  width: 100%;
}

/* Форма редактора остаётся читаемой колонкой на широком экране и занимает всю
   ширину на узком. */
.rb-form-width {
  max-width: 70rem;
}

.rb-block {
  scroll-margin-top: 5rem;
}

.rb-block-summary {
  cursor: pointer;
  min-width: 0;
}

.rb-block-summary::before {
  content: "▸";
  flex: 0 0 auto;
  color: var(--bs-secondary-color);
}

.rb-block[open] > .rb-block-summary::before {
  content: "▾";
}

.rb-block-description {
  min-width: 0;
}

.rb-block-tools {
  min-height: 1.9rem;
}

.rb-outline-list {
  max-height: 14rem;
  overflow: auto;
}

.rb-outline-item {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

.rb-copy-details > summary,
.rb-outline > summary {
  cursor: pointer;
}

/* ---- Журнал отказов запуска ---- */
/* Текст ошибки переносится и не растягивает панель; высота ограничена, чтобы
   одна многострочная трасса не занимала весь экран. */
.ws-run-error-text {
  max-height: 9rem;
  overflow: auto;
  white-space: pre-wrap;
}

/* Выбор файла: настоящий input спрятан под ярлыком-кнопкой, имя выбранного
   файла подставляет file-input.js. Нативное поле рисовал браузер — в русском
   интерфейсе оно писало «Choose File / No file chosen» по-английски и
   обрезалось по ширине. */
.ws-file-pick {
  position: relative;
  overflow: hidden;
  cursor: pointer;
}
.ws-file-pick .ws-file-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.ws-file-name {
  display: inline-block;
  max-width: 9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
.ws-file-picked {
  border-color: var(--vs-accent);
}

/* Run detail columns fit the page; narrow viewports use labelled cards. */
.rd-stages-table { table-layout: fixed; width: 100%; }
.rd-stages-table th, .rd-stages-table td { overflow-wrap: anywhere; }
.rd-stages-table th:first-child { width: 30%; }


/* Workspace sections keep the common destinations visible without mounting the
   stage graph, asset tree, history poll, and report queries on every page. */
.ws-section-nav {
  overflow-x: auto;
  flex-wrap: nowrap;
  scrollbar-width: thin;
}
.ws-section-nav .nav-link { white-space: nowrap; }
.ws-section-content { min-height: 12rem; }
.ws-overview-facts { max-width: 42rem; }
.ws-overview-facts dt { color: var(--bs-secondary-color); }
.ws-overview-facts dd { margin-bottom: .75rem; }
