/* Нод-редактор шаблонов. Токены и правила системы — DESIGN_SYSTEM.md (там же объяснено, почему
   значения именно такие); история решений — NODE_EDITOR_PLAN.md §3. */

/* Токены и @font-face переехали в tokens.css: они принадлежат всему приложению, а не канвасу.
   Здесь остались только правила нод-редактора. */

/* ---------- страница ---------- */

.graph-page {
    display: grid;
    grid-template-columns: 1fr auto;
    /* Высота считается от токена, а не литералом: шапки-аппбара не стало, и подобранное «-160px»
       оставило бы канвас на 64 пикселя ниже, чем позволяет окно. */
    height: calc(100vh - var(--fb-page-chrome));
    min-height: 480px;
    border: 1px solid var(--fb-line);
    border-radius: 12px;
    overflow: hidden;
    background: var(--fb-surface);
}

/* Палитра есть только у конструктора; прогон рисуется тем же графом, но добавлять там нечего,
   поэтому третья колонка заводится модификатором, а не в базовом классе. */
.graph-page.has-rail { grid-template-columns: auto 1fr auto; }

/* Граф как РАЗДЕЛ страницы, а не сама страница: на витрине шаблона над ним стоят примеры, под ним —
   полоса действий, и высота во весь экран увела бы кнопку за край прокрутки. Правило про то же
   `height`, что в базовом классе, поэтому модификатором, а не своей вёрсткой: панорама, зум и
   пипки должны остаться теми же — это тот же объект, просто в меньшем окне.

   min(), а не литерал: на невысоком ноутбуке 520px забрали бы весь экран, а на большом мониторе
   56vh дали бы графу больше места, чем ему нужно. */
.graph-page.is-embed {
    height: min(56vh, 520px);
    min-height: 320px;
}

/* ---------- палитра нод ---------- */

/* Узкий рельс слева — тот же приём, что у материалов в попапе монтажа: список, из которого берут,
   стоит с краю и не спорит с рабочей областью. Фон как у инспектора: панель не главная. */
.graph-rail {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* 72, а не 64: в 64 самое длинное слово группы («Материалы») обрезалось. */
    width: 72px;
    padding: 8px 6px;
    background: var(--fb-bg);
    border-right: 1px solid var(--fb-line);
    overflow-y: auto;
}

/* Заголовок раздела: «Генерация» против «Локальных» — ходит ли нода к провайдеру. Не кнопка и не
   плитка, поэтому и выглядит подписью, а не третьим видом элемента рельса. */
.graph-rail-section {
    margin: 10px 0 2px;
    color: var(--fb-ink-faint, var(--fb-ink-dim));
    font-family: var(--fb-ui);
    font-size: 8.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    text-align: center;
    opacity: .7;
}

.graph-rail-section:first-child { margin-top: 0; }

/* Плитка рельса: MudMenu рисует обычную кнопку «иконка + подпись в строку», а нам нужна подпись под
   иконкой и вся ширина рельса под клик. Плитка всегда раскрывает список — прямых кнопок «создать
   шаг» на рельсе нет: они выглядели бы так же, а делали другое. */
.graph-rail > .mud-menu { width: 100%; }

.graph-rail .mud-menu .mud-button-root {
    display: flex;
    width: 100%;
    min-width: 0;
    padding: 7px 2px;
    border-radius: 6px;
    border: 1px solid transparent;
    color: var(--fb-ink-dim);
    text-transform: none;
    letter-spacing: .01em;
    font-family: var(--fb-ui);
    font-size: 9.5px;
    line-height: 1;
    transition: color .12s ease, border-color .12s ease, background .12s ease;
}

.graph-rail .mud-menu .mud-button-root:hover {
    color: var(--fb-ink);
    border-color: var(--fb-line);
    background: var(--fb-surface);
}

/* Иконка и подпись у MudButton лежат внутри .mud-button-label — колонку надо делать именно ему,
   иначе строка «иконка + слово» просто вылезает за 52 пикселя рельса и обрезается. */
.graph-rail .mud-menu .mud-button-label {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    width: 100%;
    min-width: 0;
}

.graph-rail .mud-menu .mud-button-label > .mud-icon-root,
.graph-rail .mud-menu [class*="mud-button-icon"] {
    margin: 0;
    font-size: 20px;
}

.palette-item {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* Маршрут связи в инспекторе: «откуда» над «куда», слева кромка цвета типа — та же, что у провода
   на канвасе, поэтому связь опознаётся без чтения имён. */
.edge-route {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 10px;
    border-left: 3px solid var(--wire, var(--fb-line));
    border-radius: 0 6px 6px 0;
    background: var(--fb-hover);
}

/* Точка выхода: тот же цвет, что будет у кромки созданной ноды и у её пипки. Серая означает
   «тип выяснится позже» — у статичного файла и материала прогона это правда. */
.palette-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
}

.graph-title {
    font-family: var(--fb-display);
    letter-spacing: var(--fb-tr-head);
    /* Имя шаблона рвётся, а не распирает строку: без этого длинное название не давало соседям
       перенестись и уводило шапку прогона за правый край окна. */
    min-width: 0;
    overflow-wrap: anywhere;
}

/* Полоса над графом — общая у прогона и у конструктора: обе несут стрелку «назад», имя, цену и
   ряд действий, и разъезжались в узком окне одинаково. В конструкторе было хуже: имени шаблона там
   не оставалось ширины вовсе, и оно вставало столбиком по букве на строку, отодвигая канвас за
   нижний край экрана. */
