/* Cerix MVP — вентиляция по эскизу. Без внешних шрифтов и библиотек. */

/*
 * Спрятанное остаётся спрятанным.
 *
 * Браузер прячет [hidden] правилом своей таблицы, а её вес ниже любого
 * нашего класса: одна строка `display: flex` у класса — и элемент виден,
 * хотя код честно поставил hidden. Так наружу вылезали панель выбора
 * чертежа на пустом приложении и кнопки подбора до того, как считать стало
 * что. Правило общее, чтобы это не повторилось на следующей панели.
 */
[hidden] { display: none !important; }

:root {
  --bg: #f1f5f9;
  --panel: #ffffff;
  --ink: #0f172a;
  --muted: #64748b;
  --line: #cbd5e1;
  --brand: #1d4ed8;
  --brand-ink: #ffffff;
  --danger: #b91c1c;
  --warn: #b45309;
  --ok: #15803d;

  /*
   * Плотные поверхности — ОСНОВНОЙ вид, а не запасной.
   *
   * Стекло красиво, но его может не быть: человек попросил систему убрать
   * прозрачность, браузер не умеет размывать, машина слабая. Если вид держится
   * на стекле, во всех этих случаях он разваливается. Поэтому глубина здесь
   * сделана тем, что есть всегда: цветом, тенью, чертой и ритмом.
   */
  --chrome: #0f172a;
  /* Тёмная полоса вверху — та, на которой держится всё окно. */
  --chrome-ink: #e2e8f0;
  --band: #f8fafc;
  /* Полоса ленты: светлее рабочего поля, но не белая, — иначе она сливается с
     панелями и перестаёт читаться как отдельный ярус. */
  --lift-1: 0 1px 2px rgba(15, 23, 42, .07), 0 1px 1px rgba(15, 23, 42, .04);
  --lift-2: 0 4px 12px rgba(15, 23, 42, .10), 0 1px 3px rgba(15, 23, 42, .06);
  --lift-3: 0 18px 44px rgba(15, 23, 42, .20), 0 3px 10px rgba(15, 23, 42, .10);

  /*
   * Стекло: материал ОБОЛОЧКИ.
   *
   * Панели, полосы и плавающие окна пропускают сквозь себя то, что под ними —
   * так видно, что они лежат ПОВЕРХ чертежа, а не вместо него. Само поле
   * чертежа стеклом не бывает никогда: там тонкие линии, и цвет у них значит
   * направление воздуха. Малейшая муть на них — и приток не отличить от
   * вытяжки.
   *
   * `saturate` тут не украшение: размытие съедает насыщенность, и без возврата
   * цвета под стеклом выцветают до серого.
   */
  --glass-blur: saturate(180%) blur(22px);
  --glass-surface: rgba(255, 255, 255, .62);
  --glass-surface-strong: rgba(255, 255, 255, .84);
  --glass-stroke: rgba(255, 255, 255, .55);
  --glass-edge: rgba(15, 23, 42, .10);
  /* Блик по верхней кромке и тень по нижней — та самая «толщина» стекла. */
  --glass-sheen:
    inset 0 1px 0 rgba(255, 255, 255, .85),
    inset 0 -1px 0 rgba(15, 23, 42, .06);
  --glass-lift: 0 10px 30px rgba(15, 23, 42, .12), 0 2px 6px rgba(15, 23, 42, .06);
  --glass-lift-high: 0 24px 60px rgba(15, 23, 42, .22), 0 4px 12px rgba(15, 23, 42, .10);
  /* Скругления соосны: чем крупнее вместилище, тем круглее его угол. */
  --r-window: 20px;
  --r-panel: 16px;
  --r-control: 11px;
  --r-pill: 999px;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--ink);
  font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  display: flex;
  flex-direction: column;
}

/* ---------- шапка ---------- */

header {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 18px;
  background: #0f172a;
  color: #f8fafc;
  flex-wrap: wrap;
}

.brand strong { font-size: 18px; letter-spacing: .3px; }
.brand span { color: #94a3b8; margin-left: 10px; font-size: 13px; }

.opener { display: flex; gap: 8px; align-items: center; }

.stats {
  margin-left: auto;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  color: #cbd5e1;
}
.stats .chip {
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 6px;
  padding: 3px 8px;
}
.stats .chip b { color: #f8fafc; }

/* ---------- раскладка ---------- */

/*
 * Три колонки, как у Ревита: свойства слева, чертёж посередине, диспетчер
 * проекта справа. Расположение зон не выдумано — это двадцать лет привычки
 * проектировщика, и ломать её нечем оправдать. Новое у нас ВНУТРИ зон.
 */
main {
  flex: 1;
  min-height: 0;
  display: grid;
  /* Ширины боковых зон — переменные: границы раздвигаются мышкой (panels.js). */
  grid-template-columns: var(--panel-left, 280px) minmax(0, 1fr) var(--panel-right, 250px);
  gap: 12px;
  padding: 12px;
  position: relative; /* якорь для ручек на границах колонок */
}

/* Узкому окну третья колонка не по карману — она уходит первой, потому что
   чертёж важнее навигатора. */
@media (max-width: 1180px) {
  main { grid-template-columns: var(--panel-left, 280px) minmax(0, 1fr); }
  #project-tree { display: none; }
  .panel-handle-right { display: none; }
}

aside {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
  overflow: auto;
}

aside section + section {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid #e2e8f0;
}

aside h2 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--muted);
  margin: 0 0 8px;
}

aside label {
  display: block;
  padding: 2px 0;
  cursor: pointer;
}

/*
 * Тело вида: план, таблицы и схема живут ЗДЕСЬ, а полоса вкладок — снаружи.
 * Раньше схема накрывала всю область вместе с вкладками, и с аксонометрии
 * некуда было уйти: вкладки просто исчезали с экрана.
 */
.view-body {
  flex: 1; min-height: 0; position: relative;
  display: flex; flex-direction: column;
}

.content {
  min-width: 0;
  display: flex;
  flex-direction: column;
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}

.toolbar {
  /* Как в Ревите: зум, «весь чертёж» и координаты плавают в правом
     верхнем углу самого чертежа, а не занимают отдельную полосу. */
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 15; /* ниже окон видов (20): у разреза и схемы свои панели */
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  background: rgba(255, 255, 255, .92);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 4px 14px rgba(15, 23, 42, .10);
}
.toolbar .spacer { flex: 1; }

/*
 * Диагностика распознавания должна находиться у самого чертежа: спрятанный
 * в длинной панели слоёв выключатель инженер не обязан искать. Оранжевый —
 * тот же цвет, которым поверх DXF показана принятая алгоритмом масса стен.
 */
.recognition-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
  font-weight: 650;
  color: #9a3412;
  border-color: rgba(234, 88, 12, .38);
  background: rgba(255, 247, 237, .88) !important;
}
.recognition-toggle-icon {
  width: 9px;
  height: 9px;
  flex: 0 0 auto;
  border-radius: 999px;
  background: #fb923c;
  box-shadow: 0 0 0 3px rgba(251, 146, 60, .18);
}
.recognition-toggle.active {
  color: #fff;
  border-color: #c2410c;
  background: linear-gradient(180deg, #ea580c 0%, #c2410c 100%) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25),
              0 4px 12px rgba(194, 65, 12, .24);
}
.recognition-toggle.active .recognition-toggle-icon {
  background: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, .24);
}

.stage {
  flex: 1;
  min-height: 0;
  position: relative;
  background: #ffffff;
  touch-action: none;
  cursor: grab;
  overflow: hidden;
}
/* ---- Полоса вкладок: виды, как в Revit ---- */
.view-tabs {
  display: flex; gap: 2px; padding: 6px 8px 0;
  background: var(--panel); border-bottom: 1px solid var(--line);
  overflow-x: auto; flex: 0 0 auto;
}
.view-tabs:empty { display: none; }
.view-tab {
  flex: 0 0 auto; padding: 6px 12px; font: inherit; font-size: 13px;
  color: var(--muted); background: transparent; cursor: pointer;
  border: 1px solid transparent; border-bottom: none;
  border-radius: 8px 8px 0 0; white-space: nowrap;
}
.view-tab:hover { color: var(--ink); background: rgba(148, 163, 184, .12); }
.view-tab.active {
  color: var(--ink); background: #fff;
  border-color: var(--line); margin-bottom: -1px;
}
.view-tab .dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: baseline;
}
.view-tab .dot[data-kind="supply"] { background: var(--supply-stroke, #b91c1c); }
.view-tab .dot[data-kind="extract"] { background: var(--extract-stroke, #1d4ed8); }

/* Приглушение чужой системы (.vent-foreign) убрано: система теперь гасится
   целиком, как слой, — см. layers.js. */

.stage.panning { cursor: grabbing; }
.stage.arming  { cursor: crosshair; }
.stage svg { width: 100%; height: 100%; display: block; }

/* Все расчётные виды читают один DerivedViewState, но сообщение у него одно.
 * Оно занимает свою строку под вкладками и не перекрывает ни чертёж, ни
 * таблицы, ни кнопки. Статусы на поверхностях остаются для freshness/API. */
.derived-surface { position: relative; }
.derived-status-banner {
  flex: none;
  box-sizing: border-box;
  padding: 8px 13px;
  border-top: 1px solid rgba(180, 83, 9, .30);
  border-bottom: 1px solid rgba(180, 83, 9, .30);
  background: #fffbeb;
  color: #92400e;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  white-space: normal;
}
.derived-status-banner[hidden] { display: none; }
.derived-status-banner[data-derived-status="failed"] {
  border-color: rgba(185, 28, 28, .40);
  background: #fef2f2;
  color: #991b1b;
}
.derived-status-banner[data-derived-status="calculating"] {
  border-color: rgba(3, 105, 161, .30);
  background: #f0f9ff;
  color: #075985;
}
/*
 * Разрез перестраивается отдельно от принятого расчёта проекта. Его локальная
 * плашка живёт на ::before, а глобальная freshness — на ::after. Локальную
 * показываем лишь при globally fresh проекте: stale/failed проекта важнее.
 */
#section-stage::before {
  content: attr(data-section-refresh-message);
  display: none;
  position: absolute;
  z-index: 34;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  max-width: min(720px, calc(100% - 32px));
  box-sizing: border-box;
  padding: 8px 13px;
  border: 1px solid rgba(3, 105, 161, .35);
  border-radius: 9px;
  background: rgba(240, 249, 255, .96);
  color: #075985;
  box-shadow: 0 6px 20px rgba(15, 23, 42, .14);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  white-space: normal;
  pointer-events: none;
}
#section-stage[data-derived-status="fresh"][data-section-refresh-status="calculating"]::before,
#section-stage[data-derived-status="fresh"][data-section-refresh-status="failed"]::before {
  display: block;
}
#section-stage[data-derived-status="fresh"][data-section-refresh-status="failed"]::before {
  border-color: rgba(185, 28, 28, .45);
  background: rgba(254, 242, 242, .96);
  color: #991b1b;
}
.placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
}

/* ---------- кнопки ---------- */

button, select, input {
  font: inherit;
}

button {
  border: 1px solid var(--line);
  background: #fff;
  color: var(--ink);
  border-radius: 8px;
  padding: 7px 11px;
  cursor: pointer;
}
button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
button:disabled { opacity: .45; cursor: not-allowed; }

