/* Токены визуальной системы. Описание и обоснование значений — DESIGN_SYSTEM.md §2–§4.

   Файл подключается ПЕРВЫМ и не содержит ничего, кроме переменных, объявления шрифтов и двух
   фоновых утилит: токены принадлежат всему приложению, а не какой-то одной странице. Раньше они
   жили в graph.css, и это читалось так, будто система существует только на канвасе, — остальные
   страницы поэтому и разъехались с ней. Правило: значение, которое нужно двум файлам, живёт здесь,
   а не дублируется литералом.

   ─────────────────────────────────────────────────────────────────────────────────────────────
   ГАММА СМЕНИЛАСЬ. Была сине-чёрная с барвинковым акцентом, стала графитовая с одним сигнальным
   оранжевым. Причина не в моде: синий акцент занимал ту же полку, что и цвета типов материалов
   (текст, картинка, видео — все холодные), и «выделено» переставало отличаться от «это картинка».
   Графит нейтрален, а оранжевый в системе больше ничего не значит — поэтому он и может значить
   «внимание сюда» и «это стоит денег» одновременно.

   Файл устроен в три слоя, и порядок обязателен:

     1. БАЗОВЫЕ ШКАЛЫ    — сырой цвет. По теме НЕ переопределяются.
     2. СЕМАНТИЧЕСКИЕ    — роль (`--bg-surface`, `--text-secondary`, `--accent`). Переопределяются
                           целиком в блоке [data-theme="light"].
     3. ПСЕВДОНИМЫ --fb-*— имена, которыми написаны разметка и остальные файлы стилей. Ссылаются на
                           семантику, поэтому светлая тема достаётся им бесплатно.

   В разметке и в компонентах используются ТОЛЬКО слои 2 и 3. Написав `var(--graphite-850)` в
   правиле компонента, вы получите элемент, который не переключается вместе с темой. */

/* ═══════════════════════════════════════════════════════════════════════════════
   ШРИФТЫ
   ═══════════════════════════════════════════════════════════════════════════════

   Три гарнитуры, три роли, ни одной лишней. Все три лежат в wwwroot/fonts переменными файлами:
   приложение работает без сети, и обращение к CDN означало бы, что офлайн интерфейс молча
   набирается системным шрифтом. (Handoff дизайн-системы тянул те же семейства с Google Fonts —
   это его допущение для демо-страницы, а не требование к продукту.)

   Montserrat здесь НЕ объявлен, но файл из папки не убран: им вжигаются караоке-субтитры
   (SubtitleExecutor), и libass берёт его из этой же папки. Интерфейс и ролик разошлись по
   гарнитуре сознательно — у ролика задача кричать с экрана телефона, у интерфейса ровно
   обратная. */