@media (max-width: 900px) {
    /* Имя забирает остаток первой строки, но делит её со стрелкой «назад»: строка, отданная одной
       стрелке, — потерянный ряд, а сама стрелка посреди пустоты читается как промах вёрстки.

       `flex-grow` здесь обязателен, а не только `min-width: 0`: сжимаемый до нуля заголовок никуда
       не девается — он переносится ПО БУКВЕ, и имя шаблона вставало в конструкторе вертикальным
       столбиком высотой в пол-экрана. */
    .fb-graph-head .graph-title { flex: 1 1 auto; }
}

@media (max-width: 560px) {
    /* Растягиваются только настоящие кнопки. Кнопка-значок квадратная по определению, и, попав под
       общее правило, она разъезжалась на всю строку — значок оказывался ровно по центру экрана и
       выглядел как заголовок, а не как «назад». */
    .fb-graph-head > .mud-button-root:not(.mud-icon-button) { flex: 1 1 auto; }
}

/* ---------- канвас ---------- */

/* Фон — ТОЧКИ, а не линейная сетка. Линии тут были ошибкой вдвойне: во-первых, сетка на канвасе —
   не документ, ей нечего разлиновывать, а решётка спорила с рамками нод и с проводами; во-вторых,
   двум слоям линий JS выдавал РАЗНЫЙ шаг (обычный и восьмикратный), и вместо квадратов получались
   растянутые прямоугольники. Точка — единственная текстура системы (DESIGN_SYSTEM.md §2), и на
   больших плоскостях она спокойнее линии. */
.graph-root {
    position: relative;
    overflow: hidden;
    background-color: var(--fb-canvas);
    background-image: radial-gradient(circle at 1px 1px, var(--grid-line) 1px, transparent 0);
    touch-action: none;
    cursor: default;
    user-select: none;
}

/* Один мягкий подсвет за графом — глубина без декора. */
.graph-root::before {
    content: "";
    position: absolute;
    inset: -20%;
    background: radial-gradient(closest-side, var(--fb-canvas-glow), transparent 70%);
    pointer-events: none;
}

.graph-root.is-far { background-image: none; }
.graph-root.is-panning { cursor: grabbing; }
.graph-root.is-dragging { cursor: grabbing; }

.graph-viewport {
    position: absolute;
    inset: 0;
    transform-origin: 0 0;
}

.graph-wires {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
}

.graph-wires path {
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    transition: opacity .15s ease;
}

/* Провод целиком в цвете типа материала — так его и описывает дизайн-система: «цвет порта и провода —
   цвет типа». Спокойствие общего плана даёт ПРОЗРАЧНОСТЬ, а не серая середина: раньше цвет держался
   лишь у концов, и на длинной кривой было не видно, что по ней идёт, пока взгляд не доходил до
   пипки. Заодно ушёл <linearGradient> на каждое ребро, координаты которого JS правил на каждый кадр
   панорамы. */
.graph-wires path[data-edge] {
    stroke: var(--wire, var(--fb-wire));
    opacity: .55;
}

/* При наведении на ноду её связи загораются целиком, остальной граф приглушается. */
.graph-wires path.hot {
    stroke: var(--wire, var(--fb-wire)) !important;
    stroke-width: 2.5;
    opacity: 1;
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--wire, var(--fb-wire)) 55%, transparent));
}

/* Выбранная связь. Одной сменой цвета тут не обойтись: провод и так горит цветом типа под курсором,
   и «выбрано» было бы неотличимо от «наведено». Поэтому три признака разом — яркая жила втрое толще,
   бегущий пунктир и ореол вокруг (см. .wire-hit.is-selected ниже). */
.graph-wires path[data-edge].is-selected {
    stroke: var(--wire, var(--fb-primary)) !important;
    stroke-width: 3.5;
    opacity: 1;
    stroke-dasharray: 10 6;
    filter: drop-shadow(0 0 8px color-mix(in srgb, var(--wire, var(--fb-primary)) 75%, transparent));
    animation: wire-march 1.1s linear infinite;
}

/* Бегущий пунктир: движение замечают боковым зрением, и выделенную связь не приходится искать
   глазами по графу. */
@keyframes wire-march {
    to { stroke-dashoffset: -16; }
}

/* Невидимый провод-двойник под курсор. В двухпиксельную кривую мышью не попасть, поэтому клик ловит
   он: прозрачная обводка поверх видимой. `pointer-events: stroke` включается точечно — у самого SVG
   он выключен, иначе провода перехватывали бы панораму и протяжку связей.

   Селектор обязан начинаться с `.graph-wires path`, а не быть просто `.wire-hit`: выше по файлу
   стоит `.graph-wires path { stroke-width: 2 }` со специфичностью (0,1,1), и одиночный класс (0,1,0)
   ей проигрывает — двойник оставался шириной в сам провод, и целиться приходилось ровно в него.
   С pointer-events этого не случилось только потому, что он наследуемый, а наследование уступает
   любому прямому правилу. */
.graph-wires path.wire-hit {
    stroke: transparent;
    /* 26 пикселей: провода идут кривыми и часто рядом, в узкую полосу приходилось целиться. */
    stroke-width: 14;
    pointer-events: stroke;
    cursor: pointer;
}

/* Ореол выделения — тот же двойник, только видимый. Полоса, по которой в провод попадают, и полоса,
   которой он подсвечен, оказываются одним и тем же: подсветка честно показывает зону клика. */