button.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}
button.primary:hover:not(:disabled) { background: #1e40af; color: #fff; }

button.big {
  width: 100%;
  padding: 12px;
  font-size: 16px;
  font-weight: 600;
}

button.small { padding: 5px 9px; font-size: 13px; }
button.ghost { background: #f8fafc; }
button.danger { border-color: #fecaca; color: var(--danger); }
button.danger:hover:not(:disabled) { border-color: var(--danger); background: #fef2f2; }

select {
  border: 1px solid #334155;
  background: #1e293b;
  color: #f8fafc;
  border-radius: 8px;
  padding: 6px 9px;
  max-width: 260px;
}

.ops-grid { display: grid; gap: 6px; }
.row { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.duct-policy-controls { margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--line); }
.duct-policy-controls label { display: block; font-weight: 600; }
.duct-policy-controls .row { flex-wrap: wrap; align-items: center; }
.duct-policy-controls select { min-width: 0; flex: 1 1 100%; }
.duct-policy-controls button { flex: 0 0 auto; }
.velocity-optimization { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
.velocity-optimization h3 { margin: 0 0 5px; font-size: 13px; }
.velocity-optimization-diff { margin: 7px 0; max-height: 210px; overflow: auto; }
.velocity-optimization-row {
  display: grid; grid-template-columns: minmax(74px, 1fr) minmax(112px, 2fr);
  gap: 7px; padding: 4px 0; border-bottom: 1px solid var(--line); font-size: 11px;
}
.velocity-optimization-row.error { color: var(--danger); }

/* ---------- мелочи ---------- */

.muted { color: var(--muted); }
.small { font-size: 13px; }
.tiny  { font-size: 12px; }
.mono  { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }

.issues { list-style: none; margin: 0; padding: 0; }
.issues li {
  padding: 6px 8px;
  border-left: 3px solid var(--line);
  margin-bottom: 6px;
  background: #f8fafc;
  border-radius: 0 6px 6px 0;
  font-size: 13px;
  cursor: pointer;
}
.issues li.blocker { border-left-color: var(--danger); color: var(--danger); }
.issues li.warning { border-left-color: var(--warn); }
.issues li.info    { border-left-color: #0284c7; }
.issues li code { font-size: 11px; opacity: .7; display: block; }

#provenance p { margin: 0 0 6px; }
#provenance .import-report-preview {
  margin-top: 10px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #f8fafc;
}
#provenance .import-report-preview summary { cursor: pointer; font-weight: 700; }
#provenance .import-report-preview ul { margin: 5px 0 8px; padding-left: 18px; }
#provenance .import-report-blocked { color: var(--danger); font-weight: 700; }

/* ---------- подсказка ---------- */

.hint {
  position: absolute;
  z-index: 30;
  padding: 11px 17px;
  border-radius: 10px;
  background: #0f172a;
  color: #fff;
  font-weight: 600;
  font-size: 15px;
  box-shadow: 0 8px 24px rgba(15, 23, 42, .32);
  pointer-events: none;
  max-width: 460px;
}
.hint.pulse { animation: hint-pulse 1.1s ease-in-out infinite; }

@keyframes hint-pulse {
  0%, 100% { opacity: 1;   transform: scale(1); }
  50%      { opacity: .55; transform: scale(.97); }
}

@media (prefers-reduced-motion: reduce) {
  .hint.pulse { animation: none; }
}

/* ---------- тост ---------- */

.toast {
  position: absolute;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 30;
  background: #0f172a;
  color: #fff;
  padding: 10px 16px;
  border-radius: 10px;
  font-size: 14px;
  box-shadow: 0 8px 24px rgba(15, 23, 42, .3);
  pointer-events: none;
  max-width: 80%;
}
.toast.error { background: var(--danger); }

/* ---------- слои вентиляции внутри SVG ---------- */

.vent-edge {
  fill: none;
  stroke: var(--air-stroke);
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
/*
 * Ответвление отличается ТОЛЬКО толщиной. Цвет в программе означает
 * направление воздуха и больше ничего: ответвление несёт тот же воздух той же
 * системы, и красить его иначе — врать о системе.
 */
.vent-edge.branch { stroke-width: 2.4; }
.vent-edge.locked { stroke-dasharray: 9 3; }
/*
 * Участок, который ничего не обслуживает: расхода нет, сечения нет. Серым
 * пунктиром — человек его провёл, значит зачем-то нужен, но выдавать его за
 * воздуховод нельзя.
 */
.vent-edge.idle {
  stroke: #94a3b8; stroke-width: 2; stroke-dasharray: 10 6;
}

.vent-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 16;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}
/* Выделение — ореол под трассой, а не перекраска её самой. */
.vent-hit.selected { stroke: #f59e0b; stroke-opacity: .35; }
/*
 * Трассу можно тащить мышкой, и курсор обязан об этом сказать. Иначе правка
 * остаётся ненайденной: подсказки о ней нет нигде, кроме самого движения.
 */
.vent-hit { cursor: grab; }
.stage.moving-duct .vent-hit { cursor: grabbing; }

/*
 * Ухватка свободного конца.
 *
 * Нужна потому, что у перетаскивания конца и участка разный смысл: конец
 * ездит ВДОЛЬ и удлиняет трассу, участок — ПОПЕРЁК. Различать их по
 * направлению движения мыши было бы гаданием, а гадать на чертеже нельзя.
 */
.duct-grip {
  fill: #fff;
  stroke: var(--air-stroke, #b91c1c);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  cursor: crosshair;
}
.duct-grip:hover { fill: #fef3c7; }

/*
 * Ползунок границы сечения. Отличается от ухватки свободного конца цветом и
 * смыслом: тот удлиняет трассу, этот переставляет место, где короб меняет
 * размер. Одинаковыми их делать нельзя — жесты разные, и путать их дорого.
 */
.cut-grip {
  fill: #f59e0b;
  fill-opacity: .9;
  stroke: #b45309;
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  cursor: ew-resize;
}
.cut-grip:hover { fill: #fbbf24; }

/*
 * РУЧКА ОПУСКА (Д-04). Круглая, в отличие от квадратной ручки границы
 * сечения: жесты разные, и путать их нельзя — одна двигает переход, другая
 * весь перепад высоты. Цвет берётся у направления системы: опуск
 * принадлежит своей ветке, а не чертежу вообще.
 */
.riser-grip {
  fill: var(--air-soft);
  stroke: var(--air-stroke);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  cursor: ew-resize;
}
.riser-grip:hover { fill: var(--air-fill); }

/*
 * Зона захвата решётки. Прозрачная и широкая: решётка при обычном масштабе
 * занимает один-три пикселя, а под ней лежит шестнадцатипиксельная зона
 * захвата воздуховода. Без этой полосы щелчок по решётке доставался трассе.
 */
/*
 * Деталь перехода на плане. У неё есть начало и конец — торцевые штрихи, — и
 * своя заливка чуть плотнее короба: это изделие, а не продолжение прямой.
 * За деталь можно взяться мышкой: тяга ведёт границу сечения, как ползунок.
 */
.duct-transition {
  fill: var(--air-fill);
  fill-opacity: .28;
  stroke: var(--air-stroke);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  cursor: ew-resize;
}
.duct-transition:hover { fill-opacity: .45; }
.duct-transition-end {
  stroke: var(--air-stroke);
  stroke-width: 2.2;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/*
 * Маркер осиротевшей правки границы. Ему положено бросаться в глаза: правка
 * человека перестала применяться, и молчать об этом чертёж не имеет права.
 */
.cut-orphan circle {
  fill: #dc2626;
  fill-opacity: .85;
  stroke: #7f1d1d;
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}
.cut-orphan text {
  fill: #fff;
  font-size: 480px;
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}

/*
 * Крестик транзита. Малозаметный до наведения: он инструмент на каждый ряд,
 * а не украшение. Транзитная строка приглушена целиком — раздачи в этой
 * системе нет, и поля её не редактируются.
 */
.transit-cell { width: 26px; text-align: center; }
.transit-toggle {
  border: none; background: none; cursor: pointer;
  color: #9ca3af; font-size: 13px; padding: 2px 6px;
}
.transit-toggle:hover { color: #dc2626; }
.transit-toggle.on { color: #2563eb; }
tr.transit td { opacity: .55; }
tr.transit td.transit-cell { opacity: 1; }

.grille-hit {
  fill: transparent;
  stroke: transparent;
  stroke-width: 18;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  pointer-events: all;
  cursor: grab;
}

/*
 * Метка притяжения. Два разных цвета означают два разных исхода, и путать их
 * нельзя: зелёный — сеть СОМКНЁТСЯ и расход по ней посчитается; серый — только
 * выравнивание по линии, узла там не будет. На чертеже оба выглядят одинаково,
 * и различие всплывёт уже на расчёте.
 */
.snap-mark {
  fill: none;
  stroke: #16a34a;
  stroke-width: 2.4;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.snap-mark.aligned { stroke: #94a3b8; stroke-dasharray: 6 5; }

.vent-node { fill: var(--air-stroke); stroke: #fff; stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.vent-node.junction { fill: #f59e0b; }

.sketch-line {
  fill: none;
  stroke: #ef4444;
  stroke-width: 2;
  stroke-dasharray: 7 5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  opacity: .75;
}
.sketch-line.live { opacity: 1; stroke-width: 2.6; }

.crossing {
  fill: #f97316;
  stroke: #7c2d12;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.crossing.external { fill: #dc2626; }

/*
 * Ореол прохода через стену.
 *
 * Мягкое пятно, а не яркая заливка: проход надо заметить боковым зрением, но
 * чертёж он перекрикивать не должен — линий воздуховода и подписей там и без
 * него хватает. Обводка тоньше заливки и того же цвета: она собирает пятно в
 * метку, не превращая его в предмет чертежа.
 *
 * Наружная стена краснее внутренней — как и сама метка. Проход наружу это не
 * проём, а совсем другой разговор с конструктором.
 */
.crossing-halo {
  fill: #f97316;
  fill-opacity: .16;
  stroke: #f97316;
  stroke-opacity: .45;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.crossing-halo.external { fill: #dc2626; stroke: #dc2626; fill-opacity: .20; }


.rubber {
  fill: rgba(245, 158, 11, .12);
  stroke: #f59e0b;
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke;
}

/* ---------- выбор чертежа на листе (MVP-26) ---------- */

/* Подсветка нарочно бледная: под ней настоящий чертёж, и читать его она
   мешать не должна — это подсказка, куда щёлкнуть, а не разметка. */
.sheet-drawing {
  fill: rgba(37, 99, 235, .05);
  stroke: #2563eb;
  stroke-width: 1.5;
  stroke-dasharray: 8 6;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}

.sheet-drawing:hover { fill: rgba(37, 99, 235, .12); }

/* Выбранный чертёж — сплошной рамкой: решение принято, пунктир тут ни к чему. */
.sheet-chosen {
  fill: rgba(22, 163, 74, .10);
  stroke: #16a34a;
  stroke-width: 2.5;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.sheet-picker {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 12px;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border: 1px solid #cbd5e1;
  border-radius: 10px;
  background: rgba(255, 255, 255, .96);
  box-shadow: 0 4px 14px rgba(15, 23, 42, .12);
}

.sheet-picker button { white-space: nowrap; }

.sheet-picker #sheet-picker-apply {
  background: #16a34a;
  border-color: #16a34a;
  color: #ffffff;
}

/* ---------- версия 2: габариты, таблицы, свойства ---------- */

/*
 * Короб на плане рисуется стенками, как на настоящем чертеже: две линии,
 * заливка почти прозрачная. Обводка не масштабируется, поэтому стенки видны
 * на любом увеличении.
 */
.duct-outline {
  fill: var(--air-fill);
  fill-opacity: .12;
  stroke: var(--air-stroke);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}
/* В габаритах выделение тоже не перекрашивает короб — только обводку. */
.duct-outline.selected { stroke: #b45309; stroke-width: 3.2; }
.duct-outline.over-limit { fill: #ef4444; fill-opacity: .35; stroke: #991b1b; }
/*
 * Выбранный КУСОК — не то же, что выбранный участок. Участок обведён по всей
 * длине (`.selected`), кусок — только сам, и заливкой, а не обводкой: две
 * обводки рядом читались бы как две разные вещи, а это одна и та же трасса.
 */
.duct-outline.segment-selected {
  fill: #f59e0b;
  fill-opacity: .3;
  stroke: #b45309;
  stroke-width: 2.4;
}
.duct-outline.narrow.segment-selected { fill: #b45309; fill-opacity: 1; stroke-width: 4; }

/*
 * Мелкий масштаб: короб 200 мм при виде всего этажа занимает полпикселя, и две
 * его стенки сливаются в одну нитку тоньше осевой линии. Тогда короб рисуется
 * СПЛОШНОЙ линией по своему контуру — так же поступает любой CAD, когда толщина
 * изделия перестаёт помещаться в пиксель. Геометрия при этом не подменяется,
 * подменяется только способ её показать.
 */
.duct-outline.narrow {
  fill: var(--air-stroke);
  fill-opacity: .9;
  stroke-width: 3;
}
.duct-outline.narrow.selected { stroke: #b45309; stroke-width: 4; }
.duct-outline.narrow.over-limit { fill: #ef4444; fill-opacity: 1; }

/* Осевая при габаритной отрисовке — тонкий штрихпунктир. */
.duct-axis {
  fill: none; stroke: var(--air-ink); stroke-width: 1;
  stroke-dasharray: 12 4 2 4; vector-effect: non-scaling-stroke; opacity: .55;
}
/* В коробе уже осевой ширины она только забивает собой стенки. */
.duct-axis.hidden, .duct-label.hidden { display: none; }

.duct-label {
  fill: var(--air-ink); font: 600 220px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 60; stroke-linejoin: round;
  /* Подпись ловит щелчок (MVP-30): по ней задают сечение прямо на чертеже.
     Плата названа честно: протянуть трассу, взявшись ровно за её подпись,
     больше нельзя — беритесь за сам короб, он рядом и много крупнее. */
  pointer-events: auto;
  cursor: text;
}

/* Поле правки сечения поверх подписи. Не внутри SVG: подпись повёрнута
   вдоль трассы, и ввод в ней пришлось бы читать боком. */
.duct-label-edit {
  position: fixed;
  z-index: 60;
  width: 120px;
  padding: 2px 6px;
  font: 600 13px system-ui, sans-serif;
  text-align: center;
  color: #0f172a;
  background: #ffffff;
  border: 2px solid var(--accent, #2563eb);
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(15, 23, 42, .25);
}

.room-zone { fill: #22c55e; fill-opacity: .16; stroke: #15803d; stroke-width: 1.2;
             vector-effect: non-scaling-stroke; cursor: pointer; }
.room-zone.selected { fill-opacity: .34; stroke-width: 2.4; }
.room-zone.unknown  { fill: #f59e0b; fill-opacity: .2; stroke: #b45309; }

.room-label {
  fill: #14532d; font: 600 240px system-ui, sans-serif;
  text-anchor: middle; paint-order: stroke; stroke: #fff; stroke-width: 70;
  stroke-linejoin: round; pointer-events: none;
}
.room-label tspan.sub { font-weight: 400; font-size: 200px; }

/* ==================================================================== */
/* Цвета систем: приток тёплый, вытяжка холодная                        */
/* ==================================================================== */
/*
 * Направление задаётся ОДНИМ признаком data-kind на элементе, а не
 * копированием правил в каждое место. Добавить третье направление —
 * например, дымоудаление — будет одним блоком переменных, а не правкой
 * тридцати правил.
 */
/*
 * Оба направления заданы ЯВНО, а не «приток по умолчанию, вытяжка исключением».
 * Иначе приточный короб внутри вытяжного окружения (выбрана вытяжка — признак
 * висит на body) унаследовал бы её переменные и нарисовался синим.
 */
:root, [data-kind="supply"] {
  --air-fill:      #fed7aa;   /* приток: тёплый, базовый красный */
  --air-stroke:    #b91c1c;
  --air-ink:       #7f1d1d;
  --air-accent:    #ea580c;
  --air-soft:      #fff1e6;
}
[data-kind="extract"] {
  --air-fill:      #bfdbfe;   /* вытяжка: холодный, базовый синий */
  --air-stroke:    #1d4ed8;
  --air-ink:       #1e3a8a;
  --air-accent:    #0891b2;
  --air-soft:      #eff6ff;
}

/* условное обозначение: без него по цветной картинке не понять, что есть что */
.air-legend { display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
              font-size: 12px; color: #475569; padding: 4px 0; }
.air-legend i { display: inline-block; width: 22px; height: 10px; border-radius: 2px;
                margin-right: 5px; vertical-align: middle; border: 1.5px solid; }
.air-legend .supply i  { background: #fed7aa; border-color: #b91c1c; }
.air-legend .extract i { background: #bfdbfe; border-color: #1d4ed8; }

/* ==================================================================== */
/* Аксонометрия: второе окно поверх плана                               */
/* ==================================================================== */

/*
 * Аксонометрия — ВИД, а не окно поверх плана. Она занимает ту же область, что
 * и план, и переключается вкладкой. `.content` объявлен position: relative,
 * поэтому absolute + inset накрывает именно её, а не весь экран.
 *
 * Внутренняя вёрстка схемы не тронута ни строкой: меняется только то, где она
 * живёт. Растаскивать два больших отрисовщика ради вкладок — верный способ
 * что-нибудь сломать.
 */
#axo-window {
  position: absolute; inset: 0; z-index: 20; background: #f8fafc;
  display: flex; flex-direction: column;
}
#axo-window[hidden] { display: none; }
.axo-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 16px; background: #0f172a; color: #e2e8f0;
}
.axo-head .brand strong { font-size: 15px; }
.axo-head .brand span { margin-left: 10px; opacity: .75; font-size: 12px; }
.axo-body { flex: 1; display: flex; min-height: 0; }
.axo-side {
  width: 280px; padding: 12px 14px; overflow: auto;
  border-right: 1px solid var(--line); background: #fff;
}
.axo-side h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
               color: #64748b; margin: 0 0 6px; }
.axo-side label { display: block; margin: 3px 0; font-size: 13px; }
.axo-stage { flex: 1; min-width: 0; background: #fff; }
.axo-stage svg { width: 100%; height: 100%; display: block; cursor: crosshair; }

.axo-line {
  stroke: var(--air-stroke); stroke-width: 3; fill: none;
  vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round;
}
.axo-line.main { stroke-width: 5; }
.axo-line.riser { stroke: var(--air-ink); stroke-width: 5; }
.axo-line.selected { stroke: #0ea5e9; stroke-width: 7; }
.axo-hit { stroke: transparent; stroke-width: 900; cursor: pointer; }

/*
 * Габарит решётки на схеме — бледной подложкой, в настоящем размере.
 * По нему сразу видно, сошлись ли решётки вплотную и влезла ли решётка
 * поперёк в короб. Кусок короба рисуется только под самой решёткой: схема
 * однолинейна, и превращать её в габаритный чертёж незачем.
 */
.axo-duct-band {
  fill: var(--air-stroke); fill-opacity: .10;
  stroke: var(--air-stroke); stroke-opacity: .35; stroke-width: 1;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-grille-size {
  fill: var(--air-stroke); fill-opacity: .22;
  stroke: var(--air-stroke); stroke-opacity: .7; stroke-width: 1.2;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* Не влезла поперёк — то же, о чём говорит GRILLE_WIDER_THAN_DUCT. */
.axo-grille-size.over {
  fill: #f97316; fill-opacity: .3;
  stroke: #c2410c; stroke-opacity: 1; stroke-width: 2;
}

/* Решётку тащат за неё саму: широкая прозрачная линия поверх стрелки. */
.axo-grille-hit {
  stroke: transparent; stroke-width: 700; cursor: grab;
}

/* ЗНАК ПЕРЕВОРОТА НА СХЕМЕ (Б10). Тот же рисунок, что и на плане: штриховая
   дуга разворота с галочкой на конце, бледная — она предложение, а не факт.
   Обводка здесь в миллиметрах схемы, а не в пикселях: у схемы свой масштаб,
   и `non-scaling-stroke` сделал бы знак волоском на общем виде. */
.axo-grille-flip-mark {
  fill: none; stroke: var(--air-accent); stroke-width: 60;
  stroke-linecap: round; stroke-linejoin: round; opacity: .3;
  stroke-dasharray: 240 160; pointer-events: none;
}
.axo-grille-flip-mark.head { stroke-dasharray: none; stroke-width: 80; }
.axo-grille-flip {
  stroke: transparent; stroke-width: 700; fill: none; cursor: pointer;
}
.axo-grille-item:has(.axo-grille-flip:hover) .axo-grille-flip-mark {
  opacity: .85;
}
.axo-grille-item.dragging { cursor: grabbing; }
.axo-grille-item.dragging .axo-grille,
.axo-grille-item.dragging .axo-grille-head { opacity: .55; }

.axo-label {
  fill: var(--air-ink); font: 600 260px system-ui, sans-serif; text-anchor: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 70; stroke-linejoin: round;
  pointer-events: none;
}
.axo-label tspan.sub { font-weight: 400; font-size: 220px; }
.axo-level {
  fill: #7c3aed; font: 600 240px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 70; pointer-events: none;
}
.axo-level.start { fill: #0f766e; }
.axo-grille { stroke: var(--air-accent); stroke-width: 3; vector-effect: non-scaling-stroke; }
.axo-grille.bar { stroke-width: 6; }
/* решётка на схеме — двойная стрелка поперёк линии; головки полые, как на чертеже */
.axo-grille-head {
  fill: none; stroke: var(--air-accent); stroke-width: 3;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* штрих на изломе: без него угол читается как продолжение линии */
.axo-tick {
  stroke: var(--air-stroke); stroke-width: 2.4;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* Направление воздуха: у притока от установки, у вытяжки к ней.
   ЗАКРАШЕН НАРОЧНО. Полый треугольник здесь читался переходом сечения —
   контур конфузора выглядит так же, — и на схеме без единой смены сечения
   человек видел «переходы» у каждой решётки. Переход всегда контурный,
   поэтому закрашенную стрелку с ним уже не спутать. */
.axo-flow {
  fill: var(--air-stroke); stroke: var(--air-stroke); stroke-width: 1.2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-grille-label {
  fill: var(--air-accent); font: 600 220px system-ui, sans-serif; text-anchor: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 60; pointer-events: none;
}
.axo-grille-label tspan.sub { font-weight: 400; font-size: 190px; }
.axo-source { fill: var(--air-soft); stroke: var(--air-stroke); stroke-width: 4;
              vector-effect: non-scaling-stroke; }
/* боковые грани установки светлее лицевой — так читается объём */
.axo-source.side { fill: #fff; fill-opacity: .55; stroke-width: 2.4; }
.axo-source-label {
  fill: var(--air-stroke); font: 700 420px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle; pointer-events: none;
}

/*
 * Стояк на плане — торец вертикального короба: прямоугольник ширина×высота
 * сечения с крестом диагоналей, как его и чертят. Кружок остаётся стояку без
 * сечения — у него нет короба, есть линия на экране.
 */
.duct-riser {
  fill: var(--air-fill);
  fill-opacity: .35;
  stroke: var(--air-stroke);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.duct-riser-cross {
  stroke: var(--air-stroke);
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.riser-mark { fill: var(--air-soft); stroke: var(--air-stroke); stroke-width: 3;
              vector-effect: non-scaling-stroke; }
.riser-mark.up { fill: var(--air-stroke); }
/* решётки на воздуховоде */
.grille {
  fill: var(--air-accent); fill-opacity: .55; stroke: var(--air-ink); stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.grille.over-limit { fill: #ef4444; fill-opacity: .6; stroke: #991b1b; }
/* габарита позиции в справочнике нет: место известно, размер — нет */
.grille.unknown { fill-opacity: .2; stroke-dasharray: 5 4; }
.grille-label {
  fill: var(--air-accent); font: 600 200px system-ui, sans-serif;
  text-anchor: middle; paint-order: stroke; stroke: #fff; stroke-width: 60;
  stroke-linejoin: round; pointer-events: none;
}
.grille-label tspan.sub { font-weight: 400; font-size: 175px; }

/* баланс расхода: сошлось или нет — видно без чтения замечаний */
.balance-ok   { color: #15803d; padding: 4px 0; }
.balance-warn { color: #b45309; padding: 4px 0; }
.balance-warn .warn { color: #b91c1c; }

.air-toolbar, .air-zone-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.air-toolbar { padding: 8px; border-bottom: 1px solid var(--line); }
.air-toolbar input, .air-zone-head input { width: 120px; }
.air-zone { margin: 8px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; }
.air-zone.selected { border-color: #2563eb; box-shadow: 0 0 0 1px #2563eb inset; }
.air-zone-head { margin-bottom: 6px; }
.air-zone-name { min-width: 150px; font-weight: 600; }
.air-zone-balance { margin-left: auto; font-variant-numeric: tabular-nums; }
.air-room-summaries { display: grid; gap: 4px; margin: 6px 0 10px; }
.air-room-balance { padding: 6px 8px; border: 1px solid #d7dee8;
  border-radius: 6px; background: #f8fafc; font-variant-numeric: tabular-nums; }
.air-room-balance.ok { border-color: #86efac; background: #f0fdf4; }
.air-room-balance.over { border-color: #fca5a5; background: #fef2f2; color: #991b1b; }
.air-room-balance.pending { color: #64748b; }
.air-terminal-grid { min-width: 1180px; }
.air-terminal-grid input { min-width: 74px; }
.air-terminal-grid td.air-id { max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
.air-source { display: inline-block; margin-left: 4px; padding: 1px 4px; border-radius: 4px;
  font-size: 10px; text-transform: uppercase; }
.air-source.manual { background: #fef3c7; color: #92400e; }
.air-source.auto { background: #dbeafe; color: #1d4ed8; }
.air-empty { padding: 12px; max-width: 720px; }
.grille-item.zone-muted, .axo-grille-item.zone-muted,
.grille-label.zone-muted, .axo-grille-label.zone-muted { opacity: .18; }
.grille-item.zone-selected .grille, .axo-grille-item.zone-selected .axo-grille-size {
  stroke-width: 4; filter: drop-shadow(0 0 3px rgba(37, 99, 235, .65));
}
.grille-label.zone-selected, .axo-grille-label.zone-selected {
  font-weight: 700; filter: drop-shadow(0 0 2px rgba(37, 99, 235, .55));
}

/* глазики этажей в 3D (MVP-25): как слои — снял галку, этаж погас целиком */
#v3-floors {
  position: absolute; top: 12px; right: 12px; z-index: 5;
  display: flex; flex-direction: column; gap: 4px;
  background: rgba(255, 255, 255, .92); border: 1px solid #d0d7e2;
  border-radius: 8px; padding: 8px 12px; font-size: 13px;
}
#v3-floors label { display: flex; align-items: center; gap: 6px; cursor: pointer; }

.floor-list { display: grid; gap: 7px; }
.floor-row { border: 1px solid var(--line-soft); border-radius: 6px; padding: 7px; }
.floor-row-main { display: grid; grid-template-columns: minmax(90px, 1fr) 78px auto;
                  gap: 5px; align-items: center; }
.floor-row-main input { min-width: 0; width: 100%; }
.floor-row-meta { margin-top: 4px; display: flex; gap: 7px; flex-wrap: wrap; }

/* плашка баланса помещения: факт раздачи по всем системам. Красная рамка —
   маркер в поле чертежа; приток малиновым с плюсом, вытяжка синим с
   минусом. Только показывает — клики проходят сквозь неё к плану. */
.balance-badge { pointer-events: none; }
.balance-badge-box {
  fill: #fff; fill-opacity: .92; stroke: #d61f1f; stroke-width: 3;
  vector-effect: non-scaling-stroke;
}
.balance-badge-text {
  font: 700 220px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle;
}
.balance-badge-text.supply { fill: #c2185b; }
.balance-badge-text.extract { fill: #1a56d6; }

/* плашка расхода: задать L прямо в помещении */
.flow-badge { cursor: pointer; }
.flow-badge-box {
  fill: #fff; fill-opacity: .92; stroke: var(--air-stroke); stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.flow-badge-text {
  fill: var(--air-ink); font: 600 220px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle; pointer-events: none;
}
/* заданное руками видно сразу: это решение инженера, а не расчёт */
.flow-badge.manual .flow-badge-box { fill: var(--air-soft); stroke: var(--air-stroke); stroke-width: 3.5; }
.flow-badge.manual .flow-badge-text { fill: var(--air-ink); }
.flow-badge.empty .flow-badge-box { stroke-dasharray: 6 4; stroke: #b45309; }
.flow-badge.empty .flow-badge-text { fill: #b45309; }
.flow-badge:hover .flow-badge-box { fill-opacity: 1; stroke-width: 4; }

.flow-input {
  position: fixed; z-index: 40; box-sizing: border-box;
  padding: 2px 8px; border: 2px solid #0e7490; border-radius: 6px;
  background: #fff; color: #0e7490; font: 600 14px system-ui, sans-serif;
  text-align: center; outline: none;
}

/* таблицы: высота тоже раздвигается за верхнюю кромку (panels.js) */
.tables { border-top: 1px solid var(--line); background: #f8fafc;
          max-height: var(--panel-bottom, 38vh);
          display: flex; flex-direction: column; }
/* display:flex выше перебивает UA-правило [hidden] (та же болезнь, что была
   у оверлея онбординга): скрытые таблицы держали под сценой пустую полосу. */
.tables[hidden] { display: none; }
.tabs { display: flex; gap: 4px; padding: 6px 8px 0; }
.tab { border-radius: 8px 8px 0 0; border-bottom-color: transparent; background: #eef2f7; }
.tab.active { background: #fff; border-color: var(--line); border-bottom-color: #fff;
              color: var(--brand); font-weight: 600; }
.tab-body { overflow: auto; padding: 8px; background: #fff; flex: 1; }

table.grid { width: 100%; border-collapse: collapse; font-size: 13px; }
table.grid th, table.grid td { padding: 4px 6px; border-bottom: 1px solid #e2e8f0;
                               text-align: left; white-space: nowrap; }
table.grid th { position: sticky; top: 0; background: #f8fafc; z-index: 1; font-size: 12px;
                color: var(--muted); font-weight: 600; }
table.grid tr.selected { background: #fef9c3; }
table.grid td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.grid input, table.grid select { border: 1px solid var(--line); border-radius: 6px;
                                      padding: 2px 5px; font-size: 13px; }
table.grid input[type=number] { width: 66px; text-align: right; }
table.grid input.name { width: 150px; }
table.grid select.norm { max-width: 240px; }

.assumed { color: var(--warn); font-style: italic; }
.assumed::after { content: " ⚠"; }
.basis { color: var(--muted); font-size: 12px; }
.over  { color: var(--danger); font-weight: 600; }
.ok    { color: var(--ok); }

.props-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px;
              align-items: center; font-size: 13px; margin-bottom: 8px; }
.props-grid b { font-variant-numeric: tabular-nums; }
.size-inputs { display: flex; align-items: center; gap: 5px; }
.size-inputs input { width: 72px; text-align: right; }

@media (max-width: 980px) {
  main { grid-template-columns: 1fr; }
  aside { max-height: 40vh; }
}

/* панель систем: цвет строки и есть напоминание, что за система выбрана */
.system-row { display: flex; align-items: center; gap: 8px; padding: 5px 7px;
              border: 1px solid #e2e8f0; border-radius: 7px; margin-bottom: 5px;
              cursor: pointer; background: #fff; }
.system-row.active { border-color: var(--air-stroke); background: var(--air-soft); }
.system-dot { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto;
              background: var(--air-fill); border: 2px solid var(--air-stroke); }
.system-name { flex: 1 1 auto; min-width: 60px; border: 0; background: transparent;
               font: 600 13px system-ui, sans-serif; color: #0f172a; padding: 2px; }
.system-name:focus { background: #fff; border-radius: 4px; outline: 1px solid #cbd5e1; }
.system-row button.link { border: 0; background: transparent; cursor: pointer;
                          color: #94a3b8; font-size: 15px; line-height: 1; padding: 2px 4px; }
.system-row button.link:hover { color: #dc2626; }
.table-head { margin: 0 0 6px; padding: 5px 8px; border-radius: 6px;
              background: var(--air-soft); color: var(--air-ink); font-size: 12px; }

/* отметку, сдвинутую автоматикой, надо заметить */
.system-row .moved { color: #b45309; font-weight: 600; }

/* ==================================================================== */
/* Обозначения на плане: стрелки раздачи, отметки, марки установок       */
/* ==================================================================== */

/*
 * Знак направления воздуха у решётки. Решётка стоит в ДНИЩЕ короба, воздух
 * идёт вертикально, а план — вид сверху: вертикальную стрелку на нём не
 * нарисовать. Ставится общепринятый знак:
 *     ⊗ — оперение стрелы, летящей ОТ зрителя (приток идёт вниз);
 *     ⊙ — остриё стрелы, летящей К зрителю (вытяжка идёт вверх).
 */
.grille-throw {
  fill: none;
  stroke: var(--air-accent); stroke-width: 1.4; vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.grille-throw.dot { fill: var(--air-accent); stroke: none; }
.grille-throw.cross { stroke-width: 1.2; }
.grille-throw.side-arrow { fill: none; stroke-width: 1.6; }
.grille-throw.side-arrow-head { stroke: none; fill: var(--air-accent); }
.grille.installation-surface,
.axo-grille-item.installation-surface .axo-grille-size,
.sec-grille.installation-surface .sec-grille-body {
  stroke-width: 3;
}

/* отметка в рамке — так её и пишут на чертеже */
.level-tag-box {
  fill: #fff; fill-opacity: .92; stroke: var(--air-stroke); stroke-width: 1.6;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.level-tag {
  fill: var(--air-ink); font: 600 200px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle; pointer-events: none;
}

/* марка установки: П1, В1 — начало системы */
.unit-tag-box {
  fill: var(--air-soft); stroke: var(--air-stroke); stroke-width: 2.4;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.unit-tag {
  fill: var(--air-stroke); font: 700 460px system-ui, sans-serif;
  text-anchor: middle; dominant-baseline: middle; pointer-events: none;
}

/* ==================================================================== */
/* Узлы: тройники и врезки                                              */
/* ==================================================================== */

.junction-hit { fill: transparent; cursor: pointer; }

/*
 * Деталь узла: Т-контур тройника или патрубок врезки — той же манерой, что
 * переход, только курсор pointer: клик открывает панель узла, тяги у детали
 * нет. Торцы-фланцы — отдельные штрихи, как у перехода.
 */
.duct-junction {
  fill: var(--air-fill);
  fill-opacity: .28;
  stroke: var(--air-stroke);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}
.duct-junction:hover { fill-opacity: .45; }
/* заданное руками видно сразу: программа своё решение не прячет */
.duct-junction.manual { stroke-dasharray: 6 3; }
.duct-junction.selected {
  stroke: #f59e0b; stroke-width: 3.2; fill: #fde68a; fill-opacity: .5;
}
.duct-junction-end {
  stroke: var(--air-stroke);
  stroke-width: 2.2;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/*
 * Совместимость: расчёт, сохранённый до появления геометрии узлов, детали не
 * несёт — ему рисуется прежний значок. Форма, а не только цвет: врезка —
 * кружок (отверстие в стенке), тройник — квадрат (вставленное изделие).
 */
.junction-mark {
  fill: #fff; stroke: var(--air-stroke); stroke-width: 2.4;
  vector-effect: non-scaling-stroke; cursor: pointer;
}
.junction-mark.tee { fill: var(--air-soft); }
.junction-mark.manual { stroke-dasharray: 6 3; }
.junction-mark.selected { stroke: #f59e0b; stroke-width: 4; fill: #fde68a; }

/* узел на схеме: та же форма, что и на плане */
.axo-junction {
  fill: #fff; stroke: var(--air-stroke); stroke-width: 3;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-junction.tee { fill: var(--air-soft); }
.axo-junction-label {
  fill: var(--air-ink); font: 600 200px system-ui, sans-serif;
  dominant-baseline: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 55; pointer-events: none;
}

/* режим рисования профиля по схеме */
.axo-stage.sketching { cursor: crosshair; }
/* Ведомый жест — тёмный карандаш поверх чертежа, ни с чем не спутать. */
.axo-stroke {
  stroke: #0f172a; stroke-width: 2.4; stroke-dasharray: 10 5;
  fill: none; vector-effect: non-scaling-stroke; pointer-events: none;
}

/* решётку можно утащить вдоль воздуховода */
.grille-item { cursor: grab; }
.grille-item.dragging { cursor: grabbing; opacity: .75; }
/*
 * Решётка чужой системы не тащится: её правят, переключившись на эту систему.
 * Иначе место записалось бы в блок выбранного направления — не туда.
 */
.grille-item.foreign { cursor: default; }

/* ==================================================================== */
/* Схема: подложка со стенами и подсказки режима рисования              */
/* ==================================================================== */

/*
 * Стены на схеме лежат в плоскости отметки системы — воздуховод идёт в ней же.
 * Полупрозрачные и без обводки-контраста: это подложка, а не предмет чертежа,
 * и перетягивать внимание на себя она не должна.
 */
/*
 * Стена — ЧИСТЫЙ КАРКАС, без единой заливки.
 *
 * Сначала я залил грани и верх полупрозрачным, и вышло ровно то, чего делать
 * было нельзя: стены стали серыми пятнами и перетянули на себя внимание, а
 * трасса — то, ради чего схему открывают, — оказалась поверх мути. Каркас
 * решает обе задачи разом: стена читается предметом, а сквозь неё всё видно.
 *
 * Грани не заливаются вовсе; их дело — задать вертикальные рёбра, а рисуются
 * рёбра отдельным контуром.
 */
.axo-walls {
  fill: none;
  stroke: none;
  pointer-events: none;
}
/*
 * Верхняя кромка стены — единственное, что обведено.
 *
 * Ради неё стены и подняли: по ней видно, идёт воздуховод НАД стеной или
 * сквозь неё. Без черты верх сливается с гранью, и весь смысл теряется.
 * Линия тонкая и приглушённая — подложка не должна спорить с трассой.
 */
.axo-walls-top {
  fill: none;
  stroke: #94a3b8; stroke-width: 1; stroke-opacity: .55;
  pointer-events: none;
}
/*
 * Каркас стены: низ и вертикальные рёбра по углам.
 *
 * Одной заливки граней было мало — стена читалась пятном, а не предметом,
 * стоящим на полу. Каркас показывает, где она начинается и кончается, и на
 * какой высоте её проходит воздуховод; ради этого стены и поднимали.
 *
 * Низ бледнее верха намеренно. Верх — рабочая черта: по ней смотрят, выше
 * воздуховод или ниже. Низ только досказывает, что стена стоит на полу, и
 * спорить с трассой ему незачем.
 */
.axo-walls-base {
  fill: none;
  stroke: #94a3b8; stroke-width: 1; stroke-opacity: .28;
  pointer-events: none;
}
.axo-walls-edge {
  fill: none;
  stroke: #94a3b8; stroke-width: 1; stroke-opacity: .38;
  pointer-events: none;
}

/* линия отметки под курсором: по ней и ведут жест */
.axo-guide-line {
  stroke: #f59e0b; stroke-width: 1.4; stroke-dasharray: 14 8;
  vector-effect: non-scaling-stroke; pointer-events: none; opacity: .85;
}
.axo-guide-label {
  fill: #b45309; font: 600 260px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 70; pointer-events: none;
}

/*
 * Участок, который правит жест: широкий полупрозрачный ореол ПОД трассой.
 * Сама трасса цвет не меняет — иначе она сравнялась бы с ведомым жестом.
 */
/*
 * Что правил последний жест. Подложка идёт ПО ОСИ трубы и вдвое толще линии —
 * это «вот эта труба», а не «вот эта область». Прежняя подсветка красила зону
 * захвата шириной 900 мм, и читалась она как коридор, за который нельзя
 * выходить, — хотя выходить как раз и надо: жест задаёт высоту.
 */
.axo-edited {
  stroke: #0ea5e9; stroke-opacity: .45; stroke-width: 12; fill: none;
  stroke-linejoin: round; stroke-linecap: round; pointer-events: none;
}
/* ---- Размеры от воздуховода до стен ---- */
.offset-line {
  stroke: #0f766e; stroke-width: 1.2; vector-effect: non-scaling-stroke;
  pointer-events: none;
}
/*
 * Ненадёжный размер — пунктиром и другим цветом. Луч мог уйти в проём, стена
 * может быть не параллельна трассе. Молча показывать правдоподобное и неверное
 * число нельзя.
 */
.offset-line.unsure { stroke: #b45309; stroke-dasharray: 8 5; }
.offset-label {
  fill: #0f766e; font: 600 260px system-ui, sans-serif; text-anchor: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 90; stroke-linejoin: round;
  pointer-events: none;
}
.offset-label.unsure { fill: #b45309; }
/* Пока фигуру наводят, числа не правятся: она едет за курсором. */
.offset-label.editable { pointer-events: auto; cursor: text; }
.offset-label.editable:hover { fill: #0284c7; }
.offset-input {
  position: fixed; z-index: 80; width: 76px; padding: 2px 4px;
  font: 600 13px system-ui, sans-serif; text-align: center;
  border: 1px solid #0284c7; border-radius: 6px; background: #fff;
}

/* ---- Размещение начерченной схемы на плане ---- */
.placing {
  position: absolute; right: 14px; top: 56px; z-index: 30;
  width: 260px; padding: 10px 12px; border-radius: 10px;
  background: rgba(255, 255, 255, .96); border: 1px solid var(--line);
  box-shadow: 0 6px 20px rgba(15, 23, 42, .12);
}
.placing[hidden] { display: none; }
.placing .row { margin-top: 6px; }

/*
 * Призрак: то, что ляжет на чертёж. Пунктиром и полупрозрачно — он ещё не
 * воздуховод, а намерение. Отвод той же линией: цвет означает систему.
 */
.ghost-line {
  stroke: #0f172a; stroke-opacity: .8; stroke-width: 3;
  stroke-dasharray: 14 8; fill: none;
  vector-effect: non-scaling-stroke; pointer-events: none;
  stroke-linejoin: round; stroke-linecap: round;
}
.ghost-line.branch { stroke-width: 2.2; }
.ghost-node { fill: #fff; stroke: #0f172a; stroke-width: 1.4;
              vector-effect: non-scaling-stroke; pointer-events: none; }
.ghost-label {
  fill: #0f172a; font: 600 300px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 90;
  stroke-linejoin: round; pointer-events: none;
}

/* ---- Обратный ход: схема чертится с нуля ---- */
.axo-stage.drafting svg { cursor: crosshair; }

/*
 * Сетка — это ПЛАН на нулевой отметке, увиденный в аксонометрии, а не
 * клетчатая бумага. По ней видно и масштаб, и куда идут оси.
 */
.axo-grid {
  stroke: #cbd5e1; stroke-width: .6; vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.axo-grid.major { stroke: #94a3b8; stroke-width: 1.1; }

.axo-draft-line {
  stroke: #0f172a; stroke-width: 3; fill: none;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round; stroke-linecap: round; pointer-events: none;
}
/*
 * Отвод той же линией, что и магистраль: цвет и толщина означают систему, а не
 * роль участка. Этот урок уже стоил отдельного разбора в MVP-10, повторять его
 * на канве незачем.
 */
.axo-draft-line.branch { stroke-width: 2.4; }
.axo-draft-node { fill: #fff; stroke: #0f172a; stroke-width: 1.4;
                  vector-effect: non-scaling-stroke; pointer-events: none; }
.axo-draft-node.start { fill: #0f172a; }
/* Узел примыкания: там будет тройник или врезка. */
.axo-draft-join {
  fill: none; stroke: #b45309; stroke-width: 2;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-draft-join.live { stroke: #0284c7; stroke-dasharray: 8 5; }
/* Размеры до двух ближайших изломов: от них и отмеряют место врезки. */
.axo-measure {
  stroke: #0284c7; stroke-width: 1.2; stroke-dasharray: 6 4;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-measure-label {
  fill: #0369a1; font: 600 260px system-ui, sans-serif; text-anchor: middle;
  paint-order: stroke; stroke: #fff; stroke-width: 80;
  stroke-linejoin: round; pointer-events: none;
}
.draft-run {
  margin: 8px 0 2px; font-size: 12px; font-weight: 600; color: var(--muted);
  cursor: pointer;
}
.draft-run.active { color: var(--ink); }
.axo-draft-live {
  stroke: #0284c7; stroke-width: 2; stroke-dasharray: 10 6;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.axo-draft-label {
  fill: #0369a1; font: 600 300px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 80;
  stroke-linejoin: round; pointer-events: none;
}
.draft-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.draft-table td { padding: 2px 4px; border-bottom: 1px solid var(--line); }
.draft-table td:first-child { color: var(--muted); width: 20px; }
.draft-table td:last-child { text-align: right; }

.axo-edited-tick {
  stroke: #0284c7; stroke-width: 2; vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/* ======================================================================
 * Разрезы
 * ==================================================================== */

/*
 * Обозначение разреза на ПЛАНЕ — как на настоящем чертеже и как в Revit:
 * разомкнутая линия из двух жирных штрихов по концам, стрелки взгляда и метка
 * с номером. Сама линия показана штриховой: по ГОСТу её середину не проводят,
 * но здесь человек должен видеть, что именно она режет.
 */
.section-mark { pointer-events: none; }
.section-mark .section-trace {
  stroke: #0f172a; stroke-width: 1.4; stroke-dasharray: 24 10 4 10;
  vector-effect: non-scaling-stroke; opacity: .55;
}
.section-mark .section-tick {
  stroke: #0f172a; stroke-width: 4; vector-effect: non-scaling-stroke;
  stroke-linecap: butt; pointer-events: stroke; cursor: pointer;
}
.section-mark .section-arrow {
  stroke: #0f172a; stroke-width: 2.6; vector-effect: non-scaling-stroke;
  stroke-linecap: round;
}
.section-name {
  fill: #0f172a; font: 700 700px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 180;
  stroke-linejoin: round; pointer-events: none;
}
/* Открытый разрез виден на плане сразу: смотришь на план — знаешь, где режешь. */
.section-mark.active .section-trace { stroke: #0284c7; opacity: .9; }
.section-mark.active .section-tick,
.section-mark.active .section-arrow { stroke: #0284c7; }
.section-mark.active .section-name { fill: #0369a1; }
/* Линия, которую ведут: она ещё не проведена, поэтому бледнее. */
.section-mark.ghost .section-trace,
.section-mark.ghost .section-tick,
.section-mark.ghost .section-arrow { stroke: #0284c7; opacity: .8; }

.section-row { display: flex; gap: 4px; align-items: center; margin: 3px 0; }
.section-row button:first-child { flex: 1; text-align: left; }

#section-window {
  position: absolute; inset: 0; z-index: 20; background: #f8fafc;
  display: flex; flex-direction: column;
}
#section-window[hidden] { display: none; }

/* ---------- лист ---------- */

#sheet-window {
  position: absolute; inset: 0; z-index: 20; background: #e2e8f0;
  display: flex; flex-direction: column;
}
#sheet-window[hidden] { display: none; }
/* Серое поле вокруг листа — чтобы белая бумага читалась именно бумагой, а не
   фоном окна: на листе важно видеть его края. */
#sheet-stage { background: #e2e8f0; padding: 14px; }
.sheet-svg { width: 100%; height: 100%; display: block; }
.sheet-paper { fill: #fff; }
.sheet-edge { fill: none; stroke: #94a3b8; stroke-width: .4; }
.sheet-frame { fill: none; stroke: #0f172a; stroke-width: .7; }
.sheet-stamp rect { fill: none; stroke: #0f172a; stroke-width: .7; }
.sheet-stamp line { stroke: #0f172a; stroke-width: .35; }
/* Размер шрифта на листе задаёт чертёж, а не стиль: высота буквы на бумаге —
   такой же размер, как длина линии, и подгонять её под экран нельзя. Здесь
   остаются только цвет и начертание. */
.sheet-text { fill: #0f172a; font-family: system-ui, sans-serif; }
.sheet-text.muted { fill: #475569; }
.sheet-text.bold { font-weight: 600; }

/* ---------- вид 3D ---------- */

#view3d-window {
  position: absolute; inset: 0; z-index: 20; background: #fff;
  display: flex; flex-direction: column;
}
#view3d-window[hidden] { display: none; }
/*
 * Курсор здесь НЕ «взять и потянуть».
 *
 * Ладонь обещала бы, что сцену можно тащить левой кнопкой, — а она тащится
 * зажатым колесом, как в Ревите. Обещать движение, которого нет, хуже, чем не
 * обещать ничего: человек тянет, ничего не происходит, и он решает, что
 * сломано. Оставляем обычную стрелку, а движения названы в панели сбоку.
 */
#view3d-stage { cursor: default; }
#view3d-stage svg { width: 100%; height: 100%; display: block; touch-action: none; }

/*
 * Стены каркасом, как и на схеме, и ровно по той же причине: сквозь каркас
 * видно трассу. Здесь это ещё важнее — при повороте закрашенная стена
 * закрывала бы то, ради чего вид и открыли.
 */
.v3-wall-base { fill: none; stroke: #94a3b8; stroke-width: 1; stroke-opacity: .28;
  vector-effect: non-scaling-stroke; }
.v3-wall-edge { fill: none; stroke: #94a3b8; stroke-width: 1; stroke-opacity: .38;
  vector-effect: non-scaling-stroke; }
.v3-wall-top  { fill: none; stroke: #94a3b8; stroke-width: 1; stroke-opacity: .55;
  vector-effect: non-scaling-stroke; }

/* Объект — тоже каркас и тоже фиолетовый: тот же цвет, что на плане, чтобы не
   искать глазами, где здесь тот самый шкаф. */
.v3-object { fill: none; stroke: #a855f7; stroke-width: 1.2; stroke-opacity: .8;
  vector-effect: non-scaling-stroke; }

/* Воздуховод — линией, толщина постоянная на экране: при вращении и зуме она
   не должна прыгать, иначе трасса то теряется, то заливает вид. */
.v3-duct {
  fill: none; stroke-width: 2.4; stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.v3-duct.supply { stroke: var(--danger); }
.v3-duct.extract { stroke: var(--brand); }

/*
 * Тело воздуховода (решение владельца, 2026-08-18): белое непрозрачное, как
 * на монтажной схеме, контур цветом системы. Верх чуть светлее бортов, торцы
 * темнее — по этим трём тонам глаз читает объём без настоящего света.
 */
.v3-face {
  stroke-width: 1.6;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.v3-face.top { fill: #ffffff; }
.v3-face.side { fill: #f2f4f8; }
.v3-face.end { fill: #e4e8ef; }
.v3-face.supply { stroke: var(--danger); }
.v3-face.extract { stroke: var(--brand); }
/* Круглое тело (MVP-27): бока цилиндра без продольных рёбер — обводку
   держат торцы и кольца-фланцы, иначе 16-гранник читался бы кашей линий. */
.v3-face.seamless { stroke: none; }
.v3-ring { fill: none; stroke-width: 1.4; }
.v3-ring.supply { stroke: var(--danger); }
.v3-ring.extract { stroke: var(--brand); }
.v3-face.grille { fill: url(#v3-grille-hatch); stroke-width: 2; }

.v3-crossing {
  fill: #f97316; fill-opacity: .18;
  stroke: #f97316; stroke-opacity: .5; stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.v3-crossing.external { fill: #dc2626; stroke: #dc2626; }

/* ---- сам разрез: горизонталь — вдоль линии, вертикаль — отметка ---- */

.sec-hatch { stroke: #94a3b8; stroke-width: 26; }
.sec-wall { fill-opacity: .9; stroke: none; }
.sec-wall-edge {
  fill: none; stroke: #475569; stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}
.sec-ground { fill-opacity: .55; }
.sec-floor {
  stroke: #0f172a; stroke-width: 3.4; vector-effect: non-scaling-stroke;
}
/* Перекрытие ПРИНЯТО, а не измерено: штриховая линия об этом и говорит. */
.sec-ceiling {
  stroke: #64748b; stroke-width: 1.8; stroke-dasharray: 20 12;
  vector-effect: non-scaling-stroke;
}
/*
 * У надписей разреза размер и ореол заданы АТРИБУТАМИ, а не здесь: они зависят
 * от длины разреза, и правило в таблице стилей перебило бы атрибут.
 */
.sec-level {
  fill: #475569; font-family: system-ui, sans-serif; font-weight: 500;
  paint-order: stroke; stroke: #fff; stroke-linejoin: round;
}
/*
 * Линии знака отметки — тонкие и того же цвета, что число. Знак служебный: он
 * указывает, а не изображает, и спорить с воздуховодами ему не положено.
 */
.sec-level-line {
  stroke: #475569; stroke-width: 1.1; fill: none;
  vector-effect: non-scaling-stroke; stroke-linecap: round;
}

/*
 * Обозначение системы. Крупнее отметок и цветом своей системы: на чертеже эту
 * надпись читают первой, по ней узнают, чья трасса.
 *
 * Выноска тонкая — она указывает, а не изображает.
 */
.sec-system-line {
  stroke: var(--air-stroke); stroke-width: 1.1; fill: none;
  vector-effect: non-scaling-stroke;
}
.sec-system-name {
  fill: var(--air-stroke); font-family: system-ui, sans-serif; font-weight: 700;
  letter-spacing: .04em;
  paint-order: stroke; stroke: #fff; stroke-linejoin: round;
}

/*
 * Заголовок разреза. Чёрный, а не цветной: он про весь чертёж, а не про одну
 * систему. Черта под ним толстая — ею отделяют проекцию от соседних.
 */
/*
 * Выноска «сечение / длина». Черта дроби и наклонный штрих тонкие: они ведут
 * глаз от короба к числам и на этом их дело кончается.
 */
.sec-leader-line {
  stroke: var(--air-ink); stroke-width: 1; fill: none;
  vector-effect: non-scaling-stroke;
}
.sec-leader-text {
  fill: var(--air-ink); font-family: system-ui, sans-serif; font-weight: 500;
  font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: #fff; stroke-linejoin: round;
}

.sec-title {
  fill: #0f172a; font-family: system-ui, sans-serif; font-weight: 600;
  letter-spacing: .02em;
}
.sec-title-rule {
  stroke: #0f172a; stroke-width: 2.4; fill: none;
  vector-effect: non-scaling-stroke;
}

.sec-duct {
  fill: var(--air-fill); fill-opacity: .45;
  stroke: var(--air-stroke); stroke-width: 2.2;
  vector-effect: non-scaling-stroke;
}
/* Нарушение помечается на самом коробе: в списке замечаний его не найдёшь глазами. */
.sec-duct.bad { stroke: #dc2626; stroke-width: 3.4; fill: #fecaca; fill-opacity: .6; }

/*
 * Предмет на разрезе. Рассечённый — сплошным контуром, видимый за плоскостью —
 * тонким и штриховым. Так и на чертеже: по толщине линии читают, попал предмет
 * в рез или стоит позади, и без этого различия разрез становится набором
 * одинаковых прямоугольников.
 *
 * Цвет — тот же фиолетовый, что на плане и в виде 3D: искать глазами, где
 * здесь тот самый шкаф, человек не должен.
 */
.sec-object {
  fill: #a855f7; fill-opacity: .12;
  stroke: #7e22ce; stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.sec-object.behind {
  fill-opacity: .06;
  stroke-width: 1.1; stroke-dasharray: 10 7;
}
/* Рамка, которую ведут прямо сейчас, — та же, что на плане: бегущий штрих и
   свечение. Пока её тянут, она не должна быть похожа на уже поставленный
   предмет, иначе непонятно, ставишь ты сейчас или смотришь на прежнее. */
.sec-object.ghost {
  fill-opacity: .16;
  stroke: #a855f7; stroke-width: 2.4; stroke-dasharray: 10 6;
  filter: drop-shadow(0 0 6px rgba(168, 85, 247, .85));
  animation: object-spark 1.1s linear infinite, object-glow 1.6s ease-in-out infinite;
}
.sec-riser {
  fill: var(--air-fill); fill-opacity: .3;
  stroke: var(--air-ink); stroke-width: 2.2; stroke-dasharray: 14 8;
  vector-effect: non-scaling-stroke;
}
/*
 * Решётка — заштрихованный прямоугольник внутри короба, как на чертеже.
 *
 * Заливка бледная, штриховка своим цветом: сплошная тёмная полоса при мелком
 * изображении сливается в чернильное пятно и перестаёт читаться как решётка.
 * Контур тонкий — он очерчивает изделие, а не спорит с контуром короба.
 */
.sec-grille-body {
  fill: url(#section-grate);
  stroke: var(--air-stroke); stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
}
.sec-grille-frame {
  fill: none;
  stroke: currentColor;
  stroke-width: 24px;
}

.editable-annotation { cursor: move; user-select: none; }
.editable-annotation[data-annotation-manual="true"] {
  paint-order: stroke fill;
  stroke: color-mix(in srgb, var(--air-accent) 25%, transparent);
  stroke-width: 18px;
}
.annotation-orphans {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 90;
  width: min(460px, calc(100vw - 32px));
  max-height: 35vh;
  overflow: auto;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: var(--lift-3);
}
.annotation-orphan {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.sec-grate-line {
  stroke: var(--air-stroke); stroke-width: 9;
}
.sec-label {
  fill: var(--air-ink); font-family: system-ui, sans-serif; font-weight: 600;
  paint-order: stroke; stroke: #fff; stroke-linejoin: round;
}
.sec-dim {
  stroke: #0f766e; stroke-width: 1.4; vector-effect: non-scaling-stroke;
}
.sec-dim.bad { stroke: #dc2626; stroke-width: 2; }
.sec-dim-text {
  fill: #0f766e; font-family: system-ui, sans-serif; font-weight: 600;
  paint-order: stroke; stroke: #fff; stroke-linejoin: round;
}
.sec-dim-text.bad { fill: #dc2626; }

/* ---------- глубина разреза (MVP-16) ---------- */

/*
 * Рамка глубины на плане — как рамка вида в Revit: пунктирный прямоугольник от
 * линии разреза в сторону взгляда, на дальней грани ползунок со стрелками.
 * Показывается только у ВЫДЕЛЕННОГО разреза, иначе план зарос бы рамками.
 */
.section-depth {
  fill: #0284c7; fill-opacity: .05;
  stroke: #0284c7; stroke-width: 1.2; stroke-dasharray: 18 12;
  vector-effect: non-scaling-stroke;
}
.section-grip { pointer-events: all; cursor: ns-resize; }
.section-grip .section-grip-bar {
  stroke: #0284c7; stroke-width: 5; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.section-grip .section-grip-arrow {
  stroke: #0284c7; stroke-width: 2.2; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.section-grip:hover .section-grip-bar { stroke: #0369a1; stroke-width: 7; }
.section-depth-text {
  fill: #0369a1; font: 700 600px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 160;
  stroke-linejoin: round; pointer-events: none;
}
/* Выделенный разрез виден и в списке слева, и на плане — одно и то же место. */
.section-mark.picked .section-trace { stroke: #0284c7; opacity: .9; }
.section-mark.picked .section-tick { stroke: #0284c7; }
.section-row.picked button:first-child { color: #0369a1; font-weight: 600; }

/*
 * Вид за плоскостью. Не разрезано, а ВИДНО: контур без заливки, тоньше сечения.
 * Прозрачность ставится атрибутом при отрисовке — она зависит от расстояния.
 *
 * Контур приходит с сервера готовым: ход обведён тем же кодом, каким короб
 * рисуется на плане, поэтому отвод выходит двумя дугами, а переход — трапецией.
 * Короб за плоскостью при этом всегда ПРЯМОУГОЛЬНЫЙ в сечении, даже круглый:
 * эллипс бывает только в резе, а сбоку цилиндр выглядит прямоугольником
 * высотой в диаметр.
 */
.sec-behind { pointer-events: all; }
.sec-behind-duct {
  fill: var(--air-fill); fill-opacity: .14;
  stroke: var(--air-stroke); stroke-width: 1.6;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}

/*
 * Переход на разрезе — деталь, той же манерой, что на плане: трапеция чуть
 * плотнее полосы куска, торцы-фланцы жирнее. Сечение конуса (плоскость режет
 * саму деталь) — как сечение короба.
 */
.sec-transition {
  fill: var(--air-fill); fill-opacity: .3;
  stroke: var(--air-stroke); stroke-width: 1.6;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}
.sec-transition-end {
  stroke: var(--air-stroke); stroke-width: 2.2;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.sec-transition-cut {
  fill: var(--air-fill); fill-opacity: .45;
  stroke: var(--air-stroke); stroke-width: 2.2;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}
/*
 * Подложка разреза — фрагмент листа с начерченным архитектурным разрезом.
 *
 * Серым и без обводки: это чужой чертёж, по которому сверяют высоты, а не
 * предмет нашего. Пока точка нуля не назначена — бледнее и штрихом намёк,
 * что фрагмент ещё «висит» и ждёт щелчка.
 */
.sec-underlay {
  fill: #64748b; fill-opacity: .35; stroke: none; pointer-events: none;
}
.sec-underlay.pending { fill-opacity: .18; }

/* Узел на разрезе: тело тройника и патрубок — той же манерой, что деталь. */
.sec-junction {
  fill: var(--air-fill); fill-opacity: .3;
  stroke: var(--air-stroke); stroke-width: 1.6;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}
.sec-junction-end {
  stroke: var(--air-stroke); stroke-width: 2.2;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* Плоскость режет саму деталь узла — как сечение короба и сечение конуса. */
.sec-junction-cut {
  fill: var(--air-fill); fill-opacity: .45;
  stroke: var(--air-stroke); stroke-width: 2.2;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
}

/*
 * Осевая линия короба — штрихпунктиром, как на чертеже.
 *
 * Тоньше контура и того же цвета: ось принадлежит своему коробу, а не живёт
 * сама по себе. Штрих длинный, точка короткая — так их различают на глаз даже
 * при мелком изображении.
 *
 * `non-scaling-stroke` держит постоянными и толщину, и рисунок штриха: без него
 * при увеличении штрихи растянутся и ось станет сплошной.
 */
.sec-axis {
  fill: none;
  stroke: var(--air-stroke); stroke-width: .8;
  stroke-dasharray: 14 3 2 3;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.sec-behind-grille-body {
  fill: url(#section-grate);
  stroke: var(--air-stroke); stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* ======================================================================
 * СТЕКЛО
 *
 * Оболочка приложения — полосы, панели, плавающие окна, кнопки — сделана из
 * одного материала: полупрозрачного, с размытием того, что за ним, бликом по
 * верхней кромке и мягкой тенью снизу. Отсюда и ощущение, что окна лежат НАД
 * чертежом, а не заменяют его кусок.
 *
 * Раздел стоит последним нарочно. Он ничего не переставляет и не меняет
 * размеров — только внешность: цвет, кромку, скругление, тень. Раскладка,
 * поведение и всё, что внутри чертежа, остались как были, и вернуть прежний
 * вид можно, убрав один этот раздел целиком.
 *
 * ЧЕГО ЗДЕСЬ НЕТ И НЕ БУДЕТ: стекла на самом чертеже. Ни на `.stage`, ни на
 * его слоях. Инженер читает по нему геометрию, а цвет линии значит приток или
 * вытяжку — мутить это нельзя ничем.
 * ====================================================================== */

/* Подложка, ради которой стекло вообще имеет смысл: без неё размывать нечего.
 * Пятна холодные и очень бледные — они должны читаться как свет в комнате, а
 * не как раскраска. */
body {
  background:
    radial-gradient(1100px 700px at 12% -12%, rgba(29, 78, 216, .16), transparent 62%),
    radial-gradient(900px 620px at 96% -6%, rgba(14, 165, 233, .14), transparent 58%),
    radial-gradient(820px 780px at 55% 118%, rgba(99, 102, 241, .12), transparent 60%),
    linear-gradient(180deg, #eef2f8 0%, #e6ebf3 100%);
  background-attachment: fixed;
}

/* --- шапка и полоса проекта: стекло во всю ширину --- */

header {
  background: var(--glass-surface-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  color: var(--ink);
  border-bottom: 1px solid var(--glass-edge);
  box-shadow: var(--glass-sheen);
  padding: 12px 18px;
  position: relative;
  z-index: 5;
}
.brand span { color: var(--muted); }
.stats { color: var(--muted); }
.stats .chip {
  background: rgba(255, 255, 255, .55);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-pill);
  color: var(--ink);
  padding: 3px 10px;
}
.stats .chip b { color: var(--ink); }

/*
 * Полоса проекта: своих стилей у неё не было вовсе — она приехала вместе со
 * слоем сохранения и держалась на том, что достанется от шапки.
 */
.project-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 8px 18px;
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-edge);
  box-shadow: var(--glass-sheen);
  position: relative;
  z-index: 4;
}
.project-current {
  font-weight: 600;
  margin-left: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 22ch;
}
/* Состояние сохранения — единственное место, где цвет несёт смысл сам по
 * себе, поэтому он тут заметнее прочего. */
.save-status {
  margin-left: auto;
  font-size: 13px;
  color: var(--muted);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  white-space: nowrap;
}
.save-status.saved { color: var(--ok); border-color: rgba(21, 128, 61, .25);
  background: rgba(21, 128, 61, .08); }
.save-status.dirty, .save-status.saving { color: var(--warn);
  border-color: rgba(180, 83, 9, .25); background: rgba(180, 83, 9, .08); }
.save-status.recovering { color: var(--warn);
  border-color: rgba(180, 83, 9, .25); background: rgba(180, 83, 9, .08); }
.save-status.offline, .save-status.conflict, .save-status.error {
  color: var(--danger); border-color: rgba(185, 28, 28, .3);
  background: rgba(185, 28, 28, .08); }

/* --- панели --- */

aside {
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-stroke);
  border-radius: var(--r-panel);
  box-shadow: var(--glass-sheen), var(--glass-lift);
}
aside section + section { border-top-color: var(--glass-edge); }

.content {
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-stroke);
  border-radius: var(--r-panel);
  box-shadow: var(--glass-sheen), var(--glass-lift);
}

.toolbar { border-bottom: none; }

.view-tabs {
  background: transparent;
  border-bottom: 1px solid var(--glass-edge);
  gap: 4px;
  padding: 8px 8px 0;
}
.view-tab { border-radius: var(--r-control) var(--r-control) 0 0; }
.view-tab.active {
  background: rgba(255, 255, 255, .78);
  border-color: var(--glass-stroke);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9);
}

/*
 * Поле чертежа. Плотное и белое — под ним ничего не просвечивает и не должно.
 * Скругление на единицу меньше, чем у рамки: так угол внутри повторяет угол
 * снаружи, а не спорит с ним.
 */
.stage {
  background: #fff;
  border-radius: 0 0 calc(var(--r-panel) - 1px) calc(var(--r-panel) - 1px);
}

/* --- органы управления --- */

button {
  background: rgba(255, 255, 255, .72);
  -webkit-backdrop-filter: saturate(150%) blur(8px);
  backdrop-filter: saturate(150%) blur(8px);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-control);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8), 0 1px 2px rgba(15, 23, 42, .06);
  transition: background .16s ease, border-color .16s ease,
              box-shadow .16s ease, transform .16s ease;
}
button:hover:not(:disabled) {
  background: rgba(255, 255, 255, .92);
  border-color: rgba(29, 78, 216, .35);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .95), 0 4px 12px rgba(15, 23, 42, .10);
  transform: translateY(-1px);
}
/* Нажатие возвращает кнопку на место: стекло прогибается, а не проваливается. */
button:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: inset 0 1px 3px rgba(15, 23, 42, .12);
}
button:focus-visible {
  outline: 2px solid rgba(29, 78, 216, .55);
  outline-offset: 2px;
}
button.small { border-radius: var(--r-pill); padding: 5px 12px; }
button.big { border-radius: var(--r-control); }
button.ghost { background: rgba(255, 255, 255, .5); }
button.primary {
  background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%);
  border-color: rgba(29, 78, 216, .9);
  color: var(--brand-ink);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), 0 4px 12px rgba(29, 78, 216, .28);
}
button.primary:hover:not(:disabled) {
  background: linear-gradient(180deg, #3b82f6 0%, #1e40af 100%);
  border-color: rgba(29, 78, 216, 1);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), 0 8px 20px rgba(29, 78, 216, .34);
}
button.danger { background: rgba(255, 255, 255, .72); border-color: rgba(185, 28, 28, .35); }

select, input[type="text"], input[type="number"], input:not([type]) {
  background: rgba(255, 255, 255, .78);
  color: var(--ink);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-control);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8);
}
select:focus-visible, input:focus-visible {
  outline: 2px solid rgba(29, 78, 216, .5);
  outline-offset: 1px;
}

/* --- плавающие окна: под ними настоящий чертёж, и это видно --- */

#axo-window, #section-window {
  background: rgba(248, 250, 252, .72);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-radius: var(--r-window);
  box-shadow: var(--glass-sheen), var(--glass-lift-high);
  overflow: hidden;
}
.axo-head {
  background: var(--glass-surface-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  color: var(--ink);
  border-bottom: 1px solid var(--glass-edge);
  box-shadow: var(--glass-sheen);
}
.axo-head .brand span { color: var(--muted); }

.toast {
  background: rgba(15, 23, 42, .82);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  backdrop-filter: saturate(180%) blur(16px);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: var(--r-pill);
  padding: 10px 18px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), var(--glass-lift-high);
}
.toast.error { background: rgba(185, 28, 28, .88); }

/*
 * Подсказка лежит ПОВЕРХ чертежа и потому из того же материала, что и
 * уведомление, — тёмного стекла. Прозрачность ей идёт на пользу вдвойне:
 * подсказка появляется во время черчения, и под ней остаётся видна та самая
 * линия, о которой она говорит.
 *
 * Прозрачность здесь невелика (.86) и это намеренно: подсказка несёт слова, а
 * не украшение, и читаться обязана поверх любого чертежа.
 */
.hint {
  background: rgba(15, 23, 42, .86);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  backdrop-filter: saturate(180%) blur(16px);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: var(--r-control);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), var(--glass-lift);
}

/* ---------- лента стадий ---------- */

.ribbon {
  flex: none;
  border-bottom: 1px solid var(--glass-edge);
  background: var(--glass-surface-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-sheen);
}

.ribbon-tabs {
  display: flex;
  gap: 2px;
  padding: 6px 14px 0;
  overflow-x: auto;
}

.ribbon-tab {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  padding: 6px 14px;
  border-radius: var(--r-control) var(--r-control) 0 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.ribbon-tab:hover:not(.active) { background: rgba(15, 23, 42, .06); transform: none; }
.ribbon-tab.active {
  background: var(--panel);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 -1px 0 var(--glass-edge) inset;
}

/*
 * Значок состояния стадии — то, чего у Ревита нет: его лента не знает, что
 * происходит с проектом. Пусто значит «не начато», и тогда значка нет вовсе:
 * отсутствие состояния и спокойное состояние обязаны выглядеть по-разному.
 *
 * Форма у трёх видов разная, а не только цвет: круг, треугольник, крест.
 * Различать состояния одним цветом нельзя.
 */
.stage-mark { width: 9px; height: 9px; flex: none; }
.stage-mark.ok {
  border-radius: 50%;
  background: var(--ok);
}
.stage-mark.assumed {
  background: var(--warn);
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.stage-mark.bad {
  background: var(--danger);
  clip-path: polygon(20% 0, 50% 30%, 80% 0, 100% 20%, 70% 50%, 100% 80%,
    80% 100%, 50% 70%, 20% 100%, 0 80%, 30% 50%, 0 20%);
}

/*
 * Контекстная вкладка — отделена чертой и подкрашена: она появляется и
 * исчезает, и человек должен видеть, что это не постоянная часть ленты, а
 * ответ на его выбор.
 */
.ribbon-tab.contextual {
  margin-left: 10px;
  position: relative;
  color: var(--brand);
}
.ribbon-tab.contextual::before {
  content: "";
  position: absolute;
  left: -6px; top: 20%; bottom: 20%;
  border-left: 1px solid var(--line);
}
.ribbon-tab.contextual.active { background: rgba(29, 78, 216, .10); }

.ribbon-panels { background: var(--panel); }
/* Внутри ленты действия стоят в ряд, а не сеткой, как стояли в панели слева. */
.ribbon-panel .ops-grid,
.ribbon-panel .junction-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/*
 * `hidden` обязан прятать — а `display` его перебивает.
 *
 * Атрибут `hidden` действует правилом браузера `display: none`, и ЛЮБОЕ наше
 * правило с `display` сильнее. Я задал полотнам `display: flex` и тем отменил
 * прятанье: вылезли все шесть полотен разом, лента заняла пол-экрана, а
 * справочник открылся сам собой.
 *
 * Проверка это пропустила, потому что смотрела на свойство `.hidden` — оно
 * честно было `true`. А видно элемент или нет, я не спросил.
 *
 * Правило на будущее: задал `display` элементу, который прячут атрибутом, —
 * тут же напиши и `[hidden]`.
 */
.ribbon-panel[hidden],
.reference-panel[hidden],
.answer-bar[hidden] { display: none; }

/*
 * Переключатель: одно состояние из двух.
 *
 * Кнопки внутри уже существуют и уже работают — обойма только соединяет их
 * зрительно и заливает нажатую. Заливка здесь несёт смысл, а не украшает: по
 * ней видно, чем сейчас показан чертёж, не глядя на сам чертёж.
 */
.segmented {
  display: flex;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  overflow: hidden;
  background: var(--panel);
  flex: none;
}
.segmented .seg {
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  color: var(--muted);
  padding: 6px 13px;
  font-size: 13px;
}
.segmented .seg + .seg { border-left: 1px solid var(--line); }
.segmented .seg:hover:not(.active):not(:disabled) {
  background: rgba(15, 23, 42, .06);
  transform: none;
}
.segmented .seg.active {
  background: var(--brand);
  color: var(--brand-ink);
  font-weight: 600;
}
.segmented .seg:disabled { color: var(--muted); opacity: .55; }
.ribbon-panel {
  display: flex;
  align-items: stretch;
  gap: 0;
  padding: 6px 14px 4px;
  flex-wrap: wrap;
  min-height: 46px;
}
/* Перенесённая кнопка теряет «большой» вид: в ленте они стоят в один рост. */
.ribbon-btn.big { font-size: 14px; padding: 7px 14px; }

/*
 * Группа инструментов с подписью снизу — как в ленте Ревита и Офиса.
 *
 * Подпись стоит ПОД плитками, а не над ними: она называет уже увиденное, и
 * читают её вторым взглядом, если первый не помог. Разделительная черта
 * заменяет отступ — отступом группы не отделяются, когда их шесть в ряд.
 */
.ribbon-panel .group {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 0 12px;
  border-right: 1px solid var(--line);
}
.ribbon-panel .group:last-child { border-right: none; }
.ribbon-panel .tools { display: flex; align-items: flex-start; gap: 4px; }
.ribbon-panel .group-title {
  margin-top: 3px;
  text-align: center;
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/*
 * Плитка инструмента: значок, имя, пометка. Ширина у всех одна, иначе полка
 * инструментов читается рваной, а взгляд по ней бежит вдоль подписей.
 */
.ribbon-panel .tool {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  /* Ширина — по имени: «Приточная установка» держится в одну строку.
     Фиксированные 74px переносили имя на вторую строку, и при единой
     высоте панелей подпись группы срезалась. */
  width: auto;
  min-width: 64px;
  padding: 5px 8px 4px;
  border: 1px solid transparent;
  border-radius: var(--r-control);
  background: transparent;
  box-shadow: none;
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  line-height: 1.15;
  text-align: center;
  cursor: pointer;
}
.ribbon-panel .tool:hover:not(:disabled) {
  background: rgba(15, 23, 42, .06);
  border-color: var(--line);
  transform: none;
}
.ribbon-panel .tool:disabled { opacity: .45; cursor: default; }
.ribbon-panel .tool svg { width: 26px; height: 26px; flex: none; }
.ribbon-panel .tool .name { display: block; white-space: nowrap; }

/*
 * Пометка готовности. Она остаётся в рабочей версии, а не только в макете:
 * пока продукт растёт, честная пометка полезнее пустоты. Кнопка без пометки
 * обещает работу, кнопка с пометкой «новое» обещает замысел.
 *
 * Цвета разные, но полагаться на один цвет нельзя — слова написаны прямо.
 */
.ribbon-panel .mark {
  font-size: 9px;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 0 5px;
  border-radius: 999px;
  line-height: 14px;
}
.mark.есть   { background: #dcfce7; color: #166534; }
.mark.почти  { background: #fef3c7; color: #92400e; }
.mark.сам    { background: #e0e7ff; color: #3730a3; }
.mark.новое  { background: #f3e8ff; color: #6b21a8; }

/* Переключатель внутри полки стоит в один рост с плитками. */
.ribbon-panel .segmented { align-self: stretch; }
.ribbon-panel .segmented .seg { flex-direction: column; width: 74px; }

/*
 * Разбор несделанного инструмента.
 *
 * Три строки, и все три обязательны: что уже есть, чего не хватает, что делать
 * сейчас. Отписка «этого ещё нет» не помогает работать — а работать человеку
 * надо сегодня, а не когда мы допишем.
 */
.tool-why {
  position: fixed;
  z-index: 80;
  max-width: 380px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel, #fff);
  color: var(--ink);
  box-shadow: 0 12px 30px rgba(15, 23, 42, .22);
  font-size: 13px;
  line-height: 1.4;
}
.tool-why strong { display: block; margin-bottom: 6px; }
.tool-why p { margin: 4px 0; }
.tool-why b { color: var(--muted); font-weight: 600; }
.tool-why .now { color: var(--ink); }
.tool-why .now b { color: #166534; }

/* ---------- дерево проекта ---------- */

#project-tree h2 { margin: 0 0 8px; }

.tree-branch { margin-bottom: 6px; }
.tree-branch > summary {
  cursor: pointer;
  user-select: none;
  font-size: 12px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 4px 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.tree-branch > summary::marker { color: var(--line); }
.tree-count {
  margin-left: auto;
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.tree-item {
  appearance: none;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  padding: 4px 8px 4px 16px;
  border-radius: var(--r-control);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 7px;
}
.tree-item:hover:not(:disabled) { background: rgba(15, 23, 42, .06); transform: none; }
/* Открытое сейчас — заливкой, а не только жирным: в длинном дереве жирное
   теряется, а заливку видно боковым зрением. */
.tree-item.active {
  background: rgba(29, 78, 216, .12);
  color: var(--brand);
  font-weight: 600;
}
.tree-item:disabled { color: var(--muted); cursor: default; opacity: 1; }
.tree-item .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.tree-item .dot[data-kind="supply"] { background: var(--danger); }
.tree-item .dot[data-kind="extract"] { background: var(--brand); }

.tree-empty {
  margin: 2px 0 6px 16px;
  font-size: 12px;
  color: var(--muted);
}

/* ---------- справочник ---------- */

.reference-panel {
  position: fixed;
  right: 16px;
  bottom: 62px;
  top: 96px;
  width: min(560px, calc(100vw - 32px));
  z-index: 40;
  display: flex;
  flex-direction: column;
  border-radius: var(--r-window);
  border: 1px solid var(--glass-edge);
  background: var(--glass-surface-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-lift-high);
  overflow: hidden;
}
.reference-panel > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--glass-edge);
}
.reference-body { flex: 1; overflow: auto; padding: 12px 14px 18px; }
.reference-body h3 { margin: 16px 0 6px; font-size: 14px; }
.reference-body table { width: 100%; }

/* ---------- откуда взялась величина ---------- */

/*
 * Цепочка «расход -> помещения -> основание кратности». Свёрнута по умолчанию:
 * смотрят «сколько» часто, спрашивают «почему» реже, и панель свойств обязана
 * оставаться короткой.
 */
.origin { margin-top: 10px; }
.origin > summary {
  cursor: pointer;
  color: var(--brand);
  font-size: 13px;
  padding: 3px 0;
  user-select: none;
}
.origin > summary:hover { text-decoration: underline; }
.origin p { margin: 4px 0; }

/* Ряд помещения — кнопка во всю ширину: щелчок ведёт к нему в таблицу. */
.origin-room {
  appearance: none;
  width: 100%;
  text-align: left;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  font: inherit;
  color: var(--ink);
  padding: 6px 4px;
  cursor: pointer;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1px 10px;
}
.origin-room:hover { background: rgba(29, 78, 216, .07); transform: none; }
.origin-room > b { font-weight: 600; }
.origin-flow { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Основание — во всю ширину под именем: оно длиннее и читается строкой. */
.origin-room > span:last-child { grid-column: 1 / -1; font-size: 12px; }

/*
 * Куда нас привели. Подсветка гаснет сама: в длинной таблице иначе не понять,
 * к какой строке был переход, а вечная подсветка стала бы шумом.
 */
.room-found { animation: room-found 2s ease-out; }
@keyframes room-found {
  from { background: rgba(29, 78, 216, .22); }
  to { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .room-found { animation: none; outline: 2px solid var(--brand); }
}

/* ---------- строка ответа ---------- */

/*
 * Полоса состояния ответа: свежесть, величины на допущениях, баланс,
 * замечания. Стеклянная — она обрамление, а не чертёж; правило записано выше,
 * у переменных стекла.
 *
 * `flex: none` обязателен: полоса — последний ребёнок гибкого столбца `body`,
 * и без него её сплющит, когда чертёж потребует места.
 */
.answer-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 7px 18px;
  flex: none;
  border-top: 1px solid var(--glass-edge);
  background: var(--glass-surface-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-sheen);
}

.answer-item {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  padding: 3px 9px;
  border-radius: var(--r-pill);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
}
.answer-item:hover:not(:disabled) { background: rgba(15, 23, 42, .07); transform: none; }
.answer-item:disabled { cursor: default; opacity: 1; }

/*
 * Точка состояния. Цветом сообщать НЕЛЬЗЯ: его различают не все. Цвет здесь
 * только подкрепляет — что именно не так, сказано словами в самой надписи, и
 * её довольно, даже если точки не видно вовсе.
 */
.answer-item::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.answer-item.ok { color: var(--ok); }
.answer-item.warn { color: var(--warn); }
.answer-item.idle { color: var(--muted); }
/* Ничего ещё не сделано — точка пустая, а не серая: отсутствие состояния и
   состояние «всё спокойно» обязаны выглядеть по-разному. */
.answer-item.idle::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px currentColor;
}

/* ----------------------------------------------------------------------
 * Плотный вид: глубина без прозрачности
 *
 * Всё, что ниже, работает всегда — и когда стекло разрешено, и когда нет.
 * Красота держится на четырёх вещах, ни одна из которых не требует размытия:
 * ЦВЕТ (тёмная полоса вверху против светлого поля), ТЕНЬ (панели лежат над
 * полем, а не врисованы в него), ЧЕРТА (ярусы разделены, а не угаданы) и
 * РИТМ (заголовки, отступы и скругления соразмерны).
 * ---------------------------------------------------------------------- */

/* Шапка — тёмная всегда. Это опора верхней части окна. */
header {
  background: var(--chrome);
  color: #f8fafc;
  box-shadow: 0 1px 0 rgba(15, 23, 42, .28);
}
header .brand strong { letter-spacing: .2px; }
header .brand span { color: #94a3b8; }

/*
 * Лента лежит ЯРУСОМ НИЖЕ шапки, и это видно без всякого стекла: своя заливка,
 * своя черта снизу. Полоса вкладок и полотно с кнопками — разного тона, и
 * открытая вкладка соединяется с полотном, а не висит над ним.
 */
.ribbon {
  background: var(--band);
  border-bottom: 1px solid var(--line);
  box-shadow: none;
}
.ribbon-tabs { border-bottom: 1px solid var(--line); padding: 6px 14px 0; }
.ribbon-tab {
  border: 1px solid transparent;
  border-bottom: none;
  margin-bottom: -1px;
  padding: 7px 15px;
}
.ribbon-tab:hover:not(.active) { background: rgba(15, 23, 42, .05); }
.ribbon-tab.active {
  background: var(--panel);
  border-color: var(--line);
  box-shadow: none;
}
/* Полоска цвета действия по верхней кромке открытой вкладки: «вы здесь»
   читается краем глаза, не отвлекая от чертежа. */
.ribbon-tab.active::after {
  content: "";
  position: absolute;
  left: -1px; right: -1px; top: -1px;
  height: 2px;
  background: var(--brand);
  border-radius: 2px 2px 0 0;
}
.ribbon-tab { position: relative; }
.ribbon-panel { background: var(--panel); }

/* Панели лежат НАД полем, и тень говорит об этом лучше рамки. */
aside, .content {
  box-shadow: var(--lift-1);
  border-radius: var(--r-panel);
}

/*
 * Заголовки в панелях — мелкие, разрядкой, приглушённые. Крупные тёмные
 * заголовки перетягивали внимание на себя, хотя главное в окне — чертёж.
 * Тот же приём, что в дереве проекта: панели читаются как один язык.
 */
aside h2 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}

/* Строка ответа — тот же ярус, что лента, только снизу. */
.answer-bar {
  background: var(--band);
  border-top: 1px solid var(--line);
  box-shadow: none;
}

/* Плавающее — с настоящей тенью: оно лежит поверх чертежа. */
#axo-window, #section-window, .reference-panel { box-shadow: var(--lift-3); }
.toast, .hint { box-shadow: var(--lift-2); }

/* Кнопки: тонкая черта и лёгкий подъём вместо заливки-пятна. */
button { box-shadow: var(--lift-1); }
button.ghost { box-shadow: none; }
button.primary { border-color: transparent; box-shadow: var(--lift-1); }
button:disabled { box-shadow: none; }

/* ---------- объекты: препятствия из слоя «Объекты» ---------- */

/*
 * Фиолетовый — цвет этого слоя, и он выбран не для красоты. Красный и синий
 * заняты притоком и вытяжкой, серый — подложкой чертежа. Фиолетовый ни с чем
 * из них не спорит и сразу говорит: это не наше изделие, а поставленное
 * препятствие.
 */
.plan-object {
  fill: #a855f7;
  fill-opacity: .10;
  stroke: #7e22ce;
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 6 4;
}
/* Выделения объекта на плане пока нет — правило удалено вместе с мёртвым кодом. */
.plan-object-label {
  fill: #6b21a8;
  font: 600 260px system-ui, sans-serif;
  text-anchor: middle;
  dominant-baseline: middle;
  pointer-events: none;
}

/*
 * Обводка, которую ведут прямо сейчас. Мерцание — не украшение: пока рамка
 * тянется, она ничем не отличалась бы от уже поставленного объекта, и человек
 * не понимал бы, ставит он сейчас или смотрит на прежнее.
 *
 * Штрих бежит, свечение дышит. Ход подобран медленный: быстрое мерцание рядом
 * с чертежом мешает целиться.
 */
.plan-object.ghost {
  fill: #a855f7;
  fill-opacity: .16;
  stroke: #a855f7;
  stroke-width: 2.4;
  stroke-dasharray: 10 6;
  filter: drop-shadow(0 0 6px rgba(168, 85, 247, .85));
  animation: object-spark 1.1s linear infinite, object-glow 1.6s ease-in-out infinite;
}
@keyframes object-spark { to { stroke-dashoffset: -32; } }
@keyframes object-glow {
  0%, 100% { fill-opacity: .10; stroke-opacity: .75; }
  50%      { fill-opacity: .24; stroke-opacity: 1; }
}

/* Поле «что здесь стоит» — у самой рамки, а не системным окном браузера. */
.object-input {
  position: fixed;
  z-index: 60;
  padding: 8px 12px;
  border: 2px solid #a855f7;
  border-radius: 8px;
  background: #fff;
  color: #0f172a;
  font: 500 14px system-ui, sans-serif;
  box-shadow: 0 0 0 4px rgba(168, 85, 247, .18), 0 10px 24px rgba(15, 23, 42, .22);
  outline: none;
}
.object-input::placeholder { color: #a78bfa; font-weight: 400; }

/* Строка объекта в панели. */
.object-row {
  border: 1px solid var(--line);
  border-left: 3px solid #a855f7;
  border-radius: 6px;
  padding: 8px;
  margin-bottom: 8px;
}
.object-head { display: flex; gap: 6px; align-items: center; }
.object-name { flex: 1; min-width: 0; font-weight: 600; }
.object-row label.row { display: flex; justify-content: space-between; gap: 8px;
  align-items: center; margin-top: 4px; }
.object-row label.row input { width: 84px; }

/* ---------- разбор правок трассы ---------- */

/*
 * Панель показывает измеренное и молчит о причинах. Поэтому и вид у неё
 * сдержанный: ни одного цветного значка «важно», ни одной стрелки вверх. Цвет
 * тут ровно один и означает ровно одно — трассу тянут к жесту или от него.
 */
.corr-head { margin: 0 0 6px; font-size: 13px; }
.corr-line {
  margin: 6px 0;
  padding: 7px 9px;
  border-radius: 6px;
  border-left: 3px solid var(--line);
  background: rgba(15, 23, 42, .04);
  font-size: 12px;
  line-height: 1.35;
}
.corr-line.к-жесту { border-left-color: #16a34a; }
.corr-line.от-жеста { border-left-color: #d97706; }
.corr-line.одна-сторона { border-left-color: #7c3aed; }

.corr-table {
  width: 100%;
  margin-top: 8px;
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.corr-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: 10px;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding-bottom: 3px;
}
.corr-table td { padding: 3px 0; border-top: 1px solid var(--line); }
.corr-table th:not(:first-child), .corr-table td:not(:first-child) { text-align: right; }
.corr-table tr[data-edge-id] { cursor: pointer; }
.corr-table tr[data-edge-id]:hover td { background: rgba(15, 23, 42, .05); }

/* ---------- панель слоёв ---------- */

/*
 * Список слоёв. Три вещи в строке: выключатель, имя, число элементов.
 *
 * Число стоит не для красоты. Слой, в котором ничего нет, выключателем не
 * отличить от слоя, который не работает: щёлкаешь — на чертеже ничего не
 * меняется. Пустые строки поэтому глушатся и прямо подписаны «пусто».
 */
.layers { display: flex; flex-direction: column; gap: 1px; }

.layer-group {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 12px 0 4px;
}
.layer-group:first-child { margin-top: 0; }

.layer-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  margin: 0 -6px;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.25;
  cursor: pointer;
}
.layer-row:hover { background: var(--hover, rgba(15, 23, 42, .05)); }
.layer-row input { margin: 0; flex: none; }
.layer-name { flex: 1; min-width: 0; }
/* Число прижато к правому краю и набрано цифрами одной ширины: столбец чисел
   читают взглядом сверху вниз, а не по одной строке. */
.layer-count {
  flex: none;
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.layer-row.empty { cursor: default; color: var(--muted); }
.layer-row.empty:hover { background: none; }
.layer-row.empty .layer-name { opacity: .65; }

/* Мазок цвета у системы. Он же и легенда: отдельная картинка «приток/вытяжка»
   рядом со списком повторяла бы то, что видно в самом списке. */
.layer-swatch {
  flex: none;
  width: 14px; height: 8px;
  border-radius: 2px;
  border: 1px solid var(--line);
}
/* Цвета — те же, что были в легенде «приток/вытяжка» и что на самом чертеже. */
.layer-swatch.supply  { background: #fed7aa; border-color: #b91c1c; }
.layer-swatch.extract { background: #bfdbfe; border-color: #1d4ed8; }

/* ----------------------------------------------------------------------
 * Когда стекла быть не должно
 *
 * ЭТИ БЛОКИ ОБЯЗАНЫ ОСТАВАТЬСЯ ПОСЛЕДНИМИ В ФАЙЛЕ. Они отменяют прозрачность
 * селекторами той же силы, что и правила, которые её задают, — значит решает
 * порядок. Допиши после них новый раздел со стеклом, и просьба человека убрать
 * прозрачность перестанет действовать именно для него, молча.
 *
 * Я на этом уже попался: лента, строка ответа и справочник были дописаны ниже
 * и оставались стеклянными при включённом «меньше прозрачности», пока всё
 * остальное окно послушно становилось плотным.
 * ---------------------------------------------------------------------- */

/*
 * Убираем ПРОЗРАЧНОСТЬ, а не цвет.
 *
 * Прежний запасной путь красил всё подряд в белое — и тёмную шапку заодно. Вид
 * не просто беднел, он разваливался: пропадала тёмная полоса, на которой
 * держится вся верхняя часть окна, и приложение становилось похоже на страницу
 * без стилей вообще. Это была не бережность, а порча.
 *
 * Правило теперь одно: у поверхности отнимается только полупрозрачность и
 * размытие; её ЦВЕТ остаётся тем же. Тёмное остаётся тёмным, белое белым.
 * Плотный вид — основной, а стекло — добавка к нему, а не наоборот.
 */
@media (prefers-reduced-transparency: reduce) {
  body { background: var(--bg); }
  header, .project-bar, aside, .content, #axo-window, #section-window,
  .axo-head, button, select, input,
  .ribbon, .answer-bar, .reference-panel, .hint, .toast {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  /*
   * Перекрашиваем ТОЛЬКО то, у чего заливка полупрозрачная. Лента и строка
   * ответа сюда не входят: их цвет `--band` плотный сам по себе, и подмена
   * белым отняла бы у них ярус — ровно та ошибка, из-за которой окно выглядело
   * страницей без стилей.
   */
  .reference-panel, .axo-head { background: var(--panel); }
  #axo-window, #section-window { background: var(--band); }
  .hint, .toast { background: var(--chrome); }
  .toast.error { background: var(--danger); }
}

/*
 * Браузер без `backdrop-filter` показал бы полупрозрачные панели без размытия —
 * сквозь них читался бы чертёж, и текст стал бы неразборчивым. Там, где
 * размывать нечем, поверхности плотные. Цвет и здесь сохраняется.
 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .reference-panel, .axo-head { background: var(--panel); }
  #axo-window, #section-window { background: var(--band); }
  .hint, .toast { background: var(--chrome); }
}

/* Уважение к «поменьше движения»: подъём кнопки — это движение. */
@media (prefers-reduced-motion: reduce) {
  button { transition: none; }
  button:hover:not(:disabled), button:active:not(:disabled) { transform: none; }
  /*
   * Мерцание обводки — тоже движение, и притом у самого курсора. Гасим его,
   * но не саму рамку: она обязана остаться заметной, иначе человек не поймёт,
   * ведёт он её сейчас или смотрит на уже поставленный объект. Свечение и
   * плотная заливка справляются с этим и без анимации.
   */
  .plan-object.ghost, .sec-object.ghost { animation: none; fill-opacity: .24; }
}

/* SESSION login is the outermost admission boundary. */
.cerix-app-shell { display: contents; }
body.session-auth-required .cerix-app-shell { visibility: hidden; }
.session-login-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(15, 23, 42, .72);
  backdrop-filter: blur(4px);
}
.session-login-overlay[hidden] { display: none; }
body[data-cerix-auth-mode="disabled"] .session-login-overlay { display: none; }
.session-login-card {
  width: min(92vw, 430px);
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 24px;
  box-shadow: 0 22px 70px rgba(15, 23, 42, .45);
}
.session-login-card h1 { margin: 0 0 8px; font-size: 24px; }
.session-login-card > p.muted { margin: 0 0 16px; }
#session-login-form { display: grid; gap: 12px; }
#session-login-form label { display: grid; gap: 5px; font-size: 13px; }
#session-login-form input {
  min-width: 0;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
}
#session-login-form button { margin-top: 2px; }
.session-login-error { color: var(--danger); margin: 12px 0 0; }
.session-account {
  display: flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
  font-size: 12px;
}
.session-account[hidden] { display: none; }
body.session-auth-required { overflow: hidden; }

/* First-account acceptance and administrator-created personal invitations. */
.session-account-overlay {
  position: fixed;
  inset: 0;
  z-index: 410;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(15, 23, 42, .72);
  backdrop-filter: blur(4px);
}
.cerix-app-shell > .session-account-overlay { z-index: 350; }
.session-account-overlay[hidden],
body[data-cerix-auth-mode="disabled"] .session-account-overlay { display: none; }
.session-account-card {
  width: min(92vw, 480px);
  max-height: min(90vh, 720px);
  overflow: auto;
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 24px;
  box-shadow: 0 22px 70px rgba(15, 23, 42, .45);
}
.session-account-card h1,
.session-account-card h2 { margin: 0 0 8px; }
.session-account-card > p.muted { margin: 0 0 16px; }
#session-invitation-accept-form,
#session-invite-form,
.session-invite-result { display: grid; gap: 12px; }
#session-invite-form[hidden],
.session-invite-result[hidden] { display: none; }
#session-invitation-accept-form label,
#session-invite-form label,
.session-invite-result label { display: grid; gap: 5px; font-size: 13px; }
#session-invitation-accept-form input,
#session-invite-form input,
#session-invite-form select,
.session-invite-result textarea {
  min-width: 0;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
}
.session-invite-result textarea { resize: vertical; overflow-wrap: anywhere; }
.session-account-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 2px;
}

/*
 * Онбординг беты: оверлей имени и бейдж инженера в шапке.
 *
 * Оверлей намеренно закрывает ВСЁ приложение: пока журнал общий, а имя не
 * названо, безымянных правок быть не должно. Это заслон, а не приветствие.
 */
.onboarding-overlay {
  position: fixed;
  inset: 0;
  z-index: 300; /* выше панелей истории и проверки */
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 23, 42, .55);
  backdrop-filter: blur(2px);
}
/* display:flex выше перебивает UA-правило [hidden] — возвращаем его явно,
   иначе «скрытый» оверлей продолжал бы накрывать страницу и глотать клики. */
.onboarding-overlay[hidden] { display: none; }
.onboarding-card {
  width: min(92vw, 420px);
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 22px 24px;
  box-shadow: 0 18px 50px rgba(15, 23, 42, .35);
}
.onboarding-card h2 { margin: 0 0 6px; font-size: 20px; }
.onboarding-card p.muted { margin: 0 0 14px; }
#onboarding-form { display: flex; gap: 8px; }
#onboarding-name {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 15px;
}
.onboarding-error { color: var(--danger); margin: 10px 0 0; }

.engineer-badge { white-space: nowrap; }
.engineer-badge-icon { margin-right: 4px; }

/*
 * Ручки на границах панелей (panels.js). Сами границы — линии сетки main;
 * ручка — невидимая полоса поверх зазора, о которой сообщает только курсор
 * и лёгкая подсветка. Ловить её не нужно: полоса шире видимого зазора.
 */
.panel-handle {
  position: absolute;
  top: 12px;
  bottom: 12px;
  width: 10px;
  cursor: col-resize;
  z-index: 60;
  border-radius: 4px;
}
.panel-handle:hover, .panel-handle.dragging { background: rgba(29, 78, 216, .16); }
.panel-handle-left { left: calc(var(--panel-left, 280px) + 13px); }
.panel-handle-right { right: calc(var(--panel-right, 250px) + 13px); }

/* Верхняя кромка таблиц: та же ручка, но горизонтальная и в потоке flex. */
.panel-handle-bottom {
  position: relative;
  top: auto; bottom: auto;
  width: auto;
  height: 8px;
  margin: -3px 0 -5px;
  flex: 0 0 auto;
  cursor: row-resize;
  z-index: 60;
  border-radius: 4px;
}

/*
 * Вкладка «Файл»: полоса проектов и выбор чертежа переехали сюда из шапки
 * (решение владельца — шапка распухала на три строки). Свои «стеклянные»
 * фоны и рамки они носили ДЛЯ шапки; внутри ленты фон уже есть у панели.
 */
.ribbon-panel[data-stage="file"] {
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
}
.ribbon-panel[data-stage="file"] .project-bar,
.ribbon-panel[data-stage="file"] .opener {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom: none;
  box-shadow: none;
  padding: 2px 0;
  position: static;
}

/*
 * Лента после переезда шапки (просьба владельца, 2026-08-18).
 *
 * Бренд стоит в правом углу строки вкладок; статистика чертежа — рядом с
 * выбором чертежа во вкладке «Файл». Панели всех вкладок держат ОДНУ высоту
 * (эталон — «Воздуховоды»): лента не прыгает при переключении, а что не
 * влезло по ширине — уезжает в горизонтальную прокрутку, как в Office.
 */
.ribbon-tabs { align-items: center; }
.ribbon-tabs .brand {
  margin-left: auto;
  white-space: nowrap;
  padding: 0 12px 0 16px;
}
.ribbon-tabs .brand strong { font-size: 16px; }
@media (max-width: 1180px) {
  .ribbon-tabs .brand span { display: none; }
}

.ribbon-panel {
  height: 95px;
  min-height: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
}
.ribbon-panel[data-stage="file"] { gap: 0; }
.ribbon-panel[data-stage="file"] .project-bar,
.ribbon-panel[data-stage="file"] .opener {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
}
.ribbon-panel[data-stage="file"] .opener .stats {
  margin-left: auto;
  white-space: nowrap;
  padding-right: 8px;
}

/* Project identity and save feedback stay in the existing tab row. */
.ribbon-topline {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--line);
  min-width: 0;
}
.ribbon-topline .ribbon-tabs {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  flex-wrap: nowrap;
  border-bottom: none;
}
.ribbon-topline .ribbon-tab { flex: 0 0 auto; white-space: nowrap; }
.ribbon-topline .brand span { display: none; }
.workspace-project {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 45%;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 10px 4px 4px;
  padding: 4px 8px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink);
  font-size: 12px;
  text-align: left;
}
.workspace-project:hover { background: var(--panel); border-color: var(--line); }
.workspace-project:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.workspace-project-name {
  max-width: 24ch;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.workspace-project .save-status { margin-left: 0; font-size: 12px; }
.workbench-design-toggle {
  flex: 0 0 auto; align-self: center; margin: 4px 10px 4px 0;
  padding: 5px 10px; font-size: 12px; white-space: nowrap;
}
#workbench-header[hidden], #workbench-command-overlay[hidden] { display: none; }
body:not([data-cerix-design="spectrum"]) .workbench-only { display: none; }
.ribbon-panel[data-stage="file"] .project-bar > button,
.ribbon-panel[data-stage="file"] .opener > button {
  flex: 0 0 auto;
  white-space: nowrap;
}
.ribbon-panel[data-stage="file"] .project-bar > span,
.ribbon-panel[data-stage="file"] .project-bar > label {
  flex: 0 0 auto;
  white-space: nowrap;
}
.ribbon-panel[data-stage="file"] .project-last-actor {
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ribbon-panel[data-stage="file"] .project-bar > select,
.ribbon-panel[data-stage="file"] .opener > select {
  flex: 0 0 auto;
  max-width: 34ch;
}

/* Keep room identity in view while reading the wide engineering schedule. */
#rooms-table table.grid { border-collapse: separate; border-spacing: 0; }
#rooms-table .grid tr > :nth-child(1),
#rooms-table .grid tr > :nth-child(2) {
  position: sticky;
  background: #fff;
  z-index: 2;
}
#rooms-table .grid tr > :nth-child(1) {
  left: 0;
  box-sizing: border-box;
  min-width: 38px;
  width: 38px;
  max-width: 38px;
}
#rooms-table .grid tr > :nth-child(2) {
  left: 38px;
  box-shadow: 2px 0 0 var(--line);
}
#rooms-table .grid input.name { width: 200px; }
#rooms-table .grid tr.selected > :nth-child(-n+2) { background: #fef9c3; }
#rooms-table .grid tr.transit > :nth-child(2) { opacity: 1; }
#rooms-table .grid tr.transit > :nth-child(2) input { opacity: .55; }
#rooms-table .grid thead tr > :nth-child(-n+2) { z-index: 3; background: #f8fafc; }

/*
 * Строка ИИ — заглушка чата (решение владельца, 2026-08-18). Прежняя строка
 * ответа скрыта: её узлы живут и обновляются, но места не занимают.
 */
#answer-bar { display: none; }

body[data-cerix-future-ui="hidden"] #ai-bar { display: none; }
body[data-cerix-future-ui="hidden"] #answer-bar { display: flex; }

.ai-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  background: var(--panel);
  border-top: 1px solid var(--line);
}
.ai-bar-sees {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #f8fafc;
  color: var(--muted);
  font-size: 12.5px;
  white-space: nowrap;
}
.ai-bar-sees b { color: var(--ink); font-weight: 600; }
#ai-bar-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13.5px;
  background: #fff;
}
#ai-bar-input::placeholder { color: var(--muted); }
.ai-bar-icon {
  flex: none;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  border-radius: 50%;
  font-size: 15px;
  color: var(--muted);
  cursor: pointer;
}
.ai-bar-icon:hover { background: rgba(15, 23, 42, .06); }
.ai-bar-send {
  flex: none;
  width: 34px;
  height: 34px;
  border: none;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-ink);
  font-size: 14px;
  cursor: pointer;
}
.ai-bar-send:hover { filter: brightness(1.08); }

/* ======================================================================
   Окно «Экспорт в DXF» — тот же рисунок, что у онбординга: оверлей с
   карточкой. [hidden] возвращается явно по той же причине: display:flex
   перебивает UA-правило, и «скрытое» окно глотало бы клики.
   ====================================================================== */
.export-overlay {
  position: fixed;
  inset: 0;
  z-index: 290; /* ниже онбординга: имя спрашивается раньше всего */
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 23, 42, .55);
  backdrop-filter: blur(2px);
}
.export-overlay[hidden] { display: none; }
.export-card {
  width: min(92vw, 460px);
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 22px 24px;
  box-shadow: 0 18px 50px rgba(15, 23, 42, .35);
}
.export-choices {
  margin-top: 10px;
  max-height: 46vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.export-choice { gap: 8px; align-items: center; cursor: pointer; }
.export-choice input[disabled] + span,
.export-choice:has(input[disabled]) { color: var(--muted); cursor: default; }
.export-actions { justify-content: flex-end; gap: 8px; margin-top: 14px; }
/* Окно выбора режима правки сечений (MVP-31) берёт вид карточки экспорта:
   своих цветов не заводит, различаются только тексты и кнопки. */
.section-mode-list {
  margin: 10px 0 4px;
  padding-left: 18px;
  color: var(--muted);
  line-height: 1.5;
}
.section-mode-list b { color: var(--ink); }
/* Список ручных правок: строка на участок, размер и кнопка «снять». */
.overrides-list { display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
.overrides-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}
.overrides-row .overrides-edge { color: var(--muted); min-width: 54px; }
.overrides-row .overrides-size { font-weight: 600; }
.overrides-row .overrides-was { color: var(--muted); }
.overrides-row button { margin-left: auto; }

/* Боковая решётка на дальней грани короба (MVP-33): невидимое показывают
   штриховой — так же, как всё, что за плоскостью разреза. */
/*
 * Решётка на ДАЛЬНЕЙ грани короба (MVP-33) — штриховой линией.
 *
 * Чертёжная норма: невидимое показывают штрихом. Заливки нет — иначе
 * скрытое изделие спорило бы по весу с видимым; штрих длиннее просвета,
 * чтобы линия читалась как штриховая, а не как точечная, и не путалась
 * со штрихпунктиром контуров за плоскостью.
 */
/*
 * РУЧКИ ПЕРЕПАДОВ И РАМКА ПРАВКИ (MVP-34).
 *
 * Ручка стоит на самом перепаде и потому мала: она не предмет чертежа,
 * а место, за которое берутся. Зона захвата вокруг неё шире самой
 * ручки и прозрачна — тем же приёмом, что у решёток и воздуховодов.
 *
 * Призрак — то, что получится, если отпустить сейчас. Штрихом и
 * приглушённо: он предложение, а не положение дел.
 */
/*
 * ПЕРЕХОД НА ОСЕВОМ ПЛАНЕ (MVP-35) — стрелка по оси, остриём в сторону
 * уменьшения. Габарита на осевом плане нет, трапецию рисовать не на
 * чем, а узел показать надо: его заказывают и монтируют.
 *
 * Зона захвата шире самой стрелки и прозрачна — тем же приёмом, что у
 * решёток и воздуховодов: линия в масштабе этажа это волосок.
 */
.duct-transition-axis {
  stroke: var(--air-ink); stroke-width: 1.8; fill: none;
  vector-effect: non-scaling-stroke; stroke-linecap: round;
}
.duct-transition.axis[data-kind="extract"] .duct-transition-axis {
  stroke: #0369a1;
}
.duct-transition-hit {
  stroke: transparent; stroke-width: 16; fill: none;
  vector-effect: non-scaling-stroke; cursor: ew-resize;
}

.axo-grip {
  fill: #fff; stroke: var(--air-accent); stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.axo-grip[data-kind="extract"] { stroke: #0369a1; }
.axo-grip-hit {
  fill: transparent; stroke: none; cursor: ns-resize;
}
.axo-grip-hit:hover ~ .axo-grip { fill: var(--air-accent); }
#axo-grips:has(.axo-grip-hit:hover) .axo-grip { fill: #fde68a; }
.axo-grip-ghost {
  fill: none; stroke: var(--air-accent); stroke-width: 2;
  stroke-dasharray: 10 6; vector-effect: non-scaling-stroke;
  opacity: .85;
}
.axo-grip-label, .axo-span-label {
  fill: var(--air-ink); font-family: system-ui, sans-serif;
  font-size: 420px; font-weight: 600;
  paint-order: stroke; stroke: #fff; stroke-width: 120px;
  stroke-linejoin: round;
}
.axo-span-tick {
  stroke: #b45309; stroke-width: 2; vector-effect: non-scaling-stroke;
}
.axo-span-body {
  fill: none; stroke: #b45309; stroke-width: 5;
  vector-effect: non-scaling-stroke; opacity: .5;
  stroke-linecap: round;
}

.sec-grille-body.far-side,
.sec-behind-grille-body.far-side { fill: none; stroke-dasharray: 8 4; }

/*
 * Бледная стрелка переворота (MVP-33): та же стрелка раздачи, но на другую
 * грань короба. Она ПРЕДЛОЖЕНИЕ, а не положение дел, поэтому светлее и
 * тоньше — спутать её с настоящей раздачей нельзя. Под курсором проявляется:
 * жест должен отзываться до нажатия, иначе он не читается как жест.
 */
.grille-throw.side-arrow.ghost { opacity: .28; stroke-width: 1.1; }
.grille-throw.side-arrow-head.ghost { opacity: .28; }
/* Знак «перевернуть»: дуга разворота с галочкой на конце. Не стрелка —
   стрелка на плане вентиляции значит поток, и рядом с настоящей раздачей
   вторая читалась как «дует в обе стороны». Дуга бледная, пока на неё не
   навели: это предложение, а не то, что уже сделано. */
.grille-flip-mark {
  fill: none; stroke: var(--air-accent); stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round; opacity: .3;
  vector-effect: non-scaling-stroke; pointer-events: none;
  stroke-dasharray: 6 4;
}
.grille-flip-mark.head { stroke-dasharray: none; stroke-width: 1.8; }
.grille-flip {
  stroke: transparent; stroke-width: 14; fill: none;
  vector-effect: non-scaling-stroke; cursor: pointer;
}
.grille-item:has(.grille-flip:hover) .grille-throw.side-arrow.ghost,
.grille-item:has(.grille-flip:hover) .grille-throw.side-arrow-head.ghost {
  opacity: .75;
}
.grille-item:has(.grille-flip:hover) .grille-flip-mark { opacity: .85; }


/* Решётка, заданная рукой поверх расчёта (MVP-32). Отличается штриховой
   обводкой — цвет у решётки уже занят направлением воздуха. */
.grille.by-hand { stroke-dasharray: 3 2; stroke-width: 1.6; }

/* Размер, поставленный рукой поверх расчёта. Отличается начертанием, а не
   цветом: подпись на плане и так красится по направлению системы. */
.duct-label.section-manual {
  font-style: italic;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --- Экран «Мой почерк» ---------------------------------------------
   Панель повторяет поведение ящика проверки: правая колонка поверх
   рабочего поля, скрытая атрибутом hidden. Собственных цветов не заводит
   и живёт на тех же переменных, что и остальной интерфейс. */
.style-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  width: min(94vw, 420px);
  overflow-y: auto;
  padding: 16px 18px;
  background: var(--panel);
  border-left: 1px solid var(--line);
  box-shadow: -12px 0 40px rgba(15, 23, 42, .28);
}
.style-panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.style-panel-head h2 { margin: 0; }
.style-rules, .style-sources { list-style: none; margin: 8px 0 0; padding: 0; }
.style-rules > li { padding: 10px 0; border-top: 1px solid var(--line); }
.style-sources > li { padding: 4px 0; }
.style-rule-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.style-rule-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Отклонённое правило остаётся видимым, но гаснет: инженер должен помнить,
   что он это уже отверг, иначе он отвергнет ту же строку второй раз. */
.style-rule-rejected .style-rule-head { opacity: .55; }
.style-consent select { width: 100%; }

/* Плюсик «добавить этаж» в полосе видов. Отличается от вкладки-вида
   намеренно: это не место, куда переключаются, а действие. */
.view-tab-add {
  font-weight: 600;
  min-width: 2em;
  opacity: .75;
}
.view-tab-add:hover { opacity: 1; }

/* Перекрытия на аксонометрии. Тоньше и бледнее направляющей курсора: это
   подложка, по которой читают высоту, а не то, что чертят. */
.axo-floor-line {
  /* non-scaling-stroke — как у направляющей курсора рядом: без него линия
     толстеет при отдалении и перекрывает трассу. */
  stroke: #64748b; stroke-width: 1.1; stroke-dasharray: 10 7;
  vector-effect: non-scaling-stroke; pointer-events: none; opacity: .5;
}
.axo-floor-label {
  fill: #64748b; font: 500 220px system-ui, sans-serif;
  paint-order: stroke; stroke: #fff; stroke-width: 60;
  pointer-events: none; opacity: .85;
}

/* ---------- Правка исходного чертежа (MVP-37) ---------- */
/* В режиме правки курсор говорит, что подложка стала предметом. */
.stage.source-edit { cursor: crosshair; }
/* Выбранная сущность исходника: цвет поверх любого класса ezdxf. */
.dxf-picked {
  stroke: #d97706 !important;
  stroke-width: 2.4 !important;
  vector-effect: non-scaling-stroke;
}
text.dxf-picked { fill: #d97706 !important; stroke: none !important; }
/* Убранная сущность не показывается, но остаётся в DOM: её можно вернуть. */
.dxf-deleted { display: none !important; }
/* Дорисованные линии: тем же серо-чёрным, что и чертёж. */
#overlay-source-lines .source-line {
  stroke: #1e293b; stroke-width: 1.4; vector-effect: non-scaling-stroke;
  fill: none; stroke-linecap: round;
}
#overlay-source-ui .source-line.live {
  stroke: #d97706; stroke-width: 1.4; stroke-dasharray: 6 4;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* Рамка: окно — сплошная синяя, секущая — пунктирная зелёная, как в CAD. */
#overlay-source-ui .source-window {
  fill: rgba(37, 99, 235, .08); stroke: #2563eb; stroke-width: 1;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
#overlay-source-ui .source-crossing {
  fill: rgba(22, 163, 74, .08); stroke: #16a34a; stroke-width: 1;
  stroke-dasharray: 5 3; vector-effect: non-scaling-stroke; pointer-events: none;
}
#source-edit-toggle.active, #source-line.active { background: #fde68a; }
.source-edits { list-style: none; margin: 0; padding: 0; font-size: 12px; }
.source-edits li {
  display: flex; justify-content: space-between; gap: 6px; align-items: center;
  padding: 3px 0; border-bottom: 1px solid #e2e8f0;
}
