/* Montserrat, self-hosted (@fontsource). CDN Google Fonts запрещён стайлгайдом
   и заблокирован CSP: font-src 'self'. unicode-range взят из исходных CSS
   пакета — без него кириллический сабсет тянется даже на латинице. */
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/assets/fonts/montserrat-cyrillic-400-normal.woff2') format('woff2');
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/assets/fonts/montserrat-latin-400-normal.woff2') format('woff2');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/assets/fonts/montserrat-cyrillic-500-normal.woff2') format('woff2');
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/assets/fonts/montserrat-latin-500-normal.woff2') format('woff2');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/assets/fonts/montserrat-cyrillic-600-normal.woff2') format('woff2');
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/assets/fonts/montserrat-latin-600-normal.woff2') format('woff2');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/assets/fonts/montserrat-cyrillic-700-normal.woff2') format('woff2');
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/assets/fonts/montserrat-latin-700-normal.woff2') format('woff2');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('/assets/fonts/montserrat-cyrillic-800-normal.woff2') format('woff2');
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('/assets/fonts/montserrat-latin-800-normal.woff2') format('woff2');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* Палитра — CSS-переменные вместо TS-объекта стайлгайда: без сборки типобезопасности
   всё равно нет, а токены в JS означали бы перерисовку всей вьюхи при смене темы.
   Текстовые токены подобраны под ХУДШИЙ фон (--bg-page), поэтому на карточках
   контраст только выше. Порог WCAG AA — 4.5:1. */
:root {
    --bg-page: #F0F2F5;
    --bg-card: #FFFFFF;
    --bg-header: #FFFFFF;
    --bg-row-hover: #E0F4FA;
    --bg-row-selected: #B8E8F5;
    --bg-row-alt: #FAFBFC;

    --text-primary: #1A1D21;
    --text-secondary: #5F6773;
    --text-muted: #6B6B6B;
    --text-purple: #8031A7;
    --text-cyan: #00728A;
    --text-red: #c81e1e;
    --text-green: #1B6E2E;
    --text-on-brand: #1A1D21;
    /* текст на насыщенных заливках (красная, серая, фиолетовая) — одинаков в обеих темах */
    --text-on-fill: #FFFFFF;

    --border-light: #E5E7EB;
    --border-medium: #D1D5DB;

    --cyan: #00A9CE;
    --purple: #8031A7;
    --red: #dc3545;
    --green: #1E7E34;
    --grey: #6c757d;

    /* Демонстрационные цвета базы знаний. НЕ переопределяются в тёмной теме
       намеренно: пример контраста 1,94:1 обязан выглядеть одинаково в обеих,
       иначе показывает не то отношение, которое подписано. */
    --demo-bg: #FFFFFF;
    --demo-fail: #BABABA;   /* 1,94:1 к белому — реальная находка с сайта застройщика */
    --demo-aa: #767676;     /* 4,5:1 — минимум WCAG AA для обычного текста */
    --demo-aaa: #595959;    /* 7:1 — уровень AAA */

    --shadow-card: 0 1px 3px rgba(0,0,0,0.04);
    --shadow-elevated: 0 4px 16px rgba(0,0,0,0.06);

    /* Подложка модального окна */
    --backdrop: rgba(26, 29, 33, 0.45);

    /* Полный ход кометы — туда и обратно. Единственный источник числа: его же
       читает runs.js, чтобы поставить новый бегунок в текущую фазу. Только в
       миллисекундах — JS разбирает значение как число. */
    --comet-period: 3400ms;
}

/* Бренд-цвета как фон и рамка не меняются; как ТЕКСТ на тёмном они нечитаемы,
   поэтому текстовые варианты осветлены (STYLEGUIDE.md:196). */
body.dark-theme {
    --bg-page: #1C1C28;
    --bg-card: #242430;
    --bg-header: #242430;
    --bg-row-hover: #3A3A50;
    --bg-row-selected: #4C4C6E;
    --bg-row-alt: #22222E;

    --text-primary: #E2E2E8;
    --text-secondary: #8E8EA0;
    --text-muted: #8A8A9C;
    --text-purple: #B895E8;
    --text-cyan: #5BC9E0;
    --text-red: #fca5a5;
    --text-green: #7BD68F;

    --border-light: #353540;
    --border-medium: #454550;

    --shadow-card: 0 1px 3px rgba(0,0,0,0.2);
    --shadow-elevated: 0 4px 24px rgba(0,0,0,0.3);

    /* Плотнее светлой: на тёмной странице полупрозрачная подложка почти не
       отделяет окно от фона, и модальность перестаёт читаться */
    --backdrop: rgba(0, 0, 0, 0.6);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
                 "Helvetica Neue", Arial, sans-serif;
    font-size: 13px;
    background: var(--bg-page);
    color: var(--text-primary);
}