.graph-wires path.wire-hit.is-selected {
    stroke: color-mix(in srgb, var(--wire, var(--fb-primary)) 22%, transparent);
}

/* Пока тянут связь, двойники молчат: провод под курсором съедал бы бросок на ноду за ним. */
.graph-root.is-linking .graph-wires path.wire-hit { pointer-events: none; }

.graph-root.is-dimming .graph-wires path:not(.hot) { opacity: .22; }
.graph-root.is-dimming .graph-node:not(.hot) { opacity: .38; }

/* Приглушение выделенной связи не касается: она выбрана явно, а не подсвечена под курсором. Правило
   берёт и жилу, и ореол. Стоит НИЖЕ приглушения намеренно: специфичность у них одинаковая (0,4,1),
   и решает порядок — выше по файлу оно бы не сработало. */
.graph-root.is-dimming .graph-wires path.is-selected { opacity: 1; }

/* Провод в руке: пунктир, потому что связи ещё нет. */
.wire-temp {
    stroke: var(--wire, var(--fb-primary));
    stroke-width: 2.5;
    stroke-dasharray: 6 5;
    opacity: .9;
}

.graph-wires path.is-detached { opacity: .15; }

/* ---------- протяжка связи ---------- */

/* Пока тянут провод, граф отвечает на один вопрос: куда его можно положить. */
.graph-root.is-linking .graph-node { transition: opacity .12s ease; }
.graph-root.is-linking .graph-node.is-blocked { opacity: .3; }
.graph-root.is-linking .pip.is-blocked { opacity: .25; }

.graph-root.is-linking .pip.is-target {
    box-shadow: 0 0 0 3px var(--fb-primary-soft);
}

.graph-root.is-linking .pip.is-target.is-hover {
    background: var(--t, var(--fb-primary));
    box-shadow: 0 0 0 5px var(--focus-ring);
}

.graph-root.is-linking .graph-node { cursor: crosshair; }

/* ---------- имя связи на проводе ---------- */

/* Подпись: провод носит своё имя. В чипе стоит ровно тот токен, который пишется в промпте. */
.wire-chip {
    position: absolute;
    top: 0;
    left: 0;
    transform: translate(-50%, -50%);
    z-index: 1;
}

.wire-chip button,
.wire-chip-input {
    font-family: var(--fb-mono);
    font-size: 10px;
    line-height: 1;
    padding: 3px 7px;
    border-radius: 5px;
    border: 1px solid var(--fb-line);
    background: var(--fb-surface);
    color: var(--fb-ink-dim);
    white-space: nowrap;
    cursor: text;
}

.wire-chip-static {
    display: inline-block;
    font-family: var(--fb-mono);
    font-size: 10px;
    line-height: 1;
    padding: 3px 7px;
    border-radius: 5px;
    border: 1px solid var(--fb-line);
    background: var(--fb-surface);
    color: var(--fb-ink-dim);
    white-space: nowrap;
}

.wire-chip button:hover {
    border-color: var(--fb-primary);
    color: var(--fb-ink);
}

.wire-chip-input {
    width: 110px;
    outline: none;
    border-color: var(--fb-primary);
    color: var(--fb-ink);
}

/* Тот же тег в списке входов инспектора: одно понятие — одна форма записи, где бы оно ни стояло. */
.input-tag {
    flex: none;
    font-family: var(--fb-mono);
    font-size: 10px;
    line-height: 1;
    padding: 3px 6px;
    border-radius: 5px;
    border: 1px solid var(--fb-line);
    background: var(--fb-surface);
    color: var(--fb-ink-dim);
    white-space: nowrap;
}

.graph-root.is-dimming .wire-chip { opacity: .3; }
.graph-root.is-linking .wire-chip { opacity: .2; pointer-events: none; }

/* ---------- сцена ---------- */

/* Сцена — группировка, а не контейнер: рамка обводит ноды там, где они лежат. */
.scene-frame {
    position: absolute;
    border: 1px dashed var(--fb-line-hi);
    border-radius: 14px;
    background: var(--fb-hover);
    pointer-events: none;
}

.scene-frame-label {
    position: absolute;
    top: 6px;
    left: 14px;
    font-family: var(--fb-ui);
    font-size: 9px;
    font-weight: 600;
    letter-spacing: var(--fb-tr-eyebrow);
    text-transform: uppercase;
    color: var(--fb-ink-dim);
    opacity: .8;
}

/* ---------- нода ---------- */

/* Рамка ноды несёт цвет производимого типа — она заменила отдельную кромку сверху. Две линии
   говорили об одном и том же, а тонкая полоска в 2px на графе с двадцатью нодами не читалась вовсе;
   обведённая нода видна и на отдалении. Цвет приходит переменной --t с самой ноды (её ставит
   разметка), поэтому пипки со своим --t его не наследуют.

   Смешивание с --fb-line, а не чистый цвет типа: пять полностью цветных прямоугольников рядом
   превращают граф в витраж, а рамка обязана оставаться рамкой. Состояния (выделено, ждёт человека,
   упало, тронуто правкой) перекрывают этот цвет ниже — им важнее. */
.graph-node {
    position: absolute;
    top: 0;
    left: 0;
    width: 236px;
    border: 1px solid color-mix(in srgb, var(--t, var(--fb-t-none)) 42%, var(--fb-line));
    border-radius: var(--fb-r-card);
    background: var(--surface-node);
    box-shadow: var(--fb-shadow);
    color: var(--fb-ink);
    font-family: var(--fb-ui);
    transition: opacity .15s ease, border-color .15s ease, box-shadow .15s ease;
    cursor: grab;
}