@font-face {
    font-family: "Manrope";
    src: url("../fonts/Manrope-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 200 800;
    font-display: swap;
}

@font-face {
    font-family: "Geist";
    src: url("../fonts/Geist-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 100 900;
    font-display: swap;
}

@font-face {
    font-family: "Geist";
    src: url("../fonts/Geist-Italic-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 100 900;
    font-style: italic;
    font-display: swap;
}

@font-face {
    font-family: "Geist Mono";
    src: url("../fonts/GeistMono-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 100 900;
    font-display: swap;
}

/* ═══════════════════════════════════════════════════════════════════════════════
   1. БАЗОВЫЕ ШКАЛЫ
   ═══════════════════════════════════════════════════════════════════════════════ */

:root {
    /* ---------- графит ----------
       Fabrika — цех, а не витрина: основа нейтральная, свет приходит от одного сигнального цвета
       и от цветов типов материалов. Шкала идёт от самого тёмного (1000) к самому светлому (50);
       `paper` — не продолжение шкалы, а бумага светлой темы: тёплая, чтобы белый интерфейс не
       звенел. */
    --graphite-1000: #08090b;
    --graphite-950: #0c0e11;
    --graphite-900: #121418;
    --graphite-850: #171a1f;
    --graphite-800: #1d2027;
    --graphite-700: #262a33;
    --graphite-600: #343945;
    --graphite-500: #4a5160;
    --graphite-400: #6b7385;
    --graphite-300: #949baa;
    --graphite-200: #c2c7d2;
    --graphite-100: #e2e5eb;
    --graphite-50: #f2f3f6;
    --paper: #faf9f6;
    --paper-tint: #f1efe9;
    --white: #ffffff;

    /* ---------- сигнальный ----------
       Фирменный цвет и всё, что стоит денег: сетевая модель, платный шаг, кнопка запуска,
       выделение. Второго такого цвета в системе нет и заводить его нельзя — «важно» перестанет
       читаться с одного взгляда. */
    --signal-700: #b8330a;
    --signal-600: #e8460f;
    --signal-500: #ff5b24;
    --signal-400: #ff8055;
    --signal-300: #ffa587;
    --signal-tint: rgba(255, 91, 36, .14);

    /* ---------- локальный ----------
       Всё, что исполняется своими силами и стоит ноль: субтитры, сборка, кадр из клипа, статичный
       ассет. Пара «оранжевый ↔ голубой» отвечает на единственный вопрос, который человек задаёт
       ноде до запуска: спишутся ли за неё деньги. */
    --local-600: #12a8bd;
    --local-500: #24d3e8;
    --local-400: #5ee2f2;
    --local-tint: rgba(36, 211, 232, .14);

    /* ---------- цвета типов материалов ----------
       Общий язык нод, портов, проводов, чипов и клипов таймлайна. Один тип — один цвет во всём
       приложении, включая лендинг. */
    --type-text: #7fa9ff;
    --type-image: #ffc24b;
    --type-video: #c77dff;
    --type-audio: #34e39b;
    --type-json: #ff7a9c;
    --type-text-tint: rgba(127, 169, 255, .16);
    --type-image-tint: rgba(255, 194, 75, .16);
    --type-video-tint: rgba(199, 125, 255, .16);
    --type-audio-tint: rgba(52, 227, 155, .16);
    --type-json-tint: rgba(255, 122, 156, .16);

    /* ---------- семантика ---------- */
    --success-500: #34e39b;
    --warning-500: #ffc24b;
    --danger-500: #ff4d4d;
    --danger-600: #e03434;
    --info-500: #7fa9ff;
    --success-tint: rgba(52, 227, 155, .14);
    --warning-tint: rgba(255, 194, 75, .14);
    --danger-tint: rgba(255, 77, 77, .14);
    --info-tint: rgba(127, 169, 255, .14);

    /* ---------- «тронуто правкой ИИ» ----------
       Только режим ревью предложенных правок, больше нигде. Цвет пришлось заводить свой: все
       готовые заняты смыслом, который нельзя размывать. Оранжевый значит «выделено», жёлтый —
       «ждёт человека», а зелёный в новой гамме совпал с `--success-500`, то есть кромка правки
       читалась бы как «шаг выполнен». Лаймовый — единственный тон, свободный и от типов
       материалов, и от состояний шага. */
    --patch-500: #c3f53c;
    --patch-tint: rgba(195, 245, 60, .14);

    /* ---------- состояния шага и прогона ----------
       Ссылки, а не свои значения: «идёт работа» обязано быть тем же оранжевым, что и «стоит
       денег», иначе на канвасе окажется два разных «внимания». */
    --state-idle: var(--graphite-400);
    --state-ready: var(--type-text);
    --state-running: var(--signal-500);
    --state-awaiting: var(--warning-500);
    --state-done: var(--success-500);
    --state-failed: var(--danger-500);
    --state-skipped: var(--graphite-500);
}

/* ═══════════════════════════════════════════════════════════════════════════════
   2. СЕМАНТИКА — ТЁМНАЯ ТЕМА (умолчание, режим цеха)
   ═══════════════════════════════════════════════════════════════════════════════

   Системную тему ОС мы намеренно не слушаем: цех тёмный по умолчанию, светлая — осознанный
   выбор человека, и он живёт в localStorage. Атрибут `data-theme` ставится на <html> синхронным
   сниппетом в <head> ДО стилей, иначе на каждой перезагрузке мигал бы светлый фон. */

:root {
    color-scheme: dark;

    /* Лестница поверхностей, снизу вверх: canvas → sunken → surface → raised.
       canvas — самое дно (полотно графа, колодцы превью), sunken — фон страницы, surface —
       карточка, raised — то, что лежит на карточке. */
    --bg-canvas: var(--graphite-1000);
    --bg-sunken: var(--graphite-950);
    --bg-surface: var(--graphite-900);
    --bg-raised: var(--graphite-850);

    /* Наведение и нажатие — прозрачные плёнки, а не ступени лестницы: одна и та же плёнка
       работает поверх любой поверхности, а подобранная под каждую ступень пара цветов разъехалась
       бы на первой же правке. */
    --bg-hover: rgba(255, 255, 255, .04);
    --bg-active: rgba(255, 255, 255, .07);
    --bg-inset: rgba(0, 0, 0, .25);
    --bg-overlay: rgba(8, 9, 11, .72);

    --border-subtle: rgba(255, 255, 255, .06);
    --border-default: rgba(255, 255, 255, .11);
    --border-strong: rgba(255, 255, 255, .2);

    --text-primary: var(--graphite-50);
    --text-secondary: var(--graphite-300);
    --text-muted: var(--graphite-400);
    --text-disabled: var(--graphite-500);
    --text-inverse: var(--graphite-1000);
    --text-accent: var(--signal-400);

    --accent: var(--signal-500);
    --accent-hover: var(--signal-400);
    --accent-press: var(--signal-600);
    /* Текст на оранжевой заливке. Не белый и не чёрный: тёмно-коричневый держит контраст и не
       выглядит дырой в кнопке. */
    --accent-contrast: #1a0b04;
    --focus-ring: rgba(255, 91, 36, .55);

    /* Роли поверхностей, названные по месту, а не по ступени: место переживает перекраску. */
    --surface-card: var(--graphite-900);
    --surface-node: var(--graphite-850);
    --surface-field: var(--graphite-950);
    --surface-rail: var(--graphite-950);
    --grid-line: rgba(255, 255, 255, .045);
    /* Провод без типа и линейки таймлайна. */
    --wire-default: var(--graphite-600);
}

/* ═══════════════════════════════════════════════════════════════════════════════
   2b. СЕМАНТИКА — СВЕТЛАЯ ТЕМА
   ═══════════════════════════════════════════════════════════════════════════════

   Переопределяются ТОЛЬКО семантические имена. Ни один компонент о теме не знает, и это условие
   работоспособности: пока канвас, таймлайн и ноды были написаны прямыми значениями тёмной гаммы,
   переключатель давал не светлый интерфейс, а тёмную навигацию с белой таблицей посередине —
   поэтому его и не было.

   Цвета типов и семантики здесь ослабляются: тон, читаемый на графите, на бумаге светит и теряет
   контраст с белым. Оттенок остаётся тот же — язык данных не меняется от темы. */

[data-theme="light"] {
    color-scheme: light;

    --bg-canvas: var(--paper);
    --bg-sunken: var(--paper-tint);
    --bg-surface: var(--white);
    --bg-raised: var(--white);

    --bg-hover: rgba(12, 14, 17, .04);
    --bg-active: rgba(12, 14, 17, .07);
    --bg-inset: rgba(12, 14, 17, .04);
    --bg-overlay: rgba(12, 14, 17, .45);

    --border-subtle: rgba(12, 14, 17, .08);
    --border-default: rgba(12, 14, 17, .14);
    --border-strong: rgba(12, 14, 17, .28);

    --text-primary: var(--graphite-1000);
    --text-secondary: var(--graphite-500);
    --text-muted: var(--graphite-400);
    --text-disabled: var(--graphite-300);
    --text-inverse: var(--white);
    --text-accent: var(--signal-600);

    --accent: var(--signal-500);
    --accent-hover: var(--signal-600);
    --accent-press: var(--signal-700);
    --accent-contrast: #ffffff;
    --focus-ring: rgba(255, 91, 36, .55);

    --surface-card: var(--white);
    --surface-node: var(--white);
    --surface-field: var(--white);
    --surface-rail: var(--paper-tint);
    --grid-line: rgba(12, 14, 17, .06);
    --wire-default: var(--graphite-300);

    /* Язык данных на бумаге */
    --type-text: #3d6ede;
    --type-image: #d9930b;
    --type-video: #8f3ddb;
    --type-audio: #0f9f68;
    --type-json: #d63f66;
    --type-text-tint: rgba(61, 110, 222, .12);
    --type-image-tint: rgba(217, 147, 11, .14);
    --type-video-tint: rgba(143, 61, 219, .12);
    --type-audio-tint: rgba(15, 159, 104, .12);
    --type-json-tint: rgba(214, 63, 102, .12);

    --success-500: #0f9f68;
    --warning-500: #b87c06;
    --danger-500: #d43333;
    --danger-600: #b62525;
    --info-500: #3d6ede;
    --success-tint: rgba(15, 159, 104, .12);
    --warning-tint: rgba(184, 124, 6, .14);
    --danger-tint: rgba(212, 51, 51, .12);
    --info-tint: rgba(61, 110, 222, .12);

    --local-500: var(--local-600);
    --local-tint: rgba(18, 168, 189, .14);

    --patch-500: #6f8f00;
    --patch-tint: rgba(111, 143, 0, .14);
}

/* ═══════════════════════════════════════════════════════════════════════════════
   ТИПОГРАФИКА
   ═══════════════════════════════════════════════════════════════════════════════

   Двенадцать ступеней на всё приложение. Интерфейс живёт в 11–16, публичные поверхности
   начинаются с 16: лендинг и кабинет — разные дистанции чтения, и подобранный «на глаз» кегль
   между ними разъезжался бы. */

:root {
    --fb-display: "Manrope", "Geist", system-ui, sans-serif;
    --fb-ui: "Geist", system-ui, -apple-system, "Segoe UI", sans-serif;
    --fb-mono: "Geist Mono", "Cascadia Code", Consolas, ui-monospace, monospace;

    /* Те же три имени в словаре дизайн-системы: разметка новых компонентов пишется ими. */
    --font-display: var(--fb-display);
    --font-sans: var(--fb-ui);
    --font-mono: var(--fb-mono);

    --fb-fs-10: 10px;
    --fb-fs-11: 11px;
    --fb-fs-12: 12px;
    --fb-fs-13: 13px;
    --fb-fs-14: 14px;
    --fb-fs-16: 16px;
    --fb-fs-20: 20px;
    --fb-fs-26: 26px;
    --fb-fs-34: 34px;
    --fb-fs-46: 46px;
    --fb-fs-62: 62px;
    --fb-fs-84: 84px;

    --text-2xs: var(--fb-fs-10);
    --text-xs: var(--fb-fs-11);
    --text-sm: var(--fb-fs-12);
    --text-base: var(--fb-fs-13);
    --text-md: var(--fb-fs-14);
    --text-lg: var(--fb-fs-16);
    --text-xl: var(--fb-fs-20);
    --text-2xl: var(--fb-fs-26);
    --text-3xl: var(--fb-fs-34);
    --text-4xl: var(--fb-fs-46);
    --text-5xl: var(--fb-fs-62);
    --text-6xl: var(--fb-fs-84);

    /* Интерлиньяж по роли, а не по кеглю: у display строки стоят вплотную, у текста дышат. */
    --fb-lh-display: 1.06;
    --fb-lh-head: 1.2;
    --fb-lh-text: 1.45;
    --fb-lh-control: 1.3;
    --leading-tight: var(--fb-lh-display);
    --leading-snug: var(--fb-lh-head);
    --leading-normal: var(--fb-lh-text);
    --leading-relaxed: 1.6;

    --weight-light: 300;
    --weight-regular: 400;
    --weight-medium: 500;
    --weight-semibold: 600;
    --weight-bold: 700;
    --weight-black: 800;

    /* Трекинг. Manrope на крупных кеглях рассыпается без сжатия, на мелких — наоборот. Капитель
       эйрбоу разрежена до .08em: на .12em короткое слово переставало читаться словом. */
    --fb-tr-display: -.03em;
    --fb-tr-head: -.015em;
    --fb-tr-eyebrow: .08em;
    --tracking-tighter: var(--fb-tr-display);
    --tracking-tight: var(--fb-tr-head);
    --tracking-normal: 0;
    --tracking-wide: .02em;
    --tracking-caps: var(--fb-tr-eyebrow);

    /* Роли-шорткаты для свойства `font`. Пишутся одной строкой, поэтому невозможно взять кегль
       заголовка и забыть его интерлиньяж. */
    --type-display: var(--weight-black) var(--text-5xl) / var(--leading-tight) var(--font-display);
    --type-h1: var(--weight-bold) var(--text-3xl) / var(--leading-snug) var(--font-display);
    --type-h2: var(--weight-bold) var(--text-2xl) / var(--leading-snug) var(--font-display);
    --type-h3: var(--weight-semibold) var(--text-xl) / var(--leading-snug) var(--font-display);
    --type-body: var(--weight-regular) var(--text-md) / var(--leading-normal) var(--font-sans);
    --type-body-sm: var(--weight-regular) var(--text-base) / var(--leading-normal) var(--font-sans);
    --type-ui: var(--weight-medium) var(--text-base) / 1.3 var(--font-sans);
    --type-label: var(--weight-medium) var(--text-sm) / 1.3 var(--font-sans);
    --type-caption: var(--weight-regular) var(--text-xs) / 1.35 var(--font-sans);
    --type-mono: var(--weight-medium) var(--text-sm) / 1.35 var(--font-mono);
    --type-mono-sm: var(--weight-medium) var(--text-xs) / 1.35 var(--font-mono);
    --type-overline: var(--weight-semibold) var(--text-2xs) / 1.2 var(--font-sans);
}

/* ═══════════════════════════════════════════════════════════════════════════════
   ГЕОМЕТРИЯ, ВЫСОТА, ДВИЖЕНИЕ
   ═══════════════════════════════════════════════════════════════════════════════ */

:root {
    /* Шаг сетки — 4px. Плотный инструмент: интерфейсные отступы живут в 4–16, воздух 24–120
       отдан публичным поверхностям. */
    --space-1: 2px;
    --space-2: 4px;
    --space-3: 6px;
    --space-4: 8px;
    --space-5: 10px;
    --space-6: 12px;
    --space-7: 16px;
    --space-8: 20px;
    --space-9: 24px;
    --space-10: 32px;
    --space-11: 40px;
    --space-12: 48px;
    --space-13: 64px;
    --space-14: 80px;
    --space-15: 120px;

    /* Углы почти прямые: инструмент 4–8px, диалог 12px. Скругление — не украшение, а способ
       сказать, насколько крупный объект перед глазами. */
    --radius-xs: 2px;
    --radius-sm: 4px;
    --radius-md: 6px;
    --radius-lg: 8px;
    --radius-xl: 12px;
    --radius-pill: 999px;

    --fb-r-page: var(--radius-xl);  /* контейнер страницы, диалог */
    --fb-r-card: var(--radius-lg);  /* карточка, нода */
    --fb-r-in: var(--radius-md);    /* вложенный блок: поле, кнопка, превью */
    --fb-r-chip: var(--radius-sm);  /* тег, чип */
    --radius-control: var(--fb-r-in);
    --radius-card: var(--fb-r-card);
    --radius-node: var(--fb-r-card);
    --radius-dialog: var(--fb-r-page);
    --radius-chip: var(--fb-r-chip);

    /* Высота контролов — три ступени и ни одной больше. */
    --control-sm: 24px;
    --control-md: 32px;
    --control-lg: 40px;

    /* Ширина глобальной навигации. Ею же считается высота страницы под шапку, поэтому значение
       обязано быть одно на CSS и на разметку. */
    --fb-nav-w: 208px;
    /* Высота фиксированного топбара приложения. Он не прокручивается, поэтому под него
       резервируется место, а не подбирается отступ. */
    --fb-topbar-h: 52px;
    /* Вертикаль, которую съедает хром страницы (топбар + отступы + заголовок). Канвас дерётся за
       высоту, и подбирать её литералом в каждом файле означало бы расхождение при правке шапки. */
    --fb-page-chrome: 160px;
    --rail-width: 56px;
    --inspector-width: 340px;
    --content-max: 1180px;

    /* Движение служит станку: быстрое, короткое, без отскоков. Единственное длительное движение в
       системе — «работа идёт» (бегущий пунктир провода, пульс индикатора). */
    --duration-fast: 120ms;
    --duration-base: 180ms;
    --duration-slow: 280ms;
    --duration-pulse: 1600ms;
    --duration-march: 900ms;
    --ease-standard: cubic-bezier(.2, 0, .13, 1);
    --ease-out: cubic-bezier(.16, 1, .3, 1);
    --transition-control:
        background-color var(--duration-fast) var(--ease-standard),
        border-color var(--duration-fast) var(--ease-standard),
        color var(--duration-fast) var(--ease-standard),
        box-shadow var(--duration-fast) var(--ease-standard);
}

/* Высота на графите набирается границей и плотной чёрной подложкой, а не мягким размытием: мягкая
   тень на тёмном фоне не видна вовсе и лишь съедает контраст. На бумаге тень наоборот работает,
   поэтому в светлой теме переопределена. Свечение (ring) закреплено за состояниями, не за
   декором. */
:root {
    --fb-inner-hi: inset 0 1px 0 rgba(255, 255, 255, .05);
    --fb-shadow: 0 12px 32px rgba(0, 0, 0, .55);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow-md: 0 4px 12px rgba(0, 0, 0, .45);
    --shadow-lg: var(--fb-shadow);
    --shadow-dialog: 0 32px 80px rgba(0, 0, 0, .65);
    --shadow-node: 0 2px 6px rgba(0, 0, 0, .5);

    /* Кольцо, а не утолщённая обводка: обводка меняет размер объекта и сдвигает соседей (§7). */
    --fb-ring: 0 0 0 3px var(--signal-tint);
    --fb-ring-await: 0 0 0 3px var(--warning-tint);
    --fb-ring-danger: 0 0 0 3px var(--danger-tint);
    --fb-ring-patch: 0 0 0 3px var(--patch-tint);
    --glow-accent: var(--fb-ring);
    --glow-running: var(--fb-ring);
    --glow-awaiting: var(--fb-ring-await);
    --glow-failed: var(--fb-ring-danger);
    --ring-focus: 0 0 0 2px var(--bg-canvas), 0 0 0 4px var(--focus-ring);
    --shadow-node-selected: 0 0 0 1px var(--accent), 0 6px 20px rgba(255, 91, 36, .22);

    --fb-div: 1px solid var(--border-subtle);
}

[data-theme="light"] {
    --fb-inner-hi: inset 0 1px 0 rgba(255, 255, 255, .6);
    --fb-shadow: 0 12px 32px rgba(12, 14, 17, .1);
    --shadow-sm: 0 1px 2px rgba(12, 14, 17, .08);
    --shadow-md: 0 4px 14px rgba(12, 14, 17, .1);
    --shadow-dialog: 0 32px 80px rgba(12, 14, 17, .2);
    --shadow-node: 0 1px 3px rgba(12, 14, 17, .12);
}

/* ═══════════════════════════════════════════════════════════════════════════════
   3. ПСЕВДОНИМЫ --fb-*
   ═══════════════════════════════════════════════════════════════════════════════

   Имена, которыми написаны разметка и остальные файлы стилей. Оставлены не «для совместимости»:
   каждое называет РОЛЬ, а роль переживает перекраску. Значение приходит из семантики, поэтому обе
   темы достаются им бесплатно, а переименовывать полсотни файлов ради смены гаммы не нужно.

   Правило при добавлении нового: если имени нет ни здесь, ни в семантике — сначала заведите его
   здесь, потом используйте. Литерал в правиле компонента = элемент, который не переключается. */

:root {
    /* ---------- поверхности ---------- */
    --fb-canvas: var(--bg-canvas);     /* полотно графа, колодец превью — самое дно */
    --fb-bg: var(--bg-sunken);         /* фон страницы, топбар, рельс навигации */
    --fb-sunken: var(--bg-raised);     /* чип, вторичная кнопка, «утоплённая» плашка на странице */
    --fb-surface: var(--surface-card);  /* карточка, панель, диалог */
    --fb-raised: var(--bg-raised);      /* то, что лежит на карточке */
    --fb-inset: var(--surface-field);   /* поле ввода */
    --fb-surface-hi: var(--bg-raised);  /* верх градиента приподнятой карточки */

    --fb-line: var(--border-default);
    --fb-line-hi: var(--border-strong);

    /* ---------- смысловые цвета ---------- */
    /* Первичный = сигнальный. Выделение, фокус, активный пункт навигации, кнопка действия. */
    --fb-primary: var(--accent);
    --fb-primary-hover: var(--accent-hover);
    --fb-on-primary: var(--accent-contrast);
    --fb-primary-soft: var(--signal-tint);
    --fb-local: var(--local-500);
    --fb-local-soft: var(--local-tint);
    --fb-danger: var(--danger-500);
    --fb-danger-soft: var(--danger-tint);
    --fb-await: var(--warning-500);
    --fb-await-soft: var(--warning-tint);
    --fb-ok: var(--success-500);
    --fb-ok-soft: var(--success-tint);
    --fb-patch: var(--patch-500);
    --fb-patch-soft: var(--patch-tint);

    --fb-ink: var(--text-primary);
    --fb-ink-dim: var(--text-secondary);
    --fb-ink-faint: var(--text-muted);      /* подпись поля, эйрбоу, отключённый пункт */
    --fb-hover: var(--bg-hover);
    --fb-active: var(--bg-active);
    --fb-overlay: var(--bg-overlay);

    --fb-wire: var(--wire-default);

    /* ---------- цвета типов артефактов ----------
       Одни и те же везде: пипка, конец провода, кромка ноды, клип на таймлайне, чип в списке. */
    --fb-t-text: var(--type-text);
    --fb-t-json: var(--type-json);
    --fb-t-image: var(--type-image);
    --fb-t-audio: var(--type-audio);
    --fb-t-video: var(--type-video);
    --fb-t-none: var(--text-disabled);

    /* ---------- приёмы ---------- */
    --fb-grad: linear-gradient(180deg, var(--fb-surface-hi), var(--fb-surface));

    /* Единственная текстура системы. Фотоподложек и иллюстраций нет вовсе: фон — либо плоская
       заливка ступени, либо эта сетка. */
    --fb-dot-step: 22px;

    /* Три значения, привязанные к самому полотну графа, а не к поверхности вообще.
       glow — единственный подсвет системы: тёплое пятно за графом, дающее глубину без декора.
       fade — растворение полотна под HUD: цвет ОБЯЗАН совпадать с фоном канваса, иначе внизу
              появится вторая, чуть иная плоскость.
       sheen — белый пробег по цветной кромке работающей ноды. Он лежит на насыщенном цвете, а не
              на поверхности, поэтому в светлой теме остаётся белым. */
    --fb-canvas-glow: rgba(255, 91, 36, .05);
    --fb-canvas-fade: rgba(8, 9, 11, .92);
    --fb-sheen: rgba(255, 255, 255, .85);

    /* Полупрозрачный фон страницы под размытием — фиксированная шапка лендинга. Это НЕ `--fb-overlay`:
       тот затемняет содержимое под диалогом и обязан быть тёмным в обеих темах, а шапка обязана быть
       цвета своей страницы. Пока они были одним значением, светлая тема получала серую полосу
       поперёк бумаги. */
    --fb-bg-blur: rgba(12, 14, 17, .72);

    /* Экран кадра в редакторе монтажа. Единственная поверхность, которая НЕ переключается по теме:
       это не элемент интерфейса, а сам ролик — он чёрный и на бумаге, и на графите. Светлая
       подложка под кадром означала бы, что монтаж в светлой теме показывает другой результат. */
    --fb-screen: #08090b;
    /* Текст, лежащий НА экране кадра. Не переключается по той же причине, что и сам экран: под ним
       всегда чёрное, и `--fb-ink` в светлой теме стал бы чёрным по чёрному. */
    --fb-screen-ink: var(--graphite-50);
    --fb-screen-ink-dim: var(--graphite-300);
}

[data-theme="light"] {
    --fb-canvas-fade: rgba(250, 249, 246, .92);
    --fb-bg-blur: rgba(241, 239, 233, .78);
}

/* Точечная сетка канваса. Шаг ведётся из JS вместе с содержимым, поэтому здесь только вид. */
.fb-dot-bg {
    background-image: radial-gradient(circle at 1px 1px, var(--grid-line) 1px, transparent 0);
    background-size: var(--fb-dot-step) var(--fb-dot-step);
}

/* Линейный вариант для документов и карточек: точка на большой спокойной плоскости рябит, линия —
   нет. Шаг крупнее сеточного, чтобы два фона не читались как один. */
.fabrika-grid-bg {
    background-image:
        linear-gradient(var(--grid-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
    background-size: 24px 24px;
}

/* Обе постоянные анимации системы. Объявлены здесь, а не в файле страницы: пульс индикатора
   нужен и графу, и списку прогонов, и лендингу. */
@keyframes fabrika-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .35; }
}

@keyframes fabrika-march {
    to { stroke-dashoffset: -16; }
}

@media (prefers-reduced-motion: reduce) {
    :root {
        --duration-fast: 0ms;
        --duration-base: 0ms;
        --duration-slow: 0ms;
    }
}