/* UA-дефолт для form-controls — Arial; наследование обязательно (STYLEGUIDE.md:40) */
button, input, textarea, select { font-family: inherit; }

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

/* Шире на 200 px (по 100 с каждой стороны) — для таблиц, которым не хватает
   базовой ширины: в истории прогонов девять колонок, и подписи в них жались. */
.container--wide {
    max-width: 1400px;
}


header {
    background: var(--bg-header);
    color: var(--text-primary);
    border-bottom: 1px solid var(--border-light);
    padding: 16px 24px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* display: flex выше — авторский стиль перебивает [hidden] из UA-таблицы,
   поэтому атрибут hidden на шапке без этого правила не действует */
header[hidden] { display: none; }

header h1 { font-size: 20px; color: var(--text-purple); }

header nav a,
header nav button {
    background: transparent;
    border: none;
    color: var(--text-secondary);
    padding: 8px 16px;
    margin-left: 8px;
    border-radius: 50px;
    font-size: 13px;
    cursor: pointer;
    text-decoration: none;
}

header nav a:hover,
header nav button:hover { background: var(--bg-row-hover); color: var(--text-cyan); }

/* Активный раздел — единственное, чем шапка отличается между экранами.
   Начертание не меняем: смена веса сдвигала бы соседние пункты при переходе. */
header nav a[aria-current="page"] {
    background: var(--bg-row-hover);
    color: var(--text-cyan);
}

.card {
    background: var(--bg-card);
    border-radius: 12px;
    padding: 24px;
    margin-top: 20px;
    box-shadow: var(--shadow-card);
}

/* Карточка под таблицу: белого поля по бокам от неё не остаётся — таблица
   занимает карточку целиком, а поля держат заголовок, листалка и крайние
   ячейки. Объявлена СРАЗУ ПОСЛЕ `.card`: специфичность одинаковая, и стоя
   выше, она проиграла бы базовым полям (проверено замером — таблица тогда
   не доходит до края).
   `overflow: hidden` — чтобы заливка строки при наведении и выделении не
   вылезала за скругление карточки. */
.card--table {
    padding-left: 0;
    padding-right: 0;
    overflow: hidden;
}

.card--table > h3,
.card--table .pager { padding-left: 24px; padding-right: 24px; }

.card--table th:first-child,
.card--table td:first-child { padding-left: 24px; }
.card--table th:last-child,
.card--table td:last-child { padding-right: 24px; }

.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; }

/* Ряд контролов одной высоты. align-items: stretch, а не center: у полей и
   кнопки разные padding, border и line-height, поэтому подогнанные вручную
   высоты разошлись бы при первой же правке кегля. */
.form-row {
    display: flex;
    gap: 8px;
    margin-top: 12px;
    align-items: stretch;
}

/* Поле ряда: подсказка над контролом. Контрол растягивается по остатку высоты,
   поэтому равенство высот из .form-row сохраняется, даже если подсказка займёт
   две строки на узком экране. */
.field { display: flex; flex-direction: column; }
.field-grow { flex: 1; }

.field > .field-label {
    display: block;
    margin-bottom: 4px;
    font-size: 12px;
    font-weight: 400;
    color: var(--text-muted);
}

.field > input, .field > select, .field > button { flex: 1; }

.form-group input, .form-group select {
    width: 100%;
    padding: 6px 14px;
    border: 1.5px solid var(--border-light);
    border-radius: 8px;
    font-size: 13px;
    line-height: 1.8;
    background: var(--bg-card);
    color: var(--text-primary);
}

.form-group input:focus, .form-group select:focus {
    outline: 2px solid var(--cyan);
    outline-offset: 1px;
    border-color: var(--cyan);
}

/* Кнопка-пилюля (радиус 50) — доминирующий паттерн стайлгайда.
   Текст тёмный: белый на циане давал 2,78:1 при пороге AA 4,5:1,
   бренд-цвет заливки при этом сохранён (6,09:1). */