.graph-node:hover { border-color: color-mix(in srgb, var(--t, var(--fb-t-none)) 75%, var(--fb-line)); }


.graph-node.is-selected {
    border-color: var(--fb-primary);
    box-shadow: var(--fb-inner-hi), var(--fb-ring), var(--fb-shadow);
}

.graph-node.is-error { border-color: var(--fb-danger); }

/* Полоса решения по правкам ИИ. Стоит над графом, а не в HUD: HUD рассказывает, что изменилось,
   а решение принимается один раз и на всё сразу — ему место рядом с «Сохранить». */
.fb-review {
    padding: 12px 16px;
    border: 1px solid var(--fb-patch);
    border-radius: var(--fb-r-card);
    background: var(--fb-grad);
    box-shadow: var(--fb-inner-hi), var(--fb-ring-patch);
}

.fb-review-text { min-width: 0; }

/* ---------- ревью правок от ИИ ---------- */

/* Один цвет на «тронуто правкой», а чем именно тронуто — говорит форма кромки и знак в списке
   правок. Второй оттенок тут было бы взять неоткуда: жёлтый значит «остановилось, нужен человек»,
   и покрасить им что-то ещё — значит потерять единственный сигнал, ради которого смотрят на экран. */
.graph-node.patch-added {
    border-color: var(--fb-patch);
    box-shadow: var(--fb-inner-hi), var(--fb-ring-patch), var(--fb-shadow);
}

.graph-node.patch-modified {
    border-color: var(--fb-patch);
    border-style: dashed;
}

/* Призрак удалённой ноды. Её уже нет в шаблоне, поэтому она не принимает ничего — ни клик, ни
   провод: pointer-events снимает её с пути жестов канваса, и трогать модуль ради этого не пришлось. */
.graph-node.patch-removed {
    opacity: .42;
    border-style: dashed;
    border-color: var(--fb-danger);
    filter: grayscale(.5);
    pointer-events: none;
}

.graph-node.patch-removed .node-title { text-decoration: line-through; }

/* Полоса «идёт работа». В покое высоты у неё нет: цвет типа держит рамка ноды, и вторая цветная
   линия сверху говорила бы то же самое дважды. Разворачивается только в фазах running и queued —
   см. ниже, там же анимация пробега. */
.node-rail {
    height: 0;
    /* Скругление под верхние углы ноды: обрезать её overflow'ом нельзя — пипки торчат за кромку
       намеренно, и их бы срезало вместе с полосой. */
    border-radius: var(--fb-r-card) var(--fb-r-card) 0 0;
}

/* ---------- голова ---------- */

.node-head {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
}

/* Значок вида шага. Квадрат с тинтом цвета типа — та же роль, что была у кромки: сказать, что нода
   отдаёт, до чтения имени. Заодно значок занял место эйрбоу («I2I», «TTS»): три латинские буквы
   стояли на самом заметном месте ноды и сообщали меньше, чем картинка с подсказкой. */
.node-kind {
    flex: none;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-xs);
    background: color-mix(in srgb, var(--t, var(--fb-t-none)) 16%, transparent);
    color: var(--t, var(--fb-t-none));
}

.node-kind svg { width: 13px; height: 13px; }

.node-title {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.25;
    /* Имя обрезается многоточием, а не переносится: голова обязана остаться одной строкой, иначе
       пипка выхода, привязанная к её середине, ездила бы по вертикали от длины названия. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Ключ результата — адрес ноды: им её зовут промпты, им называются файлы прогона, им подписаны
   входы соседей. Моноширинный, потому что это машинное значение (§4). */
.node-key {
    flex: none;
    font-family: var(--fb-mono);
    font-size: 9.5px;
    line-height: 1;
    padding: 3px 5px;
    border-radius: var(--fb-r-chip);
    border: 1px solid var(--fb-line);
    background: var(--bg-inset);
    color: var(--fb-ink-faint);
    max-width: 84px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------- порты ---------- */

.node-ports {
    list-style: none;
    margin: 0;
    padding: 2px 0 6px;
    border-top: var(--fb-div);
}

.port {
    position: relative;
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 3px 12px 3px 14px;
    font-size: 11.5px;
    color: var(--fb-ink-dim);
}

.port-label { overflow-wrap: anywhere; }

.port-count {
    margin-left: auto;
    font-family: var(--fb-mono);
    font-size: 10px;
    opacity: .75;
}

/* Порт, выросший из параметра модели: мельче и тише — их бывает много, и они не про поток данных,
   а про настройку. Отделяются от портов вида шага чертой. */
.port.is-param {
    font-size: 10.5px;
    opacity: .62;
}

.port.is-param:first-of-type {
    margin-top: 3px;
    padding-top: 5px;
    border-top: 1px dashed var(--fb-line);
}

.pip.is-param {
    width: 6px;
    height: 6px;
    border-width: 1.5px;
}

/* Форма пипки кодирует арность — это видно до того, как потянут линию.
   Круг принимает одну связь, капсула — несколько. */
.pip {
    position: absolute;
    left: 0;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--fb-canvas);
    border: 2px solid var(--t, var(--fb-t-none));
    transform: translateX(-50%);
    box-sizing: content-box;
}

.pip.is-multiple {
    width: 18px;
    border-radius: 6px;
}

.pip.is-filled { background: var(--t, var(--fb-t-none)); }

.pip.is-missing {
    border-style: dashed;
    border-color: var(--fb-danger);
    animation: pip-pulse 1.8s ease-in-out infinite;
}

/* Пипка выхода: правая кромка, по центру головы. Заполнена цветом типа всегда — она не
   «подключена или нет», она просто есть.

   Селектор обязан нести ДВА класса, а `left` — обязан сбрасываться в auto. Оба требования выяснились
   ценой бага: одноклассовое правило стоит выше `.pip` по файлу, проигрывает ему по порядку при равной
   специфичности и получает от него `left: 0` вместе с `translateX(-50%)`, — то есть пипка выхода
   встаёт на ЛЕВУЮ кромку и выглядит входом. Одного `right: 0` мало: при заданных обоих краях у
   абсолютного элемента с шириной решает `left`.

   Подписи типа рядом больше нет: о типе говорят цвет пипки, значок и рамка — четвёртый способ
   сказать то же самое был лишним. */
.pip.pip-out {
    left: auto;
    right: 0;
    top: 50%;
    transform: translate(50%, -50%);
    background: var(--t, var(--fb-t-none));
}

/* Столбик выходов — зеркало столбика входов. Появляется только у ноды, которая отдаёт больше одного
   значения (дизайн голоса: звук и голос), поэтому подпись здесь всегда осмысленна: она единственное,
   чем пипки различаются, — цвет говорит про тип, а два выхода бывают и одного типа.

   Текст прижат вправо, к своей пипке: столбик входов читается слева направо от пипки к подписи, и
   выходы обязаны читаться зеркально, иначе подпись оказывается дальше от своего порта, чем от
   чужого. */
.node-outs {
    list-style: none;
    margin: 0;
    padding: 2px 0 6px;
    border-top: var(--fb-div);
}

.out {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 7px;
    padding: 3px 14px 3px 12px;
    font-size: 11.5px;
    color: var(--fb-ink-dim);
}

.out-label { overflow-wrap: anywhere; }

.out.is-missing .out-label { color: var(--fb-danger); }

@keyframes pip-pulse {
    0%, 100% { box-shadow: 0 0 0 0 var(--fb-danger-soft); }
    50% { box-shadow: 0 0 0 5px transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .pip.is-missing { animation: none; }
}

.port.is-missing .port-label { color: var(--fb-danger); }

/* ---------- ряд чипов ---------- */

/* «Чем и на каких условиях делается этот шаг»: модель, кандидаты, ретраи, одобрение. Всё в одну
   ленту, потому что читается вместе; всё моноширинное, потому что всё машинное (§4).
   Курсор обычный — нода под чипами не тащится. */
.node-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 0 10px 8px;
    cursor: default;
}

/* Чип модели красится цветом типа результата — та же роль, что у рамки, только словами. */
.node-model {
    font-family: var(--fb-mono);
    font-size: 9.5px;
    line-height: 1;
    padding: 4px 6px;
    border-radius: var(--fb-r-chip);
    border: 1px solid color-mix(in srgb, var(--t, var(--fb-t-none)) 55%, transparent);
    background: color-mix(in srgb, var(--t, var(--fb-t-none)) 12%, transparent);
    color: var(--t, var(--fb-t-none));
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Локальный шаг: модели у него нет, зато есть ответ на вопрос «спишутся ли деньги», и он даётся
   словом, а не цветовым кодом. Голубой — цвет бесплатного исполнения (DESIGN_SYSTEM.md §2). */
.node-model.is-local {
    border-color: var(--fb-local);
    background: var(--fb-local-soft);
    color: var(--fb-local);
}

/* Признак условия — «одобрение» в конструкторе, «нужен выбор» в прогоне. Нейтральный: он не про
   тип и не про деньги, а про то, что шаг остановится и будет ждать человека. */
.node-flag {
    font-family: var(--fb-mono);
    font-size: 9.5px;
    line-height: 1;
    padding: 4px 6px;
    border-radius: var(--fb-r-chip);
    border: 1px solid var(--fb-line);
    background: var(--bg-inset);
    color: var(--fb-ink-dim);
    white-space: nowrap;
}

/* Счётчик-чип: выглядит как сосед по ряду, но правится на месте. Кнопки приглушены, пока чип не
   тронут курсором: их нажимают изредка, а читают число постоянно. */
.node-count {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 2px 3px;
    border-radius: var(--fb-r-chip);
    border: 1px solid var(--fb-line);
    background: var(--bg-inset);
    font-family: var(--fb-mono);
    font-size: 9.5px;
    line-height: 1;
    color: var(--fb-ink-faint);
    white-space: nowrap;
}

/* Значение не по умолчанию — то, что человек задал руками, и на графе это надо видеть. */
.node-count.is-set { border-color: var(--fb-line-hi); color: var(--fb-ink-dim); }

.count-value {
    min-width: 8px;
    text-align: center;
    color: var(--fb-ink);
}

.node-count.is-set .count-value { color: var(--fb-primary); }

.count-label { color: inherit; }

.count-btn {
    width: 14px;
    height: 14px;
    padding: 0;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: var(--radius-xs);
    background: transparent;
    color: var(--fb-ink-faint);
    font-family: var(--fb-ui);
    font-size: 11px;
    line-height: 1;
    cursor: pointer;
    opacity: .45;
    transition: opacity .12s ease, background .12s ease, color .12s ease;
}

.graph-node:hover .count-btn { opacity: 1; }

.count-btn:hover:not(:disabled) { background: var(--fb-primary-soft); color: var(--fb-primary); }

/* Упёрлись в границу диапазона: кнопка гаснет, а не исчезает — иначе чип бы менял ширину. */
.count-btn:disabled { opacity: .15; cursor: default; }

/* ---------- режим прогона ---------- */

/* Превью держим низким и обрезаем по центру: вертикаль 9:16 в 236 пикселях ширины развернулась бы
   на пол-экрана, а нода должна читаться одним взглядом. */
.node-preview {
    position: relative;
    height: 84px;
    margin: 0 10px 8px;
    border: 1px solid var(--fb-line);
    border-radius: var(--fb-r-in);
    overflow: hidden;
    background: var(--bg-inset);
}

/* Результата ещё нет — панель остаётся и говорит, чего ждём. Пустое место на её месте читалось бы
   как «нода ничего не делает», а это ровно те минуты, когда она делает больше всего. */
.node-preview.is-waiting {
    display: grid;
    place-items: center;
    height: 44px;
    padding: 0 8px;
    border-style: dashed;
    font-family: var(--fb-mono);
    font-size: 9.5px;
    color: var(--fb-ink-faint);
    text-align: center;
}

.graph-node.phase-awaiting .node-preview.is-waiting {
    border-color: color-mix(in srgb, var(--fb-await) 45%, transparent);
    color: var(--fb-await);
}

.node-preview img,
.node-preview video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Видео лежит поверх подложки: отрисовался кадр — он её закрыл, не отрисовался — видно «видео». */
.node-preview video { position: relative; }

.node-preview-fallback {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    font-family: var(--fb-mono);
    font-size: 9.5px;
    color: var(--fb-ink-dim);
}

.node-preview-audio,
.node-preview-text {
    display: flex;
    height: 100%;
    padding: 7px 9px;
    font-family: var(--fb-mono);
    font-size: 9.5px;
    line-height: 1.45;
    color: var(--fb-ink-dim);
    overflow: hidden;
}

.node-preview-audio {
    align-items: center;
    justify-content: center;
    gap: 6px;
}

.node-error {
    margin: 0 10px 8px;
    padding: 6px 8px;
    border-radius: var(--fb-r-in);
    background: var(--fb-danger-soft);
    color: var(--fb-danger);
    font-size: 10.5px;
    line-height: 1.4;
    overflow-wrap: anywhere;
}

/* Шаг ещё не запускался — приглушён, но читается: по нему видно форму будущей работы. Приглушение
   слабее прежнего (.6): у ноды прибавилось цветного — рамка типа, чип модели, значок, — и на 60 %
   всё это уходило в кашу, хотя «ждёт входов» — самое частое состояние в начале прогона. */
.graph-node.phase-idle { opacity: .76; }

/* Входы готовы, стартует ближайшим тиком. Приглушения нет вовсе: этот шаг уже часть работы, и
   гасить его наравне с теми, кто ждёт соседа, значило бы врать — ровно на это и жаловались. */
.graph-node.phase-ready { opacity: .85; }

.graph-node.phase-awaiting {
    border-color: var(--fb-await);
    box-shadow: var(--fb-inner-hi), var(--fb-ring-await), var(--fb-shadow);
}

.graph-node.phase-failed { border-color: var(--fb-danger); }

/* ---------- подвал ---------- */

/* Состояние слева, деньги справа. Раньше они стояли в одной строке через точку: статус читают
   глазами, а цену сверяют с прайсом, и рядом друг с другом они мешали. */
.node-foot {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px 7px;
    border-top: var(--fb-div);
    font-size: 10.5px;
    color: var(--fb-ink-dim);
}

.node-state {
    display: flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
}

.node-state-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Точка состояния — та же азбука, что в списке прогонов (.fb-state в mud.css): одно понятие —
   одна форма записи. В конструкторе фазы нет, и точки нет тоже. */
.node-dot {
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--state-idle);
}

.node-dot[data-state="ready"] { background: var(--state-ready); }
.node-dot[data-state="queued"] { background: var(--state-running); opacity: .55; }
.node-dot[data-state="running"] { background: var(--state-running); animation: fabrika-pulse var(--duration-pulse) linear infinite; }
.node-dot[data-state="awaiting"] { background: var(--state-awaiting); }
.node-dot[data-state="done"] { background: var(--state-done); }
.node-dot[data-state="failed"] { background: var(--state-failed); }
.node-dot[data-state="skipped"] { background: var(--state-skipped); }

@media (prefers-reduced-motion: reduce) {
    .node-dot[data-state="running"] { animation: none; }
}

.node-cost {
    margin-left: auto;
    flex: none;
    font-family: var(--fb-mono);
    font-size: 9.5px;
    color: var(--fb-ink-faint);
}

/* Работа идёт: рамка и световой пробег по верхней кромке — оба сигнальным, а НЕ цветом типа.
   Цвет типа тут врал бы дважды: у шага озвучки он зелёный, то есть ровно цвет «выполнено», и нода,
   которая как раз работает, выглядела бы законченной. «Идёт работа» и «стоит денег» — одно и то же
   событие, у него один цвет (DESIGN_SYSTEM.md §2).

   Единственная анимация на канвасе, и она значит «ждём». В покое у полосы нулевая высота, поэтому в
   остальное время её на ноде нет вовсе. */
.graph-node.phase-running,
.graph-node.phase-queued { border-color: var(--state-running); }

.graph-node.phase-running { box-shadow: var(--fb-ring), var(--fb-shadow); }

.graph-node.phase-running .node-rail,
.graph-node.phase-queued .node-rail {
    height: 2px;
    background: linear-gradient(90deg,
        var(--state-running) 0%,
        var(--fb-sheen) 50%,
        var(--state-running) 100%);
    background-size: 200% 100%;
    animation: rail-sweep 1.6s linear infinite;
}

.graph-node.phase-queued { opacity: .8; }
.graph-node.phase-queued .node-rail { animation-duration: 3.2s; opacity: .55; }

@keyframes rail-sweep {
    from { background-position: 100% 0; }
    to { background-position: -100% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .graph-node.phase-running .node-rail,
    .graph-node.phase-queued .node-rail { animation: none; }
}

/* Промпт прямо на ноде — у любого шага, которому промпт нужен по контракту, и только в
   конструкторе. Промпт и есть то, чем один шаг отличается от другого с той же моделью: без него,
   чтобы вспомнить, что нода делает, приходилось открывать инспектор на каждую.
   Три строки с многоточием: нода не должна расти от длины текста, иначе граф расползётся. */
.node-prompt {
    margin: 0 10px 8px;
    padding: 6px 8px;
    border-radius: var(--fb-r-in);
    background: var(--bg-inset);
    border: 1px solid var(--fb-line);
    font-family: var(--fb-ui);
    font-size: 10.5px;
    line-height: 1.35;
    color: var(--fb-ink-dim);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.node-prompt.is-empty { color: var(--fb-danger); font-style: italic; }

/* Плейсхолдеры под полем промпта. Кнопки, а не подсказка строкой: фигурные скобки в русской
   раскладке не набрать, не переключившись, а имя связи ещё и правится на проводе. */
.prompt-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 6px;
}

.prompt-tag {
    padding: 3px 7px;
    border-radius: 5px;
    border: 1px solid var(--fb-line);
    background: var(--fb-sunken);
    color: var(--fb-ink-dim);
    font-family: var(--fb-mono);
    font-size: 10.5px;
    line-height: 1.2;
    cursor: pointer;
    transition: color .12s ease, border-color .12s ease, background .12s ease;
}

.prompt-tag:hover { color: var(--fb-ink); border-color: var(--fb-primary); background: var(--fb-surface-hi); }
.prompt-tag:focus-visible { outline: none; box-shadow: var(--fb-ring); }

.node-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px 8px;
    border-top: var(--fb-div);
    font-family: var(--fb-mono);
    font-size: 10px;
    color: var(--fb-ink-dim);
    overflow-wrap: anywhere;
}

.node-badge {
    margin-left: auto;
    padding: 1px 5px;
    border-radius: 4px;
    background: var(--fb-await-soft);
    color: var(--fb-await);
    white-space: nowrap;
}

/* ---------- HUD ---------- */

.graph-hud {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 7px 12px;
    background: linear-gradient(180deg, transparent, var(--fb-canvas-fade) 40%);
    font-family: var(--fb-mono);
    font-size: 11px;
    color: var(--fb-ink-dim);
    pointer-events: none;
}

.graph-hud > * { pointer-events: auto; }
.graph-hud .spacer { flex: 1; pointer-events: none; }
.graph-hud .is-error { color: var(--fb-danger); }
.graph-hud .is-warn { color: var(--fb-await); }

/* Плашка статуса, за которой раскрывается список. Выглядит как подпись, а не как кнопка: в HUD
   кнопка соревновалась бы за внимание с зумом. Что она нажимается, показывает подчёркивание. */
.hud-status {
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
}

.hud-status:hover,
.hud-status.is-open { text-decoration-style: solid; }

/* Список замечаний над HUD. Утопленная поверхность, как у канваса, — он лежит НА графе.
   Список правок от ИИ живёт по тем же правилам и делит с ним оформление: это одна и та же вещь —
   перечень претензий к графу с переходом к виновной ноде. */
.hud-issues,
.hud-changes {
    position: absolute;
    left: 12px;
    bottom: 40px;
    z-index: 6;
    max-width: min(560px, calc(100% - 24px));
    max-height: 46%;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--fb-line);
    border-radius: 10px;
    background: var(--fb-grad);
    box-shadow: var(--fb-inner-hi), var(--fb-shadow);
}