.btn {
    background: var(--cyan);
    color: var(--text-on-brand);
    border: none;
    padding: 8px 22px;
    border-radius: 50px;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
}

/* Анимируются только composited-свойства: transform, opacity, filter, box-shadow.
   Никакого transition: all — layout-анимации давали зависание интерфейса
   (STYLEGUIDE.md:258). prefers-reduced-motion намеренно не гасится глобально
   (STYLEGUIDE.md:344): гашение через 0.01ms all давало петлю transitionend ↔ resize. */
button:not(:disabled), a.btn {
    transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
}

button:not(:disabled):hover, a.btn:hover {
    transform: translateY(-1px);
    filter: brightness(1.08);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}

button:not(:disabled):active, a.btn:active {
    transform: translateY(0);
    filter: brightness(0.98);
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Кнопки без плашки: реагирует только текст, тень не нужна */
header nav button:hover, .theme-toggle:hover, .run-stop:hover { box-shadow: none; }

/* Ссылки раздела не подпадают под глобальное правило (оно про button и a.btn),
   но соседствуют с кнопкой «Выйти» — без этого их отклик отличался бы от неё */
header nav a { transition: transform 0.15s ease, filter 0.15s ease; }
header nav a:hover { transform: translateY(-1px); filter: brightness(1.08); }
header nav a:active { transform: translateY(0); filter: brightness(0.98); }
.btn-danger { background: var(--red); color: var(--text-on-fill); }
.btn-secondary { background: var(--grey); color: var(--text-on-fill); }

/* Модальное подтверждение. Позиционирование по центру, подложку и удержание
   фокуса берёт на себя браузер (`<dialog>` + `showModal`) — здесь оформление. */
.confirm-dialog {
    border: none;
    border-radius: 12px;
    padding: 24px;
    max-width: 380px;
    /* Центрирование `<dialog>` держится на его собственном `margin: auto`, а
       глобальный сброс `* { margin: 0 }` его снимает — без этой строки окно
       прижимается к левому верхнему углу */
    margin: auto;
    background: var(--bg-card);
    color: var(--text-primary);
    box-shadow: var(--shadow-elevated);
}

.confirm-dialog::backdrop { background: var(--backdrop); }

.confirm-dialog h3 { font-size: 15px; font-weight: 600; }
.confirm-dialog p { margin-top: 8px; }

.confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 20px;
}

/* Появление — только прозрачностью: `display` у диалога переключается скачком,
   и transition по нему не отработал бы вовсе */
.confirm-dialog[open] { animation: fadeIn 0.15s ease; }

/* Сообщение о готовности прогона. Уходит само, поэтому растворение задано
   переходом по opacity — единственному свойству из разрешённых к анимации,
   которое здесь и нужно. Подложка гасится своим переходом: у ::backdrop
   отдельный бокс, и прозрачность окна на него не распространяется —
   без этого фон пропадал бы рывком, пока карточка ещё видна. */
.notice {
    border: none;
    border-radius: 12px;
    padding: 20px 24px;
    max-width: 440px;
    /* Как и у .confirm-dialog: глобальный сброс `* { margin: 0 }` снимает у
       dialog его собственный `margin: auto`, и окно уезжает в левый верхний угол */
    margin: auto;
    background: var(--bg-card);
    color: var(--text-primary);
    box-shadow: var(--shadow-elevated);
    transition: opacity 0.45s ease;
}
.notice::backdrop {
    background: var(--backdrop);
    transition: opacity 0.45s ease;
}
.notice[open] { animation: fadeIn 0.15s ease; }
.notice--leaving, .notice--leaving::backdrop { opacity: 0; }

.notice-head { display: flex; align-items: center; gap: 12px; }
.notice-head h3 { font-size: 15px; font-weight: 600; }

/* Галочка: кружок и уголок из двух границ, повёрнутый на 45°. Рисунком, а не
   символом ✓ из шрифта — тот в кружке всегда стоит косо. */
.notice-check {
    position: relative;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--green);
}
.notice-check::after {
    content: '';
    position: absolute;
    left: 6px;
    top: 6px;
    width: 11px;
    height: 6px;
    border: 2px solid var(--text-on-fill);
    border-top: 0;
    border-right: 0;
    transform: rotate(-45deg);
}

.notice-facts {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px 16px;
    margin-top: 16px;
    font-size: 13px;
}
.notice-facts dt { color: var(--text-secondary); }
.notice-facts dd { overflow-wrap: anywhere; }