.hud-issue,
.hud-change {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 6px 8px;
    border-radius: 6px;
    font-family: var(--fb-ui);
    font-size: 12px;
    line-height: 1.35;
    color: var(--fb-ink);
    overflow-wrap: anywhere;
}

.hud-issue.is-clickable,
.hud-change.is-clickable { cursor: pointer; }

.hud-issue.is-clickable:hover,
.hud-change.is-clickable:hover { background: var(--fb-hover); }

/* Знак правки: +, − или ~. Моноширинный, чтобы столбец знаков не гулял по строкам. */
.hud-change-mark {
    flex: none;
    width: 10px;
    font-family: var(--fb-mono);
    text-align: center;
    color: var(--fb-ink-dim);
}

.hud-change.patch-added .hud-change-mark { color: var(--fb-patch); }
.hud-change.patch-removed .hud-change-mark { color: var(--fb-danger); }
.hud-status.is-patch { color: var(--fb-patch); }

/* Точка серьёзности: те же два цвета, что у кромки ноды и у плашки статуса. */
.hud-issue-dot {
    flex: none;
    width: 7px;
    height: 7px;
    margin-top: 5px;
    border-radius: 50%;
}

.hud-issue.is-error .hud-issue-dot { background: var(--fb-danger); }
.hud-issue.is-warn .hud-issue-dot { background: var(--fb-await); }