.hidden { display: none !important; }

/* Классы вместо inline-стилей в JS: иначе цвета оказываются размазаны
   по двум файлам и выпадают из токенов темы */
.btn-sm { padding: 4px 14px; font-size: 12px; border-radius: 50px; }
.btn-sm + .btn-sm { margin-left: 6px; }

.btn-download {
    display: inline-block;
    text-decoration: none;
    margin: 0 8px 12px 0;
}

.cell-ellipsis {
    max-width: 280px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Таблица с заданными долями колонок (`<colgroup>` в разметке). Содержимое на
   ширину больше не влияет: при авто-раскладке она зависела от того, какие
   прогоны попали на страницу, и колонки прыгали при листании, а длинные
   значения рвались пополам по пробелу. */
.table-fixed { table-layout: fixed; }

.table-fixed th,
.table-fixed td {
    position: relative;
    white-space: nowrap;
    overflow: hidden;
}

/* Не поместившееся не обрывается резко, а уходит под затухание к цвету строки.
   Появляется оно только там, где под ним текст: на пустом месте градиент к
   собственному фону невидим, поэтому условий в JS не нужно. Полное значение
   показывает подсказка — её ставит `markClipped` ровно обрезанным ячейкам. */
.table-fixed tbody td::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 1px;      /* линию под строкой затухание не перекрывает */
    width: 26px;
    background: linear-gradient(to right, transparent, var(--bg-card));
    pointer-events: none;
}

/* Затухание идёт к тому фону, на котором лежит: иначе на наведённой и
   выделенной строке оно выглядит светлым мазком поперёк заливки */
.table-fixed tbody tr:hover td::after {
    background: linear-gradient(to right, transparent, var(--bg-row-hover));
}

.table-fixed tbody tr.row-selected td::after,
.table-fixed tbody tr.row-selected:hover td::after {
    background: linear-gradient(to right, transparent, var(--bg-row-selected));
}

/* Строка журнала — не колонка таблицы: у неё своя прокрутка и своя ширина */
.table-fixed .log-row td { overflow: visible; }
.table-fixed .log-row td::after { content: none; }

/* --- База знаний --- */

.kb-lead {
    margin: 0 0 14px;
}

.kb-note {
    margin: 12px 0 0;
    font-size: 13px;
    color: var(--text-secondary);
}

.kv-list > div {
    padding: 8px 0;
    border-bottom: 1px solid var(--border-light);
}

.kv-list > div:last-child {
    border-bottom: none;
}

.kb-demo,
.kb-demo-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    margin: 16px 0;
}

.pill {
    display: inline-block;
    min-width: 74px;
    text-align: center;
    border-radius: 999px;
    padding: 3px 12px;
    font-weight: 600;
    font-size: 13px;
    color: var(--text-on-fill);
}

.pill-bad  { background: var(--red); }
.pill-warn { background: var(--grey); }
.pill-ok   { background: var(--green); }

/* Образцы контраста: собственный фон и цвет текста — это и есть содержание примера */
.swatch {
    flex: 1 1 200px;
    background: var(--demo-bg);
    border: 1px solid var(--border-medium);
    border-radius: 8px;
    padding: 14px 16px;
}

.swatch-text {
    display: block;
    font-size: 22px;
    font-weight: 700;
}

.swatch-meta {
    display: block;
    margin-top: 6px;
    font-size: 12px;
    color: var(--demo-aaa);
}

.swatch-fail .swatch-text { color: var(--demo-fail); }
.swatch-aa   .swatch-text { color: var(--demo-aa); }
.swatch-aaa  .swatch-text { color: var(--demo-aaa); }

.type-demo {
    flex: 1 1 240px;
    padding: 12px 14px;
    border: 1px solid var(--border-light);
    border-radius: 8px;
}

.type-small { font-size: 12px; }
.type-ok    { font-size: 16px; }

.kb-heuristics {
    margin: 0;
    padding-left: 22px;
    columns: 2;
    column-gap: 32px;
}

@media (max-width: 700px) {
    .kb-heuristics { columns: 1; }
}

/* Компактная строка прогона на главной: адрес и итог сверху, параметры мельче снизу */
.run {
    padding: 10px 0;
    border-bottom: 1px solid var(--border-light);
}

.run:last-child {
    border-bottom: none;
}

/* У идущего прогона разделителем служит сама дорожка бегунка — иначе линий
   было бы две вплотную. Нижний отступ снимается, чтобы дорожка встала ровно
   туда, где проходила граница. */
.run--running {
    border-bottom: none;
    padding-bottom: 0;
    /* Дорожка выше границы на свои 6px, и без этого строка подрастала бы при
       запуске анализа. Отрицательный отступ ставится на строку, а не на саму
       дорожку: у последнего потомка он схлопывается наружу и высоту родителя
       не уменьшает (проверено — строка оставалась выше на 3px). */
    margin-bottom: -2px;
}

/* Линия дорожки встаёт ровно туда, где проходила граница: у готовой строки
   это 10px под содержимым, здесь — 7px отступа плюс половина шестипиксельной
   высоты дорожки. */
.run--running .comet {
    margin-top: 7px;
}

.run-head {
    display: flex;
    gap: 8px;
    /* center, а не baseline: увеличенный transform'ом крестик от базовой линии
       свисал бы вниз (низ скейла уходит ниже baseline), по центру он ровно на
       одной линии со статусом */
    align-items: center;
}

.run-url {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;   /* иначе длинный адрес не даёт себя ужать до многоточия */
}

/* margin-left:auto уводит статус и крестик к правому краю; крестик идёт после
   статуса, поэтому оказывается у самого края */