/* ---------- инспектор ---------- */

.graph-inspector {
    width: 450px;
    border-left: 1px solid var(--fb-line);
    background: var(--fb-bg);
    overflow-y: auto;
    overflow-x: hidden;
}

.graph-inspector-empty {
    padding: 28px 20px;
    color: var(--fb-ink-dim);
    font-family: var(--fb-ui);
    font-size: 13px;
    line-height: 1.5;
}

/* Полоса возврата над инспектором (InspectorSheetBar.razor). На широком экране её нет вовсе: там
   инспектор — колонка рядом с графом, и выходить неоткуда. Включается на ступени ≤560px, где он
   становится оверлеем во весь экран.

   Липкая, а не просто первая: инспектор прокручивается, и уехавший наверх выход пришлось бы искать
   обратной прокруткой. */
.graph-sheet-bar {
    display: none;
    position: sticky;
    top: 0;
    z-index: 1;
    align-items: center;
    gap: 4px;
    padding: 4px 8px;
    background: var(--fb-bg);
    border-bottom: 1px solid var(--fb-line);
}

/* Имя не переносится и не растягивает полосу: длинное имя ноды съело бы две строки высоты у экрана,
   на котором её и так нет. */
.graph-sheet-title {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font: var(--type-label);
    color: var(--fb-ink);
}

@media (max-width: 1100px) {
    .graph-page { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
    /* Три ряда: палитра, канвас, инспектор. Палитра ложится полосой сверху — вертикальный рельс
       съедал бы ширину там, где её и так нет.

       Колонку ОБЯЗАТЕЛЬНО переопределять здесь же, у `.has-rail`. Правила выше хватает только на
       вид без палитры: у базового `.graph-page.has-rail` два класса против одного, и три колонки
       переживали медиазапрос целиком. Выглядело это так, будто раскладка не сработала вовсе, —
       палитра и инспектор стояли рядом, а канвасу доставалась щель. */
    .graph-page.has-rail { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
    .graph-rail {
        flex-direction: row;
        width: auto;
        overflow-x: auto;
        border-right: none;
        border-bottom: 1px solid var(--fb-line);
    }
    .graph-rail > .mud-menu { width: auto; flex: none; min-width: 72px; }
    .graph-inspector { width: auto; max-height: 45vh; border-left: none; border-top: 1px solid var(--fb-line); }
}

@media (max-width: 560px) {
    /* HUD над канвасом сжимается до того, что на телефоне ещё и нажимают. Счёт шагов и связей —
       справка, которую читают один раз; зум и «во весь экран» — управление, и рвать их подписи на
       три строки, чтобы уместить справку рядом, значит сломать то, чем пользуются, ради того, что
       разглядывают. Строка не переносится вовсе: HUD лежит НА канвасе, и второй его ряд закрыл бы
       нижнюю ноду. */
    .graph-hud { gap: 8px; padding: 6px 8px; flex-wrap: nowrap; }
    .graph-hud > span:first-child { display: none; }
    .graph-hud .hud-status { white-space: nowrap; }

    /* Раздел во всю ширину: рамка и скругление рисовали карточку там, где карточки нет, и отнимали
       у канваса те самые точки, за которые он дерётся (DESIGN_SYSTEM.md §11.1). */
    .graph-page {
        margin: 0 -12px;
        border-left: 0;
        border-right: 0;
        border-radius: 0;
        /* Хром страницы на телефоне другой: шапки раздела нет, зато полоса над графом занимает две
           строки. Считать от --fb-page-chrome (160px, подобранного под окно) значило бы отдать
           канвасу меньше, чем осталось. */
        height: calc(100vh - var(--fb-topbar-h) - 120px);
        min-height: 360px;
    }

    /* Инспектор — треть экрана, а не половина: на 730 точках 45vh оставляли канвасу 180, то есть
       полторы ноды, и разглядеть в них цепочку было нельзя. Открывают его, уже выбрав ноду НА
       канвасе, поэтому канвас важнее. */
    .graph-inspector { max-height: 32vh; }

    /* А вот когда что-то ВЫБРАНО — инспектор перестаёт быть полосой и раскрывается во весь экран.
       Треть экрана хороша для «ничего не выбрано» (там бриф или свойства шаблона — их проглядывают),
       но не для того, ради чего сюда приходят с телефона: одобрить кандидата, прочитав промпт и
       посмотрев превью. В 32vh помещается заголовок и половина первой плитки.

       position: fixed, а не растянутая ячейка сетки: панель обязана накрыть и топбар тоже — иначе
       поверх оверлея остаётся шапка с бургером, то есть выхода два и они делают разное. Ни у одного
       предка (.fb-shell, .fb-body, .fb-main, .graph-page) нет transform, поэтому fixed считается от
       окна, а не от .graph-page, и её overflow: hidden оверлей не режет.

       z-index выше выехавшего рельса (40 в shell.css) и топбара (30), но НИЖЕ диалога Mud'а (1400)
       и слоя всплывашек (2000): меню, подсказки и просмотрщик файлов открываются изнутри панели и
       обязаны остаться над ней. */
    .graph-page.inspector-open .graph-inspector {
        position: fixed;
        inset: 0;
        z-index: 50;
        width: auto;
        max-height: none;
        border: 0;
        background: var(--fb-bg);
    }

    .graph-page.inspector-open .graph-sheet-bar { display: flex; }
}