.run-state {
    margin-left: auto;
    color: var(--text-secondary);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.run-meta {
    margin-top: 2px;
    font-size: 13px;
    color: var(--text-muted);
}

/* Остановить прогон — красный крестик. Кнопка держит только область нажатия
   (padding для пальца), сама она при наведении неподвижна: глобальный подъём и
   тень погашены, рамка фокуса убрана, чтобы вокруг крестика не появлялось поле. */
.run-stop {
    padding: 0 4px;   /* без вертикального поля — иначе идущая строка выше готовой */
    border: none;
    background: none;
    line-height: 1;
    cursor: pointer;
}

.run-stop:not(:disabled):hover,
.run-stop:focus,
.run-stop:focus-visible {
    transform: none;
    filter: none;
    box-shadow: none;
    outline: none;
}

.run-stop:disabled {
    cursor: default;
}

/* Анимируется ТОЛЬКО крестик: увеличивается и чуть ярче, поле вокруг неподвижно.
   В потоке глиф занимает как обычный текст (font-size 16), а крупный вид даёт
   transform: он не влияет на раскладку, поэтому идущая строка не становится
   выше готовой (24px-глиф в потоке раздувал её высоту с 16 до 28px). */
.run-stop-x {
    display: inline-block;
    font-size: 16px;
    line-height: 1;
    color: var(--text-red);
    transform: scale(1.5);   /* визуально ~24px, высоту строки не трогает */
    transition: transform 0.15s ease, filter 0.15s ease;
}

.run-stop:not(:disabled):hover .run-stop-x {
    transform: scale(1.85);
    filter: brightness(1.1);
}

.run-stop:disabled .run-stop-x {
    color: var(--text-muted);
}

/* Ссылки на отчёты в списках прогонов. Свой цвет обязателен: без него браузер
   красит их тёмно-синим, и в тёмной теме они почти сливаются с фоном строки. */
.report-link { color: var(--text-cyan); }
.report-link:hover { color: var(--text-purple); }

/* Строка «Последних прогонов» раскрывается по нажатию — как строка истории */
.run-clickable { cursor: pointer; }

/* Панель журнала внутри строки главной: она уже внутри карточки с полями,
   поэтому своих полей по краям ей не нужно */
.run-log { margin-top: 8px; }

/* Журнал прогона: раскрывается строкой под своей строкой истории.
   Ячейка без внутренних полей — их держит сама панель, иначе при свёрнутой
   высоте от строки оставалась бы полоса в 24 px. */
.log-row > td { padding: 0; }

/* Полоса слева фирменным цветом привязывает панель к своей строке: фон рядов
   таблицы и фон панели различаются слабо, и без неё журнал читался бы как
   продолжение списка, а не как подробность одного прогона (приём взят у .dive) */
.log-panel {
    position: relative;   /* якорь для кнопки копирования в углу */
    /* Справа поля почти нет: полоса прокрутки журнала должна идти у самого
       края панели, а не в глубине — там её и ищут глазами */
    padding: 10px 4px 10px 12px;
    background: var(--bg-row-alt);
    border-left: 3px solid var(--cyan);
}

/* Высота ограничена: журнал бывает длиннее экрана, а раскрытая строка не
   должна прятать таблицу. Перенос по словам, а не горизонтальная прокрутка —
   журнал читают, а не листают вбок. */
/* Поле журнала занимает панель целиком — полоса прокрутки идёт по её краю.
   Кнопка стоит левее полосы, а текст под неё не подлезает: место под кнопку
   держит padding, полосу он не двигает (она принадлежит полю и стоит у его
   границы), поэтому одно другому не мешает. */
.log-text {
    max-height: 150px;
    overflow: auto;
    margin: 0;
    padding-right: 56px;
    font-size: 12px;
    line-height: 1.45;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    color: var(--text-primary);
}

.log-tools {
    position: absolute;
    top: 10px;
    right: 26px;     /* левее полосы прокрутки: она идёт у самого края панели */
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* Символ, а не плашка: кнопка лежит поверх текста и не должна его перебивать.
   Полупрозрачна, пока на неё не навели. */
.log-copy-btn {
    border: none;
    background: none;
    padding: 2px 4px;
    font-size: 16px;
    line-height: 1;
    color: var(--text-secondary);
    opacity: 0.45;
    cursor: pointer;
    transition: opacity 0.15s ease;
}

/* Глобальное правило поднимает кнопки и вешает тень — кнопке без плашки это
   не идёт: реагирует только прозрачность самого символа */
.log-copy-btn:not(:disabled):hover {
    opacity: 1;
    transform: none;
    filter: none;
    box-shadow: none;
}

.copy-status {
    font-size: 12px;
    color: var(--text-secondary);
}

/* После базового правила: у элемента оба класса разом, и при равной
   специфичности цвет берётся от того, что объявлено ниже */
.copy-status--error { color: var(--text-red); }

/* Поле для запасного способа копирования. Не `display: none` и не
   `visibility: hidden`: скрытое так поле не выделяется, а команда копирования
   работает именно с выделением. Уводим за край экрана. */
.copy-sink {
    position: fixed;
    top: -1000px;
    left: 0;
    opacity: 0;
    pointer-events: none;
}

.report-head { font-weight: 600; margin-bottom: 10px; }
.report-meta { color: var(--text-muted); font-size: 13px; margin-bottom: 8px; }
.subhead { font-weight: 600; margin-top: 8px; }

.dive {
    border-left: 3px solid var(--cyan);
    padding: 6px 12px;
    margin-bottom: 8px;
}

/* Сворачивание — единственная санкционированная «height-анимация»:
   grid-template-rows 0fr ↔ 1fr идёт по реальной высоте контента,
   без измерений в JS (STYLEGUIDE.md:343) */
.collapse {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.25s ease;
}

.collapse > * { overflow: hidden; }
.collapse.open { grid-template-rows: 1fr; }

.user-form {
    margin-top: 16px;
    padding: 16px;
    background: var(--bg-row-alt);
    border: 1px solid var(--border-light);
    border-radius: 8px;
}

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

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.spinner {
    display: inline-block;
    width: 13px;
    height: 13px;
    margin-right: 8px;
    vertical-align: -2px;
    border: 2px solid var(--border-medium);
    border-top-color: var(--cyan);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

/* Бегунок-комета: точка с хвостом идёт по линии, пока считается анализ.
   Спиннер отвечает на «работает ли вообще», комета — на «каким типом анализа»:
   цвет задаёт модификатор из каталога MODELS (runs.js).
   Едет transform'ом: только он и opacity/filter/box-shadow не заставляют
   раскладку пересчитываться на каждом кадре. */
.comet {
    display: block;
    position: relative;
    height: 6px;
    margin-top: 6px;
    overflow: hidden;            /* голова уходит за край, а не растягивает строку */
    color: var(--cyan);          /* тон по умолчанию; ниже переопределяется */
}

.comet--purple { color: var(--purple); }
.comet--cyan   { color: var(--cyan); }
.comet--grey   { color: var(--grey); }

/* Сама линия, по которой бежит комета. Толщина и цвет — как у разделителя
   строк: по готовности бегунок исчезает, и на его месте остаётся обычная
   граница, поэтому разницы быть не должно. */
.comet::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 1px;
    background: var(--border-light);
}

.comet-head {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 32%;
    animation: comet-run var(--comet-period) ease-in-out infinite;
}

/* Хвост: прозрачный сзади, насыщенный у головы */
.comet-head::before {
    content: '';
    position: absolute;
    left: 0;
    right: 3px;
    top: 50%;
    height: 2px;
    margin-top: -1px;
    border-radius: 1px;
    background: linear-gradient(90deg, transparent, currentColor);
}


/* Голова */
.comet-head::after {
    content: '';
    position: absolute;
    right: 0;
    top: 50%;
    width: 6px;
    height: 6px;
    margin-top: -3px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 6px currentColor;
}

/* Ход туда и обратно, от края до края. Смещение задано в процентах ОТ ШИРИНЫ
   САМОЙ КОМЕТЫ (32% дорожки), поэтому уход за правый край — это 100/32 ≈ 313%
   её собственной ширины.
   На краю комета разворачивается (`scaleX(-1)`), иначе на обратном пути она
   летела бы хвостом вперёд. Разворот мгновенный и делается в тот же кадр, где
   она скрыта за краем дорожки, — подмены не видно. Направление `alternate`
   здесь не годится: оно проиграло бы назад и сам разворот. */
@keyframes comet-run {
    0%    { transform: translateX(-100%) scaleX(1); }
    49.9% { transform: translateX(313%) scaleX(1); }
    50%   { transform: translateX(313%) scaleX(-1); }
    100%  { transform: translateX(-100%) scaleX(-1); }
}

.text-danger { color: var(--text-red); }
.text-secondary { color: var(--text-secondary); }

.theme-toggle {
    position: fixed;
    right: 16px;
    bottom: 16px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--border-medium);
    background: var(--bg-card);
    color: var(--text-primary);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    box-shadow: var(--shadow-elevated);
    z-index: 100;
}

.theme-toggle:hover { border-color: var(--cyan); color: var(--text-cyan); }

/* Лого Media 108: тёмный вариант на светлой теме, светлый — на тёмной */
/* Лого — ссылка на главную; наследуемое оформление ссылки здесь не нужно */
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.brand-logo { height: 28px; width: auto; display: block; }
.brand-logo-dark { display: none; }
body.dark-theme .brand-logo-light { display: none; }
body.dark-theme .brand-logo-dark { display: block; }

.login-logo { height: 32px; margin: 0 auto 16px; }

table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 16px;
}

th, td {
    text-align: left;
    padding: 12px;
    border-bottom: 1px solid var(--border-light);
}

th { background: var(--bg-row-alt); font-weight: 600; color: var(--text-secondary); }

tbody tr:hover { background: var(--bg-row-hover); }
/* Выделение строки истории: перебивает наведение, иначе выбранная строка
   светлеет под курсором и выглядит как невыбранная */
.row-clickable { cursor: pointer; }
tbody tr.row-selected,
tbody tr.row-selected:hover { background: var(--bg-row-selected); }

.pager {
    display: flex;
    align-items: center;
    justify-content: center;   /* по центру карточки, а не прижата к её левому краю */
    gap: 12px;
    margin-top: 16px;
}
.pager button:disabled { opacity: .45; cursor: default; }
.pager label { display: inline-flex; align-items: center; gap: 6px; }
/* Рамка, радиус и размер шрифта — как у селектов формы; вертикальные отступы
   меньше, чтобы селектор встал на одну линию с кнопками btn-sm */
.pager select {
    padding: 4px 12px;
    border: 1.5px solid var(--border-light);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--text-primary);
    font-size: 13px;
    cursor: pointer;
}
.pager select:focus { outline: 2px solid var(--cyan); outline-offset: 1px; }

.tag {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 50px;
    font-size: 12px;
    font-weight: 500;
}

.tag-admin { background: var(--purple); color: var(--text-on-fill); }
.tag-analyst { background: var(--cyan); color: var(--text-on-brand); }
.tag-viewer { background: var(--grey); color: var(--text-on-fill); }

.error { color: var(--text-red); margin-top: 8px; }
.success { color: var(--text-green); margin-top: 8px; }

.login-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
}

.login-box {
    width: 360px;
    padding: 32px;
    background: var(--bg-card);
    border-radius: 12px;
    box-shadow: var(--shadow-elevated);
}

.login-box h2 { margin-bottom: 20px; text-align: center; color: var(--text-purple); }
.login-box .btn { width: 100%; margin-top: 8px; }
