/* ============================================================================
   VentCalc — компонентні стилі. Кольори/простір/типографіка беруться ВИКЛЮЧНО
   з design-tokens.css (сирих hex тут бути не повинно).
   ============================================================================ */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--t-base);
    line-height: var(--lh-base);
    color: var(--c-text);
    background: var(--c-bg);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { line-height: var(--lh-tight); font-weight: 650; letter-spacing: -0.012em; margin: 0; }
/* Кегль ЗАГОЛОВКА СТОРІНКИ — плавний, і саме тут, на самому h1. Доти плавним
   був лише клас .page-title, тобто 15 сторінок із 25: решта десять малювали
   той самий за роллю заголовок сталими 28 px, і на 390 px «Підбір виробу за
   витратою» займав 67,2 px проти 53,9 px у сусідньої «Норми вентиляції
   приміщень» (заміряно 10.09 обходом дванадцяти сторінок). Один елемент — два
   розміри, поділені лише тим, яка сторінка встигла перейти на PageHeading.
   Підлога 1,5rem, а не 1,375rem: на дефолтній підлозі h1 сходився з h2
   (22,455 проти 22,0 — 0,455 px різниці), тобто ієрархія зникала рівно там,
   де екран найвужчий і вона найпотрібніша. */
h1 { font-size: clamp(1.5rem, 1.15rem + 1.55vw, 1.75rem); }
h2 { font-size: var(--t-lg); }
h3 { font-size: var(--t-md); }
p { margin: 0 0 var(--s-3); }

/* <small> не мав у продукті ЖОДНОГО оголошення кегля — його давала стильова
   таблиця браузера (`font-size: smaller`, тобто ×0,833 ВІД БАТЬКА). Через це
   один і той самий підказковий рядок важив 10,83 px усередині мітки 13 px,
   12,5 px усередині блоку 15 px, а в чотирьох місцях 12 px, дописані руками.
   Заміряно 16.09 на телефоні (390 px, 14 сторінок): 37 видимих <small>, ТРИ
   різні кеглі, і 20 із них НИЖЧЕ за підлогу 11 px, названу в токенах.

   Це той самий дрейф, що вже прибрали в бейджах джерела (--t-2xs), лише
   непомітний у CSS: правила, яке його створює, у нас немає — воно в браузері.
   Роль <small> у продукті одна — службовий підпис, тож і токен один. */
small { font-size: var(--t-xs); }

a { color: var(--c-brand); text-decoration-color: color-mix(in srgb, var(--c-brand) 35%, transparent); text-underline-offset: 2px; }
a:hover { color: var(--c-brand-hover); }

h1:focus, h2:focus, h3:focus { outline: none; }  /* FocusOnNavigate фокусує h1 — рамка тут зайва */
:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; border-radius: var(--r-sm); }

/* Числа (ціни, розміри, аеро) — моноширинні цифри, щоб колонки не «стрибали» */
.price-total, .price-lines, .quote-table, .aero-card dd, .variant-price,
.variant-aero dd, .saved-table, .field input { font-variant-numeric: tabular-nums; }
/* Одиниці на кшталт «м³/год» не рвуться переносом після «/» — розірвана
   одиниця читається як одрук (UI/UX-прохід 18.08). */
.nowrap { white-space: nowrap; }

/* Пошук у кабінеті (зріз 50) і рядок «показано N з M»: фільтр, що звузив список
   до нуля, без цього рядка читався б як «збережене зникло». */
.account-search { max-width: 420px; margin-bottom: var(--s-4); }
.account-filtered { margin: 0 0 var(--s-2); color: var(--c-muted); font-size: var(--t-sm); }
/* === Елементи форми ====================================================== */
.field { display: flex; flex-direction: column; gap: var(--s-1); margin-bottom: var(--s-4); min-width: 0; }
.field > span, .field-label { font-weight: 600; font-size: var(--t-sm); color: var(--c-text-soft); }
.field small, .field-label small { font-weight: 400; color: var(--c-muted); }
/* У вузькій колонці (1000–1240 px із сайдбаром, або мобільний) підказка рвала
   підпис посеред дужок — «Довжина L, мм (100-1000, / крок 5)». Даємо їй рядок. */
@media (max-width: 1240px) {
    .field > span small, .field-label small { display: block; margin-top: 1px; }
}

/* Чекбокс-поле: без цього правила чекбокс успадковував колонку .field і
   width:100%/min-height:42px від .field input — величезний квадрат по центру
   з підписом ПІД ним (аудит UI: «Пленум-бокс PB», «Лише ADPI-комфорт»). */
.field-check { flex-direction: row; align-items: baseline; gap: var(--s-2); cursor: pointer; }
.field-check input[type="checkbox"] {
    width: 1.05rem; height: 1.05rem; min-height: 0; flex: none;
    margin: 0; transform: translateY(2px); accent-color: var(--c-brand);
    cursor: pointer;
}
.field-check > span { font-weight: 600; font-size: var(--t-sm); color: var(--c-text-soft); }

/* Поле, на яке скаржиться валідатор: саме повідомлення живе внизу форми, і без
   підсвітки зв'язок «крок 5 мм» ↔ «Довжина L» доводилось вгадувати. */
.field input.field-invalid, .field select.field-invalid {
    border-color: var(--err-600); background: color-mix(in srgb, var(--err-600) 5%, var(--c-surface));
}
.field input.field-invalid:focus { outline-color: var(--err-600); }

.field input, .field select, .field textarea, .save-quote-title {
    width: 100%;
    padding: 0.55rem 0.7rem;
    min-height: 42px;
    font: inherit;
    color: var(--c-text);
    background: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-sm);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--c-muted); }

/* Підказка в полі — РОЛЛЮ теми, а не дефолтом браузера. Правила `::placeholder`
   не було в усьому src/, тож обидві теми діставали браузерне rgb(117,117,117):
   у темній це 3,72:1 до фону поля при потрібних 4,5:1 (заміряно 10.09 на
   чотирьох полях — назва об'єкта в КП, пошук каталогу, фільтр моделей і
   вставка специфікації). Палітра до цього кольору не доходила взагалі, бо
   ніхто його не задавав. */
input::placeholder, textarea::placeholder { color: var(--c-muted); opacity: 1; }

/* 16 px на телефоні — не косметика, а поведінка САМОГО браузера: Safari на iOS
   зумить сторінку на фокусі поля, кегль якого менший за 16 px, і назад воно не
   від'їжджає. Заміряно 10.09 обходом семи сторінок: із 20 видимих полів
   16 px не мало ЖОДНЕ (15 px — сімнадцять, 13 px — два, 12 px — лістбокс
   типорозмірів), тобто зум ловив користувача на кожному дотику до будь-якого
   поля застосунку. Правило шириною, а не «вказівником»: `pointer: coarse` не
   покриває iPad із клавіатурою, а зайві 16 px на вузькому екрані нікому не
   шкодять.

   Дрібні службові поля (кількість у рядку КП) сюди теж входять навмисно: зум
   на них найпомітніший, бо стоять вони посеред таблиці. */
@media screen and (max-width: 639px) {
    /* `!important` тут навмисно, і це не лінощі. Кегль полів задають ЧОТИРНАДЦЯТЬ
       компонентних правил із класами (.field input {font: inherit}, .qty-input,
       .size-filter, #spec-text…), тобто зі специфічністю 0-1-1 і вище; правило
       без важливості програло б їм усім, і перший замір це показав — після
       нього 9 полів із 27 лишились дрібними. Дублювати сюди перелік класів
       означало б завести список, що застаріває мовчки. Властивість тут
       ПОВЕДІНКОВА (чи зумить браузер), а не оформлювальна, тож перебити її
       складніше й не треба. */
    /* Поле файлу — виняток: воно не зумить (відкриває системний вибірник), а
       кегль там міняє розмір НАТИВНОЇ кнопки всередині зони перетягування. */
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
    select, textarea { font-size: 16px !important; }
}
.field input:focus, .field select:focus, .field textarea:focus, .save-quote-title:focus {
    outline: none; border-color: var(--c-brand); box-shadow: var(--ring);
}
.field select {
    appearance: none;
    /* Трикрапка, а не кліп посеред слова: на 390 px «…LDT (з лам» читалось як
       битий текст, і розрізнювальний суфікс виконання зникав без сліду
       (UI/UX-прохід 18.08; Chromium підтримує text-overflow на select). */
    text-overflow: ellipsis;
    background-image: linear-gradient(45deg, transparent 50%, var(--c-muted) 50%),
                      linear-gradient(135deg, var(--c-muted) 50%, transparent 50%);
    background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
    padding-right: var(--s-8);
}
.field select:disabled { background-color: var(--c-surface-sunk); color: var(--c-muted); }
/* Ряд полів. Підписи різної висоти зсували СВОЇ інпути: «Колір RAL (9016 — у
   базовій ціні)» переносився на другий рядок, і його поле опинялось нижче за
   сусідні «Кут ламелей» і «Кількість, шт». Медіазапит нижче лікував це лише до
   1240 px (там підказка отримує власний рядок у КОЖНОГО поля) — на широкому
   екрані ряд і далі розсипався.
   Тепер рядки «підпис / контрол / дельта» вирівняні між полями через subgrid:
   скільки б рядків не зайняв будь-який підпис, усі інпути стоять на одній лінії.
   Без підтримки subgrid лишається попередня поведінка з вирівнюванням по низу. */
.field-row { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: flex-end; }
.field-row .field { flex: 1 1 140px; margin-bottom: var(--s-4); }

/* Підбір за витратою: п'ять полів із підказками різної довжини. Підказка завжди
   своїм рядком і мінімум 170 px на колонку — інакше «Довжина L, м (до стіни / пів
   відстані…)» ламалась на 3 рядки, а поля пливли по вертикалі (аудит UI). */
.select-inputs .field > span small { display: block; margin-top: 1px; }
/* min-width, а не лише flex-basis: у контейнері 620px четверо полів у ряд
   стискались до 143px, і селект різав власну опцію — «Тихий (≤25 д…»
   (UI/UX-огляд 03.08). З мінімумом зайве поле переноситься в наступний ряд.
   200 px і явний міжколонковий проміжок — після зрізу 04.08 (14 полів):
   на 170 px підпис «Висота решітки ≤, мм» упирався в сусідній «Приєднання
   Ду, мм» без зазору, а селект призначення різав «Приплив (розда…». */
.select-inputs .field { flex-basis: 200px; min-width: 200px; }
/* Подвійний селектор не для краси: у subgrid-браузерах шаблон колонок задає
   .field-row (minmax 140px) ПІЗНІШЕ у файлі, і одинарний .select-inputs йому
   програвав за порядком — колонки стискались до 140 px, підписи наїжджали
   на сусідні («…ммПриєднання Ду, мм»). */
.field-row.select-inputs {
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    column-gap: var(--s-4);
}

/* Фото-інпут: нативний «Choose File» англійською і не в стилі застосунку —
   ховаємо контрол (лишаючи його фокусованим), клік по label відкриває діалог. */
.photo-drop { position: relative; }
.photo-drop input[type="file"] {
    position: absolute; width: 1px; height: 1px; opacity: 0;
    overflow: hidden; clip: rect(0 0 0 0);
}
.photo-drop .photo-pick { align-self: flex-start; pointer-events: none; }
.photo-drop input[type="file"]:focus-visible ~ .photo-pick {
    outline: 2px solid var(--c-brand); outline-offset: 2px;
}
/* Drop-зона: рамка є ЗАВЖДИ (інакше про перетягування не здогадатись),
   підсвічується під файлом (клас ставить initPhotoDrop в App.razor). */
.photo-drop {
    padding: var(--s-4); border: 2px dashed var(--c-border-strong);
    border-radius: var(--r-lg); transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.photo-drop.drop-over { border-color: var(--c-brand); background: color-mix(in srgb, var(--c-brand) 6%, transparent); }
.photo-example { width: 168px; max-width: 60%; border-radius: var(--r-sm); border: 1px solid var(--c-border); }
/* Приклад — окремим блоком ПІД дроп-зоною, не всередині: там він читався як
   прев'ю «сюди ляже ваш знімок» (UI/UX-прохід 18.08). */
.photo-example-block { margin: var(--s-3) 0 0; }
.photo-example-block figcaption { font-size: var(--t-xs); color: var(--c-muted); margin-top: var(--s-1); }

@supports (grid-template-rows: subgrid) {
    .field-row {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
        grid-template-rows: auto auto auto;
        align-items: stretch;
        margin-bottom: var(--s-4);
    }

    .field-row .field {
        display: grid;
        grid-row: span 3;
        grid-template-rows: subgrid;
        row-gap: var(--s-1);
        margin-bottom: 0;
    }
}
.field-delta { color: var(--warn-600); font-size: var(--t-xs); font-weight: 600; }
/* «Порахувати» (реліз 1.3.0, задача 8): затиск входу в межі методики — під
   полем, кольором попередження, як дельта RAL: число в полі людина ввела сама,
   а в розрахунок пішла межа, і це мусить бути видно без alert (спек §6.3). */
.input-clamp { color: var(--warn-600); font-size: var(--t-xs); font-weight: 600; }
/* Ряд L · H · Δt «Порахувати»: мінімум колонки — у rem, а не сталі 140 px .field-row
   (8,75rem — ті самі 140 px при звичайному кеглі, тож на 100 % не змінюється нічого).
   Підпис «Різниця температур Δt» не переноситься всередині слова «температур», і на
   200 % тексту (WCAG 1.4.12) воно ширше за колонку в 140 px: третє поле вилазило з ряду
   на 32 px (Inter) і 52 (DejaVu), з DejaVu — ще й за край вікна на 1440 (фінальний
   адверсарний прохід 1.3.0, B-5). Мінімум, що росте з кеглем, переносить ряд у
   стовпчик раніше, ніж слово перестане вміщатися; min(…, 100%) — щоб колонка не
   ставала ширшою за сам ряд. */
.field-row.calc-jet { grid-template-columns: repeat(auto-fit, minmax(min(8.75rem, 100%), 1fr)); }
.face-note { color: var(--c-muted); font-size: var(--t-xs); }
/* Перехресний місток між конфігураторами (зріз 162). Окремий клас, а не
   .face-note: тести й скрипти адресують .face-note як ЄДИНУ примітку серії,
   і другий елемент того самого класу ламає строгі локатори. */
.cross-link { color: var(--c-muted); font-size: var(--t-xs); margin: 0 0 var(--s-3); }
/* Величина дальності біля числа (L₀,₂ у KQ) і примітка про змішані величини
   у видачі /select — рішення власника 29.08 «позначка величини скрізь». */
.throw-magnitude { color: var(--c-text-soft); font-weight: 600; margin-right: 2px; }
.throw-mixed-note { color: var(--c-muted); font-size: var(--t-xs); margin: 0 0 var(--s-2); }
/* Кнопка, що виглядає лінком (вихід із розділу на /accessory): дія рахується
   на сервері в момент кліку — на відміну від href, який рендер міг лишити
   застарілим (жива гонка, спіймана e2e локально і на шарді CI). */
.link-inline { background: none; border: 0; padding: 0; font: inherit;
  color: var(--c-brand); text-decoration: underline; cursor: pointer; }
.link-inline:hover { color: var(--c-brand-hover); }

/* === Кнопки ============================================================== */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
    min-height: 42px; padding: 0.5rem 1rem;
    font: inherit; font-weight: 600; font-size: var(--t-sm);
    border-radius: var(--r-sm); border: 1px solid transparent;
    cursor: pointer; text-decoration: none; white-space: nowrap;
    transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
                color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.btn:active { transform: translateY(1px); }

/* Базовий вигляд — вторинна кнопка. Доти `.btn` без модифікатора не мав ні тла,
   ні рамки (`border: 1px solid transparent`), тож «Зберегти в кабінет» на /quote
   і «Вийти» в кабінеті малювались голим текстом і на кнопки не скидались.
   Модифікатори нижче перекривають це за порядком у файлі. */
.btn { background: var(--c-surface); color: var(--c-text-soft); border-color: var(--c-border-strong); }
.btn:hover { border-color: var(--c-brand); color: var(--c-brand); }

.btn-primary { background: var(--c-brand); color: var(--c-on-brand); border-color: var(--c-brand); box-shadow: var(--sh-xs); }
.btn-primary:hover { background: var(--c-brand-hover); border-color: var(--c-brand-hover); color: var(--c-on-brand); }
.btn-ghost { background: var(--c-surface); color: var(--c-text-soft); border-color: var(--c-border-strong); }
.btn-ghost:hover { border-color: var(--c-brand); color: var(--c-brand); }
.btn-danger { background: var(--c-surface); color: var(--err-600); border-color: var(--c-border-strong); }
.btn-danger:hover { background: var(--err-50); border-color: var(--err-600); }
.btn-small { min-height: 30px; padding: 0.15rem 0.5rem; font-size: var(--t-xs); }
.btn-request { margin-top: var(--s-2); }
/* Вимкнена кнопка мусить ВИГЛЯДАТИ вимкненою (INSTALLER-X-1, аудит 06.10):
   «Додати» на /import після додавання ставала disabled, але лишалась тим самим
   брендовим прямокутником, і людина читала її як живу. Ті самі токени, що
   в `.field select:disabled`; специфічність вища за модифікатори, тож порядок
   у файлі тут не важить. */
.btn:disabled, .btn:disabled:hover {
    background: var(--c-surface-sunk); color: var(--c-muted);
    border-color: var(--c-border-strong); box-shadow: none; cursor: not-allowed;
}
.btn:disabled:active { transform: none; }
.actions { display: flex; gap: var(--s-2); margin-top: var(--s-4); flex-wrap: wrap; }

/* Базове правило мусить стояти ПЕРЕД медіа-блоком, який його вмикає:
   специфічність однакова, тож при зворотному порядку `display: block`
   у медіа не діяв би зовсім (заміряно 10.09 — підказку показувало чуже
   правило з іншим порогом, тобто в смузі 1181–1240 px вона брехала). */
.chips-hint { display: none; font-weight: 400; color: var(--c-muted); font-size: var(--t-xs); margin-top: 2px; }

/* === Чипи типорозмірів =================================================== */
.preset-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }
@media (max-width: 1180px) {
    /* До ~1180 px колонка форми лишається вузькою (350–450 px), і 21 типорозмір
       розкладається в 6–7 рядів чипів, які ховають усе інше. Горизонтальна стрічка
       тримає блок в один ряд; згасання праворуч показує, що ряд продовжується. */
    .preset-chips {
        flex-wrap: nowrap; overflow-x: auto; scroll-snap-type: x proximity;
        padding-bottom: var(--s-1); scrollbar-width: thin;
        mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
        /* Фокус Tab-ом гортає стрічку до чипа (nav.js, focusin) — але без
           відступу чип лягав РІВНО під згасання: на 1024 px видно 28–37 з
           85 px (клавіатурний прохід 30.09). Відступ прокрутки — ширина
           згасання; внутрішній відступ у кінці дає ОСТАННЬОМУ чипу місце
           вийти з-під нього. */
        scroll-padding-inline: 0 28px;
        padding-inline-end: 28px;
    }
    .preset-chips .chip { flex: 0 0 auto; scroll-snap-align: start; }

    /* КОРОТКИЙ ряд (≤8 чипів) краще загорнути, ніж гортати: два ряди на 390 px
       читаються цілком, а стрічка ховала половину варіантів за згасанням. На
       /ldt ДВІ сусідні стрічки (щілини + довжини) ще й зливались в одну — їх
       плутали (UX-аудит 03.08). Довгі ряди решіток/аксесуарів лишаються
       стрічкою: 16–21 чип у 6–7 рядів ховає решту форми (коментар вище). */
    .preset-chips:not(:has(.chip:nth-child(9))) {
        flex-wrap: wrap; overflow-x: visible; mask-image: none; scroll-snap-type: none;
    }

    /* Згасання на 28 px на світлому тлі майже не видно, а overlay-скролбари
       macOS не видно взагалі — про решту ряду можна було не здогадатись
       (аудит UI). Для КОРОТКОГО ряду підказку не рендерить сам ChipsHint
       (WrapMax): ховати її звідси через :has не можна — style-рушій частини
       збірок Chromium матчить :has не так, як querySelector (доведено виміром
       на /ldt). Поріг тут (nth-child(9)) і WrapMax=8 — одне число у двох
       синтаксисах; збіг тримає Chips_wrap_threshold_matches_css. */
    .chips-hint { display: block; }
}

/* Довгий типоряд у нативному селекті: на 390 px мітки з ціною обрізались
   праворуч («— 8,272.7…»), тобто саме число, заради якого список і читають. */
@media (max-width: 480px) {
    .field select[size] { font-size: var(--t-xs); }
}

/* Ціни немає через помилку вводу — картка пояснює, чому, замість порожнечі. */
.price-card-blocked { color: var(--c-text-soft); }
.chip {
    min-height: 34px; padding: 0.3rem 0.7rem;
    font: inherit; font-size: var(--t-sm); font-variant-numeric: tabular-nums;
    color: var(--c-text-soft); background: var(--c-surface);
    border: 1px solid var(--c-border-strong); border-radius: var(--r-pill);
    cursor: pointer; transition: all var(--dur) var(--ease);
}
.chip:hover { border-color: var(--c-brand); color: var(--c-brand); }
.chip-active { background: var(--c-brand); border-color: var(--c-brand); color: var(--c-on-brand); font-weight: 650; }
.chip-active:hover { background: var(--c-brand-hover); color: var(--c-on-brand); }

/* === Повідомлення ======================================================== */
.issue {
    padding: var(--s-2) var(--s-3); border-radius: var(--r-sm);
    font-size: var(--t-sm); margin: var(--s-2) 0; border-left: 3px solid transparent;
}
.issue-error { background: var(--err-50); color: var(--err-600); border-left-color: var(--err-600); }
.issue-ok { background: var(--ok-50, var(--c-surface-sunk)); color: var(--ok-600, var(--c-text)); border-left-color: var(--ok-600, var(--c-brand)); }
.issue-info { background: var(--info-50); color: var(--info-600); border-left-color: var(--info-600); }
.issue-warn { background: var(--warn-50); color: var(--warn-600); border-left-color: var(--warn-600); }

/* === Каркас конфігуратора ================================================
   Точка зламу тут 720 px, а НЕ 1000, як у шела, — і це не розсинхрон. Нижче 1000
   сайдбар їде за екран, тож контенту дістається вся ширина вікна: на планшеті
   768–1000 двох колонок вистачає із запасом. Поки злам був єдиний (1000), уся ця
   смуга діставала мобільну розкладку з прев'ю на 596 px заввишки — форма опинялась
   повністю під згином. */
/* minmax(0, 1fr), а не 1fr: у грид-елемента min-width за замовчуванням auto, тож
   нескорочувана горизонтальна стрічка чипів розпирала колонку і давала сторінці
   ~1990 px прокрутки на телефоні. */
.configurator { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-5); }
.config-form, .config-result { min-width: 0; }
/* Кегль тут НЕ задається: він живе на самому h1 (див. вище) — інакше половина
   заголовків продукту знову поїхала б власним розміром. */
.page-title { margin-bottom: var(--s-5); }
/* Заголовок, що ДОСЛІВНО повторює липкий топбар (див. PageHeading.razor):
   на телефоні напис ішов двічі поспіль — 8 адрес із 64 за обходом sitemap
   10.09. Ховаємо ВІЗУАЛЬНО, не з розмітки: H1 тримає структуру документа й
   видачу, зчитувач його читає, а в друку топбара немає взагалі (`no-print`),
   тож правило обмежене екраном і вузькою шириною. Ті самі декларації, що в
   `.visually-hidden`, — окремим правилом, бо клас на елементі УМОВНИЙ і
   рахується на сервері. */
@media screen and (max-width: 639px) {
    .page-title-echo {
        position: absolute; width: 1px; height: 1px; margin: -1px;
        padding: 0; border: 0; overflow: hidden; white-space: nowrap;
        clip: rect(0 0 0 0); clip-path: inset(50%);
    }
}

/* Абзац-відповідь серії — перше, що читає людина під заголовком, і перше, що
   бачить пошуковик. Заміряно на /grille?s=ART-2 при 1280 px, ОБИДВА стани того
   самого вузла: без правила рядок 984 px кольором основного тексту, з правилом
   668 px (70ch, як .norms-explain) кольором другорядного. Тобто без нього абзац
   читався як другий заголовок на всю колонку, а не як пояснення.

   margin-top тут НЕ задається навмисно: глобальне `p { margin: 0 0 var(--s-3) }`
   (вище в цьому файлі) уже дає нуль, і власна декларація була б мертвою —
   перша редакція її мала, і замір це спростував. Задається лише низ: s-3 замало,
   щоб відділити абзац від форми (12 px проти 20 px, і 20 збігається з відступом
   .page-title).

   Правило живе тут, а не в scoped-css сторінки, бо абзац стоїть на ЧОТИРЬОХ
   конфігураторах: чотири копії однієї декларації розійшлися б першою правкою. */
.series-lead {
    margin-bottom: var(--s-5);
    max-width: 70ch;
    color: var(--c-muted);
}
.config-result { display: flex; flex-direction: column; gap: var(--s-4); }

@media (min-width: 720px) {
    .configurator { grid-template-columns: minmax(300px, 1fr) minmax(320px, 1.05fr); gap: var(--s-6); align-items: start; }
    .config-result { position: sticky; top: var(--s-4); }
    /* «Таблиця даних» (доробка F5) — третім рядком сітки під ОБОМА колонками, як
       на макеті 2.png. Без grid-column вона стала б у ЛІВУ колонку під формою —
       тобто в ту саму вузьку смугу, з якої її виносили (486 px на 1280 проти
       потреби 920). */
    .configurator > .series-wide { grid-column: 1 / -1; }
    /* Липка колонка тримається в межах СІТКИ, а не свого рядка: з таблицею
       третім рядком вона прилипала на y=16 і НАКРИВАЛА таблицю (заміряно 24.09
       на 1280: прокрутка 600 — колонка 16…1651, верх таблиці 1488), ще й
       ховаючи власну картку ціни за нижнім краєм екрана. Доти липкість на
       вкладці «Порахувати» не діяла взагалі: колонка була найвищим блоком свого
       рядка, і прилипати їй не було куди. */
    .configurator:has(> .series-wide) > .config-result { position: static; }
}

@media (min-width: 1280px) {
    .configurator { grid-template-columns: minmax(340px, 1fr) minmax(360px, 1.05fr); gap: var(--s-8); }
}

@media (max-width: 719px) {
    /* «Розчиняємо» праву колонку, щоб її діти стали елементами тієї самої сітки:
       прев'ю йде НАД формою (спершу бачиш виріб), а ціна й аеродинаміка — ПІД нею.
       Доти обидві картки стояли над формою, і до полів доводилось прокручувати екран. */
    /* Селектори — від .config-result, а не від .configurator: display:contents прибирає
       бокс, але НЕ змінює DOM, тож «.configurator > .preview» не збігається ні з чим. */
    .config-result { display: contents; }
    .config-result > .preview { order: -1; }
    .config-result > .price-card { order: 1; }
    .config-result > .aero-card { order: 2; }
    /* Без явного order ADPI і поради лишались з order:0 і випливали МІЖ формою
       та ціною (аудит UI: на мобільному ціна опинялась четвертою). Порядок той
       самий, що в десктопній колонці: ціна → аеродинаміка → ADPI → поради. */
    .config-result > .adpi-card { order: 3; }
    .config-result > .advice-card { order: 4; }
    /* Та сама пастка втретє (рев'ю зрізу 34): kf-рядок і криві без order
       випливали МІЖ формою та ціною, відірвавшись від AeroCard, чию дальність
       вони коментують. Однаковий order — DOM-порядок усередині зберігається:
       аеродинаміка → kf-рядок → криві. */
    .config-result > .kf-note { order: 2; }
    .config-result > .aero-curves { order: 2; }
    /* Пастка вчетверте (жива персона на 375px, 07.08): .fan-spec-card «Дані
       виробника» без order випливала МІЖ формою та ціною — тобто каталожні
       характеристики вентилятора стояли перед ціною самого виробу. Тепер вона
       поруч з аеродинамікою, до якої змістовно й належить.
       ПРАВИЛО НА МАЙБУТНЄ: кожен новий прямий нащадок .config-result мусить
       дістати order ТУТ ЖЕ — інакше він мовчки стає першим. */
    .config-result > .fan-spec-card { order: 2; }
    /* Пленум-бокс PB (зріз S09): одразу ПІСЛЯ ціни — у КП він іде окремою
       позицією поруч із дифузором, тож габарит і Ø мусять стояти біля суми,
       яку вони пояснюють, а не під аеродинамікою. Однаковий order із ціною —
       DOM-порядок усередині тримає «ціна → пленум». */
    .config-result > .plenum-card { order: 1; }
    /* Вкладки сторінки виробу (ТЗ «решітки АРТ», зріз S03b). Порядок на
       телефоні: h1 і лід → РЯДОК ВКЛАДОК → зміст вкладки → форма → ціна.
       Доти рядок стояв після картки ціни, на глибині понад два екрани, тобто
       головна навігація сторінки за ТЗ була схована.
       • Рядок — прямий нащадок сітки, а не колонки, тож правило без префікса;
         order −2 ставить його над усім, включно з прев'ю (−1).
       • Панель, що стоїть ПЕРШОЮ в колонці (усі вкладки, крім «Вибраного
         продукту»), іде одразу під рядком — теж −1, а DOM-порядок тримає її
         перед прев'ю (на «Розмірі»/«Документі» прев'ю й так усередині панелі).
       • На «Вибраному продукті» перша в колонці — прев'ю, і панель (опис
         серії) лишається після ціни, як було.
       Відступ рядка знизу — s-4, а не проміжок сітки s-5: рядок і зміст, який
       він перемикає, — одна річ, і на 390 зміст мусить починатися не далі ніж
       за 16 px (заміряно e2e). */
    .config-result > .product-panel { order: 2; }
    .config-result > .product-panel:first-child { order: -1; }
    /* «Таблиця даних» — прямий нащадок сітки (доробка F5), тож правило без
       префікса колонки. Той самий order, що панель-перша-дитина: DOM ставить
       панель раніше, тобто порядок «панель → таблиця → форма (0) → ціна (1)» —
       таблиця відповідає на витрату, яку людина щойно ввела в панелі. */
    .configurator > .series-wide { order: -1; }
    .product-tabs { order: -2; margin-bottom: calc(var(--s-4) - var(--s-5)); }
}

/* === Вкладки сторінки виробу ==============================================
   Дві ширини — дві розкладки, підпис ОДИН і дослівно з ТЗ
   (ProductTabAddress.Label, рішення власника 13.10: «Вибраний продукт ·
   Порахувати · Розмір · Аксесуари · Додати специфікацію · Документ»):
   • ≤ 719 px — сітка 2 × 3: два стовпці, три ряди. Не три стовпці, як доти:
     у третину ширини найдовше СЛОВО («специфікацію») не вміщається — на
     320 px вкладка 93 px, а слово шрифтом CI (DejaVu Sans, на раннері Inter
     немає) 95 px, тобто текст вилазив за рамку вкладки. У половину ширини на
     390 (вкладка 175 px) «Додати специфікацію» стоїть одним рядком (Inter
     138 px, DejaVu 150 px), а на 320 (140 px) переноситься на два рядки й
     рамку не перетинає. Заміряно 13.10 обома шрифтами на 320…1280
     (docs/research/ko-grilles-2026-10-13/proofs/mutations.txt, Т7); сторож —
     product-tabs.spec, «телефон 390» (ширини 390/320/720).
   • ≥ 720 px — один рядок на всю ширину сітки конфігуратора. На 720–1024
     вкладка 112–142 px, і довгі підписи там теж ідуть у два рядки.
   Переносить підпис сама вузька колонка сітки — між словами, як будь-який
   текст. white-space: normal і hyphens: manual у правилі вкладки — це ЯВНО
   записані дефолти, запобіжник від успадкованих nowrap чи auto (тоді підпис
   вилазив би за рамку або різався дефісом посеред слова назви ТЗ), а не
   механізм переносу. Більше двох рядків підпис не займає на жодній заміряній
   ширині. Третьої розкладки («повні підписи з 860 px контейнера»)
   більше немає: вона давала вкладці два імені (зріз S03b).
   40 px заввишки — палець, а не курсор (WCAG 2.5.8 вимагає 24; рішення плану ТЗ
   — 40, як кнопки дій); двома рядками вкладка 49 px. */
.product-tabs {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px;
    padding: 2px; background: var(--c-surface-sunk);
    border: 1px solid var(--c-border); border-radius: var(--r-sm);
}
.product-tab {
    display: flex; align-items: center; justify-content: center; text-align: center;
    white-space: normal; hyphens: manual;
    min-height: 40px; padding: 0.25rem 0.5rem;
    border: 1px solid transparent; border-radius: 4px;
    color: var(--c-muted); font-size: var(--t-sm); text-decoration: none;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.product-tab:hover { color: var(--c-text); }
/* Активну вкладку видно РАМКОЮ кольору бренду, а не вагою шрифту (рішення
   задачі 8 релізу 1.3.0, рецепт .mode-option). Доти рамка була --c-border-strong
   на треку --c-surface-sunk — 1,53:1 у світлій темі й 1,78:1 у темній, нижче
   3:1 WCAG 1.4.11, — тобто єдиною ознакою не кольором лишалась вага 650. А вага
   робила ширину підпису залежною від стану: жирне «специфікацію» шрифтом CI
   (DejaVu) — 105,9 px при вмістовій рамці 94 px на 720, тож підпис заходив у
   відступ, а клік по вкладці міг переносити рядок. Рамка бренду — ≥ 3:1 до
   треку в обох темах (сторож — CssStateContrastTests), ширина підпису від стану
   не залежить (product-tabs.spec, «телефон 390», обома шрифтами). Фон
   --c-surface майже збігається з треком (1,03:1 у світлій темі, 1,04:1 у
   темній), тож розрізняє саме рамка. */
.product-tab-active {
    background: var(--c-surface); color: var(--c-text);
    border-color: var(--c-brand); box-shadow: var(--sh-xs);
}
@media (min-width: 720px) {
    /* Рядок, а не сітка: вкладок п'ять («Монтую») або шість, і стовпці мусять
       ділити ширину порівну за будь-якої кількості. */
    .product-tabs { grid-column: 1 / -1; grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
    /* Відступ збоку 0,375rem замість 0,5rem — ЛИШЕ в рядку (≥ 720), де вкладка
       найвужча: на 720 вона 112 px, і з відступом 0,5rem вмістова рамка 94 px
       не вміщала навіть звичайне «специфікацію» шрифтом CI (DejaVu Book
       94,9 px при 13 px) — підпис заходив у відступ на 0,45 px з кожного боку
       й тримався лише на допуску перевірки (рев'ю задачі 8, раунд 2). Тепер
       рамка 98 px, запас ≈ 1,5 px з кожного боку (Inter 86,4 px — ≈ 5,8);
       жирне 105,9 px не влізло б і тут — тому активна вкладка й без ваги.
       Телефонна сітка (≤ 719) свій відступ лишає: там вкладка ширша.
       Сторож — product-tabs.spec, «телефон 390»: запас ≥ 1 px з кожного боку. */
    .product-tab { padding-left: 0.375rem; padding-right: 0.375rem; }
}
.product-panel { display: flex; flex-direction: column; gap: var(--s-4); }
.panel-note { margin: 0; }
.series-more { white-space: nowrap; }
/* Опис серії з каталогу TEZ у вкладці «Вибраний продукт» (SeriesDescriptions). Ширина
   рядка — та сама, що в ліду (70ch): це проза, і на 1280 без межі рядок
   розтягувався б на всю колонку результату. Підзаголовок малий, як у картці
   аеродинаміки: у панелі він ділить текст, а не відкриває новий розділ сторінки. */
.series-about { max-width: 70ch; }
.series-about p { margin: 0 0 var(--s-3); }
.series-about-title { font-size: var(--t-md); margin: 0 0 var(--s-2); }
.series-features { margin: 0; padding-left: var(--s-5); }
.series-features li { margin-bottom: var(--s-1); }
.series-about .series-about-source { margin: 0; font-size: var(--t-sm); color: var(--c-muted); }
/* Таблиця розмірів вкладки «Розміри» (GrilleDimensionTable). Три колонки
   вміщаються й на 390 px: літери й числа вузькі, переноситься лише опис.
   Число не переноситься — «1054×1054» на два рядки читалось би як два розміри. */
.dim-table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.dim-table th, .dim-table td { text-align: left; padding: var(--s-2) var(--s-2) var(--s-2) 0; border-bottom: 1px solid var(--c-border); vertical-align: baseline; }
.dim-table thead th { color: var(--c-muted); font-weight: 600; }
.dim-table tbody th { white-space: nowrap; }
.dim-table .dim-value { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 650; text-align: right; padding-right: 0; }
/* Колонка значень — СТАЛОЇ ширини: під найширшу пару, яку серія друкує ціною
   («1054×1054» — вісім цифр і знак, 9ch). Доти ширину давав вміст, і набір
   розміру її міняв: сусідня колонка слів переносилась інакше, а таблиця на
   телефоні стоїть на вкладці «Розмір» НАД полями — вона сідала й росла на
   19 px, і поле під пальцем підстрибувало на «» і «5» та верталось на «50»
   (INSTALLER-9, замір аудиту «top полів 392 → 373 px»; статично на 390 px:
   таблиця 297 → 278 → 297, з 9ch — 317 на всіх кроках і на краю серії).
   Лише таблиця решітки (.dim-table-steady): пленум /ldt на тому самому
   .dim-table друкує одиночні числа, і 9ch звузили б йому колонку слів.
   Сторож пари «ширина ↔ найширша пара серії» — GrilleSizeTypingTests. */
.dim-table-steady td.dim-value { width: 9ch; }
/* Числа не переносяться (вище), тож на 200 % тексту таблиця «Розмір» вузького екрана
   ширша за колонку (хвиля 3, M-3c: +56 px сторінки на 390 з Inter, +48 з DejaVu —
   w3-200text-probe.txt). Гортається вона САМА, у
   своїй обгортці, як графік «Звук і тиск», — сторінка вбік не їде. На 100 % обгортка
   нічого не міняє: таблиця вміщається (заміряно INSTALLER-9: 317 px на 390). */
.dim-table-scroll { max-width: 100%; overflow-x: auto; }
.dim-table-scroll:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.dim-table thead th:last-child { text-align: right; padding-right: 0; }
.dim-note { font-size: var(--t-xs); color: var(--c-muted); }

/* «Таблиця даних» вкладки «Порахувати» (SeriesDataTable, зріз S07): розміри
   серії. З доробки F5 — широкий блок під сіткою конфігуратора (.series-wide),
   а не частина правої колонки, де на 1280 їй діставалось 486 px при потребі
   920. Рамка ще МОЖЕ гортатися вбік (запас на 640–719 px), і розмір прилипає
   ліворуч — гортаючи вбік, людина не губить, який це рядок. Вертикальна межа —
   завжди (задача 10): 105 рядків — десяток екранів, і вікна навколо обраного
   більше немає (перерахунок — на Enter, а не на клавішу). Шапка з сортуванням
   і фільтрами прилипає вгорі рамки. */
.series-wide { min-width: 0; }
/* Вказівник у панелі — кнопка, що виглядає посиланням (див. GrillePage: <a
   href="#…"> вів на головну). */
.series-jump-link {
    all: unset; cursor: pointer; color: var(--c-brand); text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--c-brand) 35%, transparent); text-underline-offset: 2px;
}
.series-jump-link:hover { color: var(--c-brand-hover); }
.series-jump-link:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.series-data { margin: 0; }
.series-data h2 { font-size: var(--t-md); margin: 0 0 var(--s-2); }
.series-scroll {
    overflow-x: auto; margin-top: var(--s-3);
    border: 1px solid var(--c-border); border-radius: var(--r-sm);
    max-height: 30rem; overflow-y: auto;
    /* Фокус на рядку не ховається під прилиплою шапкою рамки: доводячи кнопку
       розміру у видиму зону (Shift+Tab угору), браузер ставив її рівно на
       верхній край рамки, тобто ПІД шапку (WCAG 2.4.11; той самий механізм, що
       з топбаром і scroll-padding-top на html). Запас — найвища шапка з кнопкою й
       ⧩ на цій ширині з запасом (заміряно 14.10 обома шрифтами: ≥ 1280 — 51,4 px,
       720–1279 — 80,1, телефон — 74,0; сторож — a11y.spec на 390, 1024 і 1280). */
    scroll-padding-top: 3.75rem;
}
.series-scroll:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.series-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--t-sm); }
/* Вертикальний відступ 6 px, а не 8: рядок — це кнопка розміру (24 px, ціль
   дотику) плюс відступи, і з 8 px він мав 41 px — на піксель понад ціль 40 на
   1280 (доробка F5). */
.series-table th, .series-table td {
    padding: 0.375rem var(--s-2); border-bottom: 1px solid var(--c-border);
    text-align: left; vertical-align: top; white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.series-table thead th {
    position: sticky; top: 0; z-index: 2; padding: 0;
    background: var(--c-surface-sunk); color: var(--c-muted); font-size: var(--t-xs);
}
.series-table tbody th { position: sticky; left: 0; z-index: 1; background: var(--c-surface); }
.series-table thead th:first-child { left: 0; z-index: 3; }
/* Бейджі (доробка F5): ОДИН у рядку — біля розміру, за ΔP, шум і дальність
   (SeriesTableRow.PassportBadge), і по одному в шапці живого перетину й
   швидкості. Доти бейдж стояв під КОЖНИМ числом: п'ять «оцінка · поза
   таблицею» в рядку, рядок 57 px і таблиця 920 px (заміряно 24.09 на 1280).
   Сам бейдж на десктопі не ламається: перша редакція S07 ламала «оцінка · поза
   таблицею» надвоє, і рядок ріс до 86 px.
   Розмір і бейдж — рядок-обгортка з переносом: бейдж стоїть поруч із
   розміром, доки колонці вистачає місця, і переходить під нього цілим, коли
   рамці бракує ширини (при 4000 м³/год позначка «Зашвидко» розширює колонку
   швидкості, і на 768/1024 таблиця з бейджем у рядок мала 773 px у рамці
   734/726; заміряно 24.09). Під розміром на всіх ширинах бейдж робив рядок
   57 px на 1024/900/768 проти цілі 44.
   Префікс .series-table у бейджа — не прикраса: загальне .aero-badge
   (margin-left) стоїть у файлі НИЖЧЕ з тією самою вагою селектора. */
.series-size { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--s-1); row-gap: 1px; }
.series-table .series-row-badge { margin: 0; white-space: nowrap; }
/* На 1280+ розмір і бейдж — в ОДНОМУ рядку завжди: з одиницями в шапках (S1
   раунду 1 задачі 10) ширші підписи забирали в колонки B зайву ширину, і в
   DejaVu бейдж «оцінка · екстраполяція» падав під кнопку — сім рядків по 56,5 px
   при цілі 40 (заміряно 14.10, ART-1 f=300). Нижче 1280 перенос лишається: там
   він і є запасом ширини (доробка F5). */
@media (min-width: 1280px) {
    .series-size { flex-wrap: nowrap; }
}
/* Підрядок площі · швидкості · vL · дальності — лише телефон (≤ 639 px): ширше ці
   величини мають власні колонки, і зчитувач не мусить чути їх двічі. */
.series-sub { display: none; }
/* Щоб колонка розміру з бейджем у рядок не розпирала рамку (775 px у 726 на
   1024, заміряно 24.09), до 1279 px кегль таблиці — --t-xs, а бічний відступ
   клітинок — --s-1; на 1280 місця досить і з --t-sm. */
@media (min-width: 720px) and (max-width: 1279px) {
    .series-table { font-size: var(--t-xs); }
    .series-table th, .series-table td { padding-left: var(--s-1); padding-right: var(--s-1); }
    .series-table thead th { padding: 0; }
    .series-table .select-sort { padding-left: var(--s-1); padding-right: var(--s-1); }
    /* ⧩ фільтра — під підписом і тут (задача 10): у рядок із підписом він
       розширював кожну шапку на ~20 px. */
    .series-th { flex-wrap: wrap; }
    .series-th > .select-sort { flex-basis: 100%; }
    .series-filter > summary { padding: 0 var(--s-1) var(--s-1); }
    /* Шапка тут найвища (підпис з одиницею у два-три рядки й ⧩ під ним — 80 px),
       і запас прокрутки під неї — свій (I2 раунду 1: на 1024 рядки ставали під
       шапку при запасі 48 px). */
    .series-scroll { scroll-padding-top: 6rem; }
}
/* 640–719 px: рамка гортається вбік (B прилипає ліворуч), і ширину колонок
   таблиця розподіляє сама. На ART-1F, чиї бейджі короткі, колонка B стискалась до
   67–88 px, і рядок власного розміру ставав у три рядки («405» / «ваш розмір» /
   бейдж) — 70,7 px при цілі 64 (R5 раунду 2 задачі 10, обома шрифтами). Мінімум
   7,5rem тримає «405 ваш розмір» в одному рядку — 56,5 px; рамка гортається на
   32–41 px більше. ART-1/ART-2 мають B 156/177 px і правила не помічають. */
@media screen and (min-width: 640px) and (max-width: 719px) {
    .series-table tbody th { min-width: 7.5rem; }
}
/* З 720 px першими стискаються ШАПКИ (підпис з одиницею — у два-три рядки), і
   лише потім бейдж рядка переходить під розмір. Заміряно 14.10 (задача 10,
   раунд 1: одиниці в шапках, клітинки — саме число) обома шрифтами на 768 і
   1024, ART-1 f=300/4000 і ART-2 405×200 f=900: рамка 734 / 726 px без бічного
   гортання; рядки, чий бейдж не влізає поруч із розміром, — 56,5 px. Доти, з
   одиницями в кожній клітинці, таблиця в DejaVu виходила за рамку до 65 px. */
@media (min-width: 720px) {
    .series-table thead th, .series-table .select-sort { white-space: normal; }
}
.series-num { font-weight: 600; }
.series-mark { color: var(--c-muted); margin-left: 1px; }
.series-flag {
    display: inline-block; margin-left: var(--s-1); padding: 0 var(--s-1); border-radius: var(--r-sm);
    background: var(--warn-50); color: var(--warn-600); font-size: var(--t-2xs); font-weight: 650;
}
.series-custom { color: var(--c-muted); font-size: var(--t-2xs); line-height: 1.2; }
.series-pick {
    all: unset; cursor: pointer; font-weight: 650; color: var(--c-brand);
    min-height: 1.5rem; display: inline-block;
}
.series-pick:hover { text-decoration: underline; }
.series-pick:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
/* Обраний рядок: тло — і в прилиплій клітинці розміру теж, інакше при
   гортанні вбік виділення лишалось би лише на половині рядка. */
.series-table tr[aria-current] > * { background: var(--brand-50); }
.series-table tr[aria-current] .series-pick { color: var(--c-text); }
.series-legend { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); }
/* Шапка колонки (задача 10): кнопка сортування й ⧩ фільтра поруч. Кнопка бере
   всю решту ширини — ціль кліку лишається цілою шапкою, як до фільтрів. */
.series-th { display: flex; align-items: flex-start; }
.series-th > .select-sort { flex: 1 1 auto; width: auto; min-width: 0; }
/* Фільтр «Обмежити: від–до» (як у Klimaoprema). Поля — ПОВЕРХ рядків під
   шапкою, а не в ній: відкритий фільтр усередині клітинки розсунув би колонку
   й зсунув усю таблицю під курсором. Прилипла шапка — позиціонований предок,
   тож поле стоїть під своєю колонкою; перші дві (B, H) — від лівого краю,
   решта — від правого, щоб поля не виходили за рамку. */
.series-filter { flex: none; }
/* ⧩ — ціль дотику не менша за 24×24 (WCAG 2.5.8; axe target-size на /grille
   «Порахувати» — a11y.spec), хоч сам знак вужчий. */
.series-filter > summary {
    list-style: none; cursor: pointer; padding: 0.45rem 0.35rem; color: var(--c-muted);
    font-weight: 400; line-height: 1.2; min-width: 1.5rem; min-height: 1.5rem; box-sizing: border-box;
    display: flex; align-items: center; justify-content: center;
}
.series-filter > summary::-webkit-details-marker { display: none; }
.series-filter > summary:hover, .series-filter-on > summary { color: var(--c-brand); }
.series-filter > summary:focus-visible { outline: 2px solid var(--c-focus); outline-offset: -2px; }
/* Вікно «від–до» — стовпчиком (поля одне під одним): у рядок воно мало ~300 px і
   на 390 та 1280 вилазило за рамку таблиці (M7 раунду 1 задачі 10; мутація «знову
   в рядок» червонить обидві ширини). Стовпчиком воно 150 px; куди його
   прив'язувати — правило нижче. Сторож рамки — popoversOutsideFrame у
   grille-table.spec, на ART-1/ART-2 і на ART-1F (серія з найвужчою колонкою B). */
.series-filter-body {
    position: absolute; top: 100%; right: 0; z-index: 4;
    display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-1); padding: var(--s-2);
    background: var(--c-surface); color: var(--c-text); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
    font-weight: 400; white-space: nowrap;
}
/* Перші дві колонки (B і H) — від ЛІВОГО краю клітинки шапки: прив'язане праворуч
   вікно (150 px) у вужчій за нього колонці стирчить ліворуч за рамку. На ART-1F
   (короткі бейджі) B має 87–137 px на 768…1440 (на 640–719 — 120: мінімум у правилі
   смуги 640–719 вище), H — 41–53: вікно B вилазило на 13–61 px (768…1440; 61–82 на
   640–719 до того мінімуму),
   H — на 8–30 (640…1024), обома шрифтами (R1 раунду 2 задачі 10, заміряно 14.10).
   На ART-1/ART-2 колонка B не вужча за 156 px, і там це правило
   нічого не міняє — тому раунд 1, що міряв лише їх, помилково зняв його як мертве.
   Решта колонок — праворуч: прив'язане ліворуч вікно шуму чи дальності вилазило б
   за ПРАВИЙ край (на 390 — на 73 px). */
.series-table thead th:nth-child(-n+2) .series-filter-body { left: 0; right: auto; }
.series-filter-body label { display: inline-flex; align-items: center; gap: var(--s-1); }
.series-filter-body input { width: 5.5rem; font: inherit; padding: 0.125rem var(--s-1); }
/* Що сховано фільтром — рядок під таблицею; порожній (фільтрів немає) — без
   висоти, але в дереві: live-регіон мусить існувати ДО тексту. */
.series-filtered { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-muted); }
.series-filtered:empty { margin: 0; }
.series-inverted { color: var(--warn-600); font-weight: 600; }
/* Стиль «цензура» (спек §6.3, аналог червоного Klimaoprema): поріг «<N» — не
   вимір, і читатись як число в одному ряду з виміряними він не сміє. Курсив і
   приглушений колір — у клітинці й у зразку легенди однаково (легенда показує
   стиль, а не описує його). */
.series-table td.series-censored .series-num, .series-legend .series-censored {
    font-style: italic; font-weight: 600; color: var(--c-muted);
}
/* ◆ — рядок-модель номограми каталогу (легенда під таблицею). */
.series-node { margin-left: 2px; color: var(--c-brand); font-size: var(--t-2xs); }
/* Порожній бейдж, позначка чи знак примітки — НЕ «немає вузла», а вузол без
   тексту: структура рядка стала за будь-якої витрати, і клавіша витрати платить
   лише за зміну тексту (SeriesTableRow.Cell). Ховаємо їх тут, а не в розмітці. */
.series-table .aero-badge:empty, .series-flag:empty, .series-mark:empty, .series-node:empty { display: none; }

/* Телефон (≤ 639 px, доробка F5) — СПРАВЖНЯ таблиця на п'ять колонок: B, H
   (окремо з задачі 10), ΔP, шум, ціна. Живий перетин, швидкість, vL і дальність
   ховаються колонками й друкуються підрядком у клітинці B (.series-sub — ті
   самі рядки клітинок).
   Доти рядок був КАРТКОЮ з підписом над кожним числом і бейджем біля кожного:
   202 px на рядок, 2956 px на вікно з 15 рядків, вкладка 6504 px (заміряно
   24.09 на 390) — таблицю читали гортанням, а не поглядом, і порівняти два
   сусідні розміри в одному екрані було неможливо. Кнопки сортування й фільтри
   видимих колонок лишаються в шапці (sticky, як на десктопі); сховані колонки
   сортуються й обмежуються на ширшому екрані.
   Бічного гортання немає: кегль --t-xs і відступ --s-1 — заміряні, щоб колонки
   вмістились у 356 px рамки на 390. */
@media screen and (max-width: 639px) {
    /* Шапка телефона вища: ⧩ під підписом. */
    .series-scroll { max-height: 70vh; scroll-padding-top: 5.25rem; }
    .series-table { font-size: var(--t-xs); }
    /* 3 px по вертикалі, а не 4: найвищий рядок (бейдж і підрядок — обидва в два
       рядки) мав 63,8 px, тобто ціль 64 вирішувалась би десятими пікселя. */
    .series-table th, .series-table td { padding: 0.1875rem var(--s-1); }
    .series-table thead th { padding: 0; }
    .series-table .select-sort { padding: var(--s-2) var(--s-1); }
    /* Задача 10: B і H — окремі колонки, а підрядок подовшав на vL (природна
       ширина до 303 px в Inter і 327 у DejaVu), і ціль ≤ 64 px тримається лише
       тоді, коли підрядок уміщається у ДВА рядки клітинки B. Перший замір дав
       146 px на B і рядки по 75 px (25 із 105 на ART-2 f=900 в Inter, до 82 у
       DejaVu); кожна поступка нижче — заміряна 14.10 обома шрифтами на ART-1 і
       ART-2 при 60…6000 м³/год (≤ 57,4 px в Inter, ≤ 61,8 у DejaVu):
       • ⧩ фільтра — ПІД підписом: у рядок підпис і ⧩ робили шапки H, ΔP і шуму
         ширшими за їхні клітинки (39 / 47 / 63 px);
       • вузькі колонки (H, Δp, шум, ціна) — кегль --t-2xs і відступ 2 px: числа в
         них короткі, а кожен піксель іде клітинці B;
       • позначка швидкості в підрядку — рядкова, а не блок-рядкова: «понад 4 м/с»
         неподільним блоком лишала самотнє «м» дальності третім рядком.
       Знаки приміток у підрядку — символами ¹²³ (SeriesTableRow.Subline), тож
       sup, що розсував рядок (33,7 px на два рядки замість 26,4), там більше немає. */
    .series-th { flex-wrap: wrap; }
    .series-th > .select-sort { flex-basis: 100%; }
    .series-filter > summary { padding: 0 var(--s-1) var(--s-1); }
    .series-table td { padding-left: 2px; padding-right: 2px; font-size: var(--t-2xs); }
    .series-table .select-sort { padding-left: 2px; padding-right: 2px; }
    .series-sub .series-flag {
        display: inline; margin-left: 0; padding: 0 1px;
        -webkit-box-decoration-break: clone; box-decoration-break: clone;
    }
    /* Підписи шапки — тим самим кеглем, що вузькі клітинки: з одиницями (S1) саме
       шапка шуму («дБ(А)» неподільне) задавала ширину колонки, і клітинці B
       бракувало двох пікселів на два рядки підрядка в DejaVu. */
    .series-table thead .select-sort { font-size: var(--t-2xs); }
    .series-table [data-col="area"], .series-table [data-col="velocity"], .series-table [data-col="vl"],
    .series-table [data-col="throw"] { display: none; }
    /* Переносять УСІ клітинки, а не лише розмір: з одиницями в рядок («419.00
       грн», «40 дБ(А)», «Ціна за шт» у шапці) чотири колонки лишали розміру
       144 px, і бейдж та підрядок ламались надвоє — рядок 93 px (заміряно).
       Перенесена одиниця під числом коштує висоти лише вузької клітинки,
       нижчої за клітинку розміру. */
    .series-table th, .series-table td, .series-table .select-sort { white-space: normal; }
    /* Уся решта ширини — колонці розміру: без цього таблиця ділила ширину за
       max-content, і розміру діставалось 159 px — на 2 px менше за бейдж
       тодішнього «оцінка · поза таблицею», який ламався надвоє (рядок 79–93 px);
       нинішні «оцінка · інтерполяція/екстраполяція» — тієї самої довжини (R3-aero-2). */
    .series-table tbody th { width: 100%; }
    .series-sub { display: block; color: var(--c-muted); font-size: var(--t-2xs); line-height: 1.2; }
    /* Бейдж — у рядку з розміром і переноситься ВСЕРЕДИНІ себе, а не падає
       під кнопку: під кнопкою рядок із позначкою швидкості мав 76–77 px
       (кнопка 24 + бейдж 16 + підрядок, що з «Зашвидко» ламається надвоє, 26;
       заміряно на ART-2 при 900 м³/год — 63 рядки з 105 понад 64). Бейдж у два
       рядки поруч із кнопкою — 28 px, тобто не вищий за кнопку більш ніж на 4. */
    .series-size { flex-wrap: nowrap; align-items: flex-start; }
    .series-table .series-row-badge { min-width: 0; white-space: normal; font-size: var(--t-2xs); line-height: 1.2; }
}

/* Вкладка «Додати специфікацію» (макет 4.1.2). Кнопка — власного розміру, а не на всю
   колонку: у стовпчику flex вона розтягувалась би на 600+ px і читалась як
   банер, а не дія. Перелік вивантажень — ті самі межі прози, що в описі серії. */
.spec-panel { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); max-width: 70ch; }
.spec-count { margin: 0; }
.spec-exports { margin: 0; padding-left: var(--s-5); }
.spec-exports li { margin-bottom: var(--s-1); }

/* === Картки ============================================================== */
.preview, .price-card, .aero-card, .variant-card, .quote-table, .saved-table,
.aero-curves, .spc-card, .fan-spec-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
}
.preview { padding: var(--s-4); }
/* Тільки ПРЯМИЙ нащадок. Нащадковий «.preview svg» діставав і ВКЛАДЕНІ <svg>
   видів DwgView, а CSS-ширина у SVG2 перекриває геометричний атрибут width:
   кожен вид ставав на всю ширину кадру (560 одиниць), центрувався вже в ній —
   і другий вид («Переріз») з'їжджав за viewBox: у решітки/дифузора/анемостата
   лишався обрізок заголовка, у LDT переріз зникав без сліду. Залежить від
   збірки браузера (Chromium пісочниці v1194 малює зсув, новіший у CI — ні),
   тож зелений CI цього не бачив; ловиться drawing-labels.spec.ts на браузері
   пісочниці («вилазить за кадр на 134px»). */
.preview > svg { width: 100%; height: auto; display: block; }

/* Підписи креслення. Правила навмисно на .drawing, а не на .preview: мініатюри
   схем у КП (.quote-thumb) лежать ПОЗА .preview, тож із «.preview .dim» вони
   лишались зовсім без стилю — розмірні написи падали в дефолт браузера
   (чорні 16 px) і в самій пропозиції, і в друку. */
/* --dwg-k — коефіцієнт кегля підписів. Тут оголошена ОДИНИЦЯ (зовнішній кадр
   не масштабує сам себе), а кожен DwgView перевизначає її своїм 1/масштаб —
   інакше той самий підпис рендерився б різного розміру в різних видах
   (заміряно 25,0 % розкиду). Оголошення живе в CSS, а не лише в розмітці:
   сторож CssTokenTests вимагає, щоб кожна вжита змінна була оголошена, і саме
   він це й упіймав. */
.drawing { width: 100%; height: auto; display: block; --dwg-k: 1; }

/* КРЕСЛЕННЯ НА ТЕЛЕФОНІ НЕ СТИСКАЄТЬСЯ НИЖЧЕ МЕЖІ ЧИТАБЕЛЬНОСТІ.
   Заміряно 15.09 на ВСІХ п'яти прев'ю: на 1280 підпис рендериться 9,72 px
   (svg 453,6 px), на 390 — 6,94 px (svg 324), на 360 — 6,30. Кегль живе в
   координатах виду, тобто рендерний розмір ПРЯМО пропорційний ширині svg
   (9,72 / 453,6 = 0,0214 px на піксель ширини), і з вужчим екраном падає разом
   із ним. Підняти сам кегль не можна: текст роздувся б ВІДНОСНО геометрії й
   поповз за кадр — це вже коштувало мініатюрам каталогу (див. catalog.spec,
   борг UI/UX-проходу 18.08). Тому росте не текст, а мінімальна ширина самого
   креслення, а надлишок їде вбік — рівно той виняток, який CLAUDE.md лишає
   діаграмам («кожна у своєму overflow-x: auto контейнері»).
   454 px — не кругле число, а ПАРИТЕТ із десктопом: саме на цій ширині підпис
   дорівнює тим самим 9,72 px. Менше означало б «на телефоні гірше, ніж на
   екрані», а саме це й лагодиться. */
.drawing-scroll { overflow-x: auto; scrollbar-width: thin; }
/* БЕЗ медіазапиту — і це виправлення першої редакції. Вона ставила мінімум лише
   на ≤639 px, тобто сама стояла нерухомо в тому самому вимірі, який лагодила:
   на 768/1024/1100 px вмикається ДВОКОЛОНКОВА сітка, колонка прев'ю вужча за
   десктопну (326–398 px проти 453,6), і підпис падав до 7,00–8,53 px — тобто
   ноутбук 1024 малював креслення ДРІБНІШЕ за телефон 390 після першої правки.
   450 px, а не 454: на 1280 колонка має рівно 453,6 px, і 454 давали б смугу
   прокрутки на десктопі там, де прокручувати нічого. */
.preview .drawing-scroll .drawing { min-width: 450px; }
/* Маски-згасання праворуч тут НЕМАЄ навмисно, хоч сусідня .catalog-nav її має:
   там згасають чіпи, а тут згасав би САМ КРЕСЛЕНИЙ ТЕКСТ — тобто підписи, заради
   читабельності яких усе це й зроблено. Ознака прокрутки — обрізаний край
   креслення (видно початок сусіднього виду) і тонка смуга прокрутки. */

/* --- Ролі ліній креслення -------------------------------------------------
   Доти товщина задавалась числом у кожному <line>, тож контур, ламель, вісь і
   розмірна лінія виглядали однаково важливими — око не мало за що зачепитись.
   non-scaling-stroke тримає товщину сталою: кожен вид — вкладений <svg> зі
   ВЛАСНИМ масштабом, інакше лінії в різних видах були б різної ваги. */
.drawing .dwg-contour, .drawing .dwg-thin, .drawing .dwg-hidden, .drawing .dwg-axis,
.drawing .dwg-dim, .drawing .dwg-ext, .drawing .dwg-air { vector-effect: non-scaling-stroke; }

.drawing .dwg-contour { stroke: var(--dwg-ink); stroke-width: 1.7; fill: none; }
.drawing .dwg-thin { stroke: var(--dwg-thin); stroke-width: 1; fill: none; }
.drawing .dwg-hidden { stroke: var(--dwg-thin); stroke-width: 1; fill: none; stroke-dasharray: 5 3; }
/* ДРУГИЙ РЯД ламелей (АРТ-2) у перерізі — смуга глибини, яку він займає.
   Штрихова, бо це не профіль у січній площині, а габарит ряду за нею; суцільна
   тонка читалась як ще один корпус. У ФРОНТАЛЬНОМУ виді ряд 2 не малюється
   взагалі — три редакції (суцільна, штрихова, приглушена) давали на кроці
   20 мм дрібну сітку, у якій не читався ЖОДЕН ряд (відкинуто за скриншотом). */
.drawing .dwg-rear { stroke: var(--dwg-thin); stroke-width: 1; fill: none; opacity: .55; stroke-dasharray: 4 2; }
.drawing .dwg-axis { stroke: var(--dwg-mark); stroke-width: .8; fill: none; stroke-dasharray: 9 3 2 3; }
.drawing .dwg-dim, .drawing .dwg-ext { stroke: var(--dwg-line); stroke-width: .8; fill: none; }
.drawing .dwg-arrow { fill: var(--dwg-line); stroke: none; }
.drawing .dwg-body { fill: var(--dwg-fill); stroke: none; }
.drawing .dwg-void { fill: var(--dwg-void); stroke: none; }
/* Матеріал у розрізі — суцільною заливкою: штрихування на товщині 3 одиниці
   перетворюється на кашу. */
.drawing .dwg-cut { fill: var(--dwg-ink); stroke: none; }
.drawing .dwg-air { stroke: var(--dwg-air); fill: none; }
.drawing .dwg-air-head { fill: var(--dwg-air); stroke: none; }

/* Підписи. Обведення кольором тла прибирає накладання тексту на лінії.

   Кегль і колір були підібрані «щоб не заважало», і схема вийшла найдрібнішим
   і найблідішим текстом на сторінці: ~10 px проти 13 px у підписів полів поруч,
   ще й кольором розмірної ЛІНІЇ (--dwg-line), а не тексту. Читати креслення
   було важче, ніж будь-що інше в тій самій картці. Тепер кегль на рівні решти
   інтерфейсу, а колір — чорнильний: розмірний напис це текст, а не тонка лінія
   побудови. Розмір тримає e2e drawing-labels: підписи не сміють злипнутися. */
/* Кегль — через коефіцієнт виду (--dwg-k, ставить DwgView): текст живе в
   координатах виду й масштабується разом із ним, а масштаби видів різні.
   Заміряно до правки: дев'ять видів п'яти прев'ю дали від 7,99 до 9,99 px,
   розкид 25,0 %. Halo тим самим коефіцієнтом — інакше обведення підпису
   гуляло б услід за кеглем. Дефолт 1 залишає розмір як був там, де
   змінної немає (мініатюра КП поза DwgView, друк).

   Базові 7,5 стали 12 НЕ «щоб більше»: 7,5 були в координатах ВИДУ, а тепер
   число живе в координатах зовнішнього кадру, де масштаб інший (заміряно 0,81
   на 1280 px). Ціль — верх колишнього діапазону: підписи рендерились від 7,99
   до 9,99 px, і найдрібніші стояли саме там, де креслення найгустіше. 12 × 0,81
   дає ≈9,7 px усім видам; e2e drawing-labels стереже, що підписи від цього не
   злиплись. */
.drawing .dwg-label, .drawing .dwg-title {
    font-size: calc(12px * var(--dwg-k, 1)); fill: var(--dwg-ink);
    paint-order: stroke; stroke: var(--dwg-void);
    stroke-width: calc(4px * var(--dwg-k, 1)); stroke-linejoin: round;
}
.drawing .dwg-title { font-weight: 700; fill: var(--dwg-mark); }
/* Позначки перерізу й кута — акцентом, але тим самим кеглем, що й решта
   підписів. Старий клас .section-mark задавав 14 px у координатах виду, і в
   нормалізованому виді літери «А» виходили більшими за сам виріб; його правило
   (разом із .section-title і .angle-text) прожило в файлі ще довго ПІСЛЯ того,
   як розмітка перейшла на .dwg-mark — мертвий стиль читається як чинний, тож
   прибрано (замір 20.08). */
.drawing .dwg-mark { font-weight: 700; fill: var(--dwg-mark); }
.drawing .dwg-label.air-text { fill: var(--dwg-air); }
/* Одиниця вимірювання — ОДИН раз на креслення, у сталому куті зовнішнього svg.
   Кегль тут в одиницях ЗОВНІШНЬОГО кадру (560×250), а не виду, тож він єдиний
   на всіх п'яти прев'ю незалежно від масштабу самих видів. */
.drawing .dwg-units { font-size: 10px; fill: var(--c-muted); }
/* Висота ВЕЛИКОГО полотна прев'ю — одна на фото й на заглушку. Оголошена тут, а
   не в design-tokens.css, бо це не токен дизайн-системи, а розмір конкретного
   місця; але саме ОДНА, бо два числа розійшлися б із першою ж правкою, і тоді
   стрибок висоти між моделлю зі знімком і моделлю без нього повернувся б. */
:root { --preview-large-h: 270px; }

/* Родина без габаритів у прайсі (вентилятори, кріплення, пластикові канали):
   замість «узагальненого виробу», якого не існує, — чесна заглушка. Прямокутник
   там, де геометрії немає, гірший за порожнє місце: він читається як креслення. */
.drawing-none {
  display: flex; flex-direction: column; align-items: center; gap: var(--s-3);
  padding: var(--s-6) var(--s-5); text-align: center; color: var(--c-muted);
}
/* Явний розмір лишається потрібним: «.preview > svg» іконку вже не чіпає
   (вона не прямий нащадок), але без width іконка впала б у власний розмір
   спрайта — і колись уже розтягувалась на всю картку. */
.drawing-none .drawing-none-box { display: flex; align-items: center; justify-content: center; }
.drawing-none .drawing-none-ico { width: 56px; height: 56px; opacity: .5; }
.drawing-none p { margin: 0; font-size: var(--t-sm); max-width: 34ch; }
/* Велика заглушка на сторінці виробу — ТА САМА висота полотна, що у великого
   фото (--preview-large-h): інакше перемикання моделі зі знімком на модель без
   знімка сувало ціну й головну кнопку на чверть екрана. Висота задана ОДНИМ
   токеном на обидва випадки — два числа тут розійшлися б із першою ж правкою. */
.drawing-none-large { padding: 0; }
.drawing-none-large .drawing-none-box { min-height: var(--preview-large-h); }

/* Фото позиції з прайс-книги постачальника — образ виробу там, де креслення
   чесно немає. Біла підкладка НАВМИСНА і в темній темі: фото постачальника
   зняті на білому, і прозорий PNG на темному тлі перетворюється на силует. */
/* ОДНАКОВЕ полотно для знімка будь-якого виробу.
   Доти висота залежала від самого файла: max-height 220 означало, що знімок
   1000×1000 займає 220 px, а 100×100 — сто, і поруч у видачі стояли картка на
   пів-екрана й марка в кутку (скарга власника «зображення однотипні, якісні
   для всіх виробів»). Полотно фіксоване рядком ґрідa, знімок центрується.
   Збільшення НЕ робимо: max-* ніколи не розтягує — домальовувати пікселі
   означало б видавати вигадку за фото постачальника. */
.drawing-photo {
  display: grid; grid-template-rows: 240px auto; justify-items: center; align-items: center;
  gap: var(--s-2); margin: 0; padding: var(--s-3) var(--s-3) 0;
}
.drawing-photo img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  background: #fff; border-radius: 10px; padding: var(--s-2);
  border: 1px solid var(--c-border);
}
.drawing-photo figcaption { font-size: var(--t-xs); color: var(--c-muted); text-align: center; }
/* У мініатюрах каталогу/КП підпис зайвий — образ і так підписаний карткою. */
.catalog-thumb .drawing-photo figcaption, .quote-thumb .drawing-photo figcaption { display: none; }
.catalog-thumb .drawing-photo { padding: 0; grid-template-rows: 80px; }
.catalog-thumb .drawing-photo img { padding: 2px; }
.drawing .air-text { fill: var(--dwg-air); }
.drawing-spec { margin: var(--s-3) 0 0; font-size: var(--t-xs); color: var(--c-muted); }
.drawing-spec strong { color: var(--c-text-soft); }

.price-card { padding: var(--s-5); }
/* Пульс при ЗМІНІ суми (вузол перестворюється через @key у PriceCard):
   видимий доказ, що перерахунок живий, — замість кнопки «Розрахувати»,
   якої просив зовнішній аудит і яка суперечила б принципу №1. */
@keyframes price-tick {
    from { background: color-mix(in srgb, var(--c-brand) 16%, transparent); }
    to { background: transparent; }
}
.price-total, .price-sticky-total {
    animation: price-tick 0.6s var(--ease);
    border-radius: var(--r-sm);
}
@media (prefers-reduced-motion: reduce) {
    .price-total, .price-sticky-total { animation: none; }
}
.price-card.quote { background: var(--warn-50); border-color: color-mix(in srgb, var(--warn-600) 30%, transparent); color: var(--warn-600); }
.price-total { font-size: var(--t-price); font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; }
/* Ціна попереднього розміру, поки новий ще набирають (INSTALLER-9): число
   лишається на місці — вміст не стрибає, — але видимо «не поточне». */
.price-total-pending { color: var(--c-muted); }
.price-pending { margin: var(--s-1) 0 0; font-size: var(--t-sm); font-weight: 600; color: var(--warn-600); }
.price-sticky-note { font-size: var(--t-xs); font-weight: 600; color: var(--warn-600); }
.price-unit { color: var(--c-muted); font-size: var(--t-sm); margin-bottom: var(--s-3); }
/* Діапазон цін родини під розбивкою: якір «скільки це коштує взагалі» ще до
   перебору розмірів. Тон приглушений — головне число лишається головним. */
.price-range { color: var(--c-muted); font-size: var(--t-sm); margin: var(--s-2) 0 0; }
.price-range strong { color: var(--c-text); font-weight: 600; }
.price-range small { display: block; font-size: var(--t-xs); }
.price-lines { list-style: none; padding: var(--s-3) 0 0; margin: var(--s-3) 0 0; border-top: 1px solid var(--c-border); }

/* Абзац-відповідь під заголовком родини і видимий ряд виконань. */
.family-lead { margin: 0 0 var(--s-3); color: var(--c-text-soft); max-width: 70ch; }
.family-sizes { margin-top: var(--s-5); }
.family-sizes summary { cursor: pointer; color: var(--c-muted); font-size: var(--t-sm); }
.family-size-table td:nth-child(2) { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Тут стояв клас .money (nowrap на самій сумі) — ПРИБРАНО як мертва межа.
   Він з'явився проти розриву «203.00 грн» на два рядки, але той розрив лікує
   картка нижче: заміряно 10.09 на 320 і 390 px по трьох найгірших родинах —
   із класом 0 розривів, БЕЗ класу теж 0. Межа, якої мутація не червонить, у
   цьому репозиторії не лишається неперевіреною (CLAUDE.md). Вище 639 px суму
   тримає nowrap самої колонки. */
/* На телефоні ця колонка НЕ забороняє перенос. `nowrap` тут стоїть заради
   ЦІНИ («1 762.00 грн» не має ламатись), але та сама клітинка друкує ще й
   «на прорахунку у виробника» — 206 px одним рядком, і таблиця розганялась до
   434 px у контейнері 358: колонка «Код замовлення» їхала за екран НА ВСІХ
   23 рядках (заміряно 10.09), а код замовлення — це те, чим виріб замовляють.
   З переносом таблиця стає 336 px і вміщається цілком.

   Друга половина цієї правки коштувала окремого заміру, бо перша редакція
   коментаря тут БРЕХАЛА: «ціна не постраждає, у неї нерозривний пробіл» — а в
   рядку стояв звичайний, і після дозволу переносу «203.00 грн» ламалось на два
   рядки в 131 рядку зі 131 (ARX-GRILL) і 149 зі 149 (FAN-DUCT). Тримає її
   тепер клас .money на самій сумі, а не припущення про пробіл. */
@media screen and (max-width: 639px) {
    .family-size-table td:nth-child(2) { white-space: normal; }
    /* Рядок ряду виконань на телефоні — КАРТКА, як у КП і в кабінеті. Дозвіл
       переносу вище зняв обрізання коду, але не ширину: заміряно 10.09 після
       нього — MOUNT 440 px у контейнері 358, FAN-REG 439, FAN-DUCT 416, тобто
       до 82 px за екраном, і дістатись туди можна лише здогадавшись гортати
       вбік (афордансу немає: смуга 0 px, документ горизонтально не скролиться).
       А третя колонка — це КОД, яким виріб замовляють. Карткою всі 20 родин
       вкладаються в 358 без жодного бічного скролу. */
    .family-size-table, .family-size-table tbody, .family-size-table tr { display: block; }
    .family-size-table thead { display: none; }
    .family-size-table tr {
        display: flex; flex-wrap: wrap; align-items: baseline;
        column-gap: var(--s-3); row-gap: var(--s-1);
        padding: var(--s-3) 0; border-bottom: 1px solid var(--c-border);
    }
    /* Селектор із ОБОМА класами — не для краси: таблиця носить ще й
       `.quote-table`, а її правила (рамка й падінг клітинки) стоять НИЖЧЕ в
       цьому файлі й при однаковій вазі перемагали. Заміряно 10.09: у картці
       лишалась рамка 1px під кожною клітинкою — коротка риска посеред картки
       під ціною. Це третій раз, коли порядок правил у цьому файлі коштує
       розкладки (див. коментарі до .quote-cards і .discount-field). */
    .family-size-table.quote-table td { display: block; border: none; padding: 0; }
    .family-size-table td:first-child { flex: 1 1 100%; font-weight: 650; }
    .family-size-table td[data-label]::before {
        content: attr(data-label) " ";
        font-size: var(--t-xs); color: var(--c-muted);
        font-weight: 400; text-transform: uppercase; letter-spacing: .04em;
    }
    /* Код замовлення буває довгий (у MOUNT — 42 символи), і саме він мусить
       переноситись, а не тікати за край. */
    .family-size-table td:last-child { flex: 1 1 100%; overflow-wrap: anywhere; }
}

/* Рядок відновлення чернетки КП: пояснює, звідки взялися позиції. */
.quote-restored { margin: 0 0 var(--s-3); font-size: var(--t-sm); color: var(--c-muted); }
/* Прибрана позиція і «Повернути» (QuotePage.RemoveRowAsync): вузол постійний
   заради live-регіону, тож порожній не займає місця. */
/* Липне під топбаром: фокус після «✕» стає на «Повернути», і саме липкість
   тримає ціль на екрані, хоч би з якого рядка довгої КП її викликали, —
   focus() видимого елемента сторінку не гортає. Доти фокус тягнув сторінку до
   верху кошика після кожного видалення (адверсарний прохід 30.09); мутація
   «зняти липкість» це відтворює, а «фокус із прокруткою» — ні. */
.quote-removed {
    margin: 0 0 var(--s-3); padding: var(--s-2) 0; font-size: var(--t-sm);
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
    position: sticky; top: var(--topbar-h); z-index: 5; background: var(--c-bg);
}
/* Порожній — БЕЗ висоти, але не display:none: live-регіон мусить бути в
   дереві доступності ДО першого повідомлення, інакше перше «Прибрано: …»
   зчитувач не оголошує (регіон, що з'явився разом зі змістом, мовчить). */
.quote-removed:empty { margin: 0; padding: 0; }

/* Видима доріжка крихт: шлях угору з будь-якої сторінки виробу. */
.crumbs { margin: 0 0 var(--s-3); font-size: var(--t-sm); }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s-1); padding: 0; margin: 0; }
.crumbs li + li::before { content: "→"; color: var(--c-muted); margin-right: var(--s-1); }
.crumbs a { color: var(--c-muted); }
.crumbs [aria-current="page"] { color: var(--c-text-soft); }

/* Підпис походження цін під карткою: дата прайсу там, де стоїть число. */
.price-origin { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); }

/* Підтвердження видалення в кабінеті — інлайном, а не діалогом. */
.confirm-delete { font-size: var(--t-sm); color: var(--c-text-soft); margin-right: var(--s-2); }

/* Код замовлення на картці ціни: те, що менеджер диктує виробнику. */
.order-code { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-muted); }
.order-code strong { color: var(--c-text-soft); font-variant-numeric: tabular-nums; }
/* Той самий код у формі решітки (задача 11, як у формі Klimaoprema) і під ним —
   звіт підбору. Вигляд — як у картки ціни: це те саме число. */
.form-order-code { margin: 0; font-size: var(--t-sm); color: var(--c-muted); }
.form-order-code strong { color: var(--c-text-soft); font-variant-numeric: tabular-nums; }
.form-report { margin: 0; font-size: var(--t-sm); }
/* Заголовок рядка може бути довгим («Фарбування RAL 5015 «Небесно-синій»
   (+15…70%, у сумі — нижня межа)») — він мусить переноситися, а не з'їжджати
   за край картки разом із сумою. */
.price-lines li {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: var(--s-4); padding: 0.2rem 0; font-size: var(--t-sm); color: var(--c-text-soft);
}
.price-lines li > :first-child { min-width: 0; overflow-wrap: anywhere; }
.price-lines li > :last-child { white-space: nowrap; }
.price-card .actions { margin-top: var(--s-4); }

.aero-card { padding: var(--s-5); }
.aero-card h2 { font-size: var(--t-md); margin-bottom: var(--s-3); }
.aero-card dl { display: grid; grid-template-columns: auto auto; gap: var(--s-1) var(--s-4); margin: 0; }
.aero-card dt { color: var(--c-muted); font-size: var(--t-sm); }
.aero-card dd { margin: 0; font-weight: 650; text-align: right; font-size: var(--t-sm); }
.aero-rating { margin: var(--s-3) 0 0; font-size: var(--t-sm); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); font-weight: 600; }

/* Каталожні характеристики вентилятора (прайс-книга AirRoxy) — та сама
   типографіка, що AeroCard, але БЕЗ вердиктів/рейтингів: це максимуми
   каталогу, не робоча точка мережі. */
.fan-spec-card { padding: var(--s-5); }
.fan-spec-card h2 { font-size: var(--t-md); margin: 0 0 var(--s-3); }
.fan-spec-card h2 .aero-source { font-size: var(--t-xs); font-weight: 400; color: var(--c-muted); }
.fan-spec-grid { display: grid; grid-template-columns: auto auto; gap: var(--s-1) var(--s-4); margin: 0; }
.fan-spec-grid > div { display: contents; }
.fan-spec-grid dt { color: var(--c-muted); font-size: var(--t-sm); }
.fan-spec-grid dd { margin: 0; font-weight: 650; text-align: right; font-size: var(--t-sm); }
.fan-spec-note { margin: var(--s-3) 0 0; font-size: var(--t-xs); color: var(--c-muted); }
/* Корекція Kf (/accessory, KQ1/KQ8): рядок під AeroCard — результат, а не примітка,
   тож звичайний колір тексту; джерело позначає вкладений .aero-source. */
.kf-note { margin: var(--s-2) 0 0; font-size: var(--t-sm); }
.kf-note .aero-source { font-size: var(--t-xs); color: var(--c-muted); }
.rating-quiet { background: var(--ok-50); color: var(--ok-600); }
.rating-normal { background: var(--info-50); color: var(--info-600); }
.rating-loud { background: var(--warn-50); color: var(--warn-600); }
.rating-toofast { background: var(--err-50); color: var(--err-600); }

/* === Характеристичні криві (AeroCurves) ==================================
   Стиль лінії = джерело значення: суцільна «каталог», пунктир «оцінка» —
   та сама мова, що бейджі AeroCard. Кольори — акцент «повітря», як і решта
   аеродинаміки; жодного сирого hex (правило design-tokens). */
.aero-curves { padding: var(--s-4) var(--s-5); }
.aero-curves > summary { cursor: pointer; font-size: var(--t-sm); font-weight: 650; color: var(--c-text-soft); }
.aero-curves > summary small { font-weight: 400; color: var(--c-muted); }
.aero-curves-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--s-4); margin-top: var(--s-3); }
/* Тільки ПРЯМИЙ нащадок + явні width/height в атрибутах svg: на Chromium
   v1194 CSS-ширина перекриває геометричну (див. коментар до .preview > svg) —
   тут svg не вкладені, але селектор навмисно такий самий вузький, щоб правило
   ніколи не дотяглось туди, куди не мітило. */
.ac-chart > svg { width: 100%; height: auto; display: block; }

/* Товщина стала при масштабуванні кадру — як у ролей ліній креслення. */
.ac-cat, .ac-est, .ac-floor, .ac-axis, .ac-env { vector-effect: non-scaling-stroke; fill: none; }
.ac-cat   { stroke: var(--air-700); stroke-width: 1.7; }
.ac-est   { stroke: var(--air-500); stroke-width: 1.4; stroke-dasharray: 5 4; }
/* Цензурований шум: плазом по порогу, крапками — «тут каталог не міряв». */
.ac-floor { stroke: var(--c-muted); stroke-width: 1.2; stroke-dasharray: 2 3; }
.ac-axis  { stroke: var(--c-border-strong); stroke-width: 1; }
.ac-env   { stroke: var(--c-border-strong); stroke-width: 1; stroke-dasharray: 2 3; }
.ac-floor-zone { fill: var(--c-surface-sunk); stroke: none; }
.ac-tick, .ac-unit { font-size: 9px; fill: var(--c-muted); }
.ac-unit { font-weight: 650; }
.ac-point { fill: var(--c-brand); stroke: var(--c-surface); stroke-width: 1.5; }
.ac-point-label { font-size: 10px; font-weight: 650; fill: var(--c-text); }
.ac-legend { margin: var(--s-3) 0 0; font-size: var(--t-xs); color: var(--c-muted); }

/* Графік «Звук і тиск» вкладки «Порахувати» (реліз 1.3.0, задача 9; спек §6.3).
   Кадр — 430×286 ЯВНИМИ атрибутами й без CSS-масштабу (SoundPressureChart.FrameWidth):
   ширину виміряно ЗІ СМУГОЮ прокрутки 17 px — вміст картки 436,9 px на 1280 і
   434,9 на 1536 (без смуги 445,6 і 443,6; перемір задачі 12, 14.10, Inter і
   DejaVu), тож на 1280 і 1536 смуги гортання немає й на Windows із класичною
   смугою, а на вужчій колонці (1024 і 768 — ~320 px, 390 — 316)
   кадр гортається обгорткою, а не стискається: шрифт 10 px і клік 1:1 з
   координатами svg лишаються на будь-якій ширині. Крива — ті самі ac-cat /
   ac-est / ac-floor, що малі криві: одна мова джерела. */
.spc-card { padding: var(--s-4) var(--s-5); }
.spc-title { margin: 0 0 var(--s-2); font-size: var(--t-sm); font-weight: 650; color: var(--c-text-soft); }
/* Тінь гортання (раунд виправлень 1, M1): на вужчій колонці правий край кадру
   схований, а оверлейна смуга гортання його не видає. Дві «кришки» кольору
   картки їдуть із вмістом (local) і закривають тінь там, де гортати вже нікуди;
   дві тіні стоять на краях вікна (scroll). Кольори — токенами. */
.spc-scroll {
    overflow-x: auto;
    background:
        linear-gradient(to right, var(--c-surface) 30%, transparent) left center / 2rem 100% no-repeat local,
        linear-gradient(to left, var(--c-surface) 30%, transparent) right center / 2rem 100% no-repeat local,
        radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--c-text) 22%, transparent), transparent) left center / 0.75rem 100% no-repeat scroll,
        radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--c-text) 22%, transparent), transparent) right center / 0.75rem 100% no-repeat scroll;
}
.spc-scroll:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.spc-scroll > svg.spc { display: block; max-width: none; }
.spc-grid { stroke: var(--c-border); stroke-width: 1; stroke-dasharray: 2 3; fill: none; }
.spc-axis { stroke: var(--c-border-strong); stroke-width: 1; fill: none; }
.spc-tick > text, .spc-axis-title { font-size: 10px; fill: var(--c-muted); }
.spc-mark > circle { fill: var(--air-700); stroke: var(--c-surface); stroke-width: 1.5; }
.spc-mark > text { font-size: 10px; fill: var(--air-700); }
.spc-drop { stroke: var(--c-brand); stroke-width: 1; stroke-dasharray: 3 3; fill: none; }
.spc-point { fill: var(--c-brand); stroke: var(--c-surface); stroke-width: 1.5; }
/* Табличка робочої точки й табличка наведення — одне місце, один вигляд;
   поки миша над графіком (data-hover ставить aerochart.js), видно лише другу. */
.spc-box-bg { fill: var(--c-surface); fill-opacity: 0.92; stroke: var(--c-border); stroke-width: 1; }
.spc-box > text, .spc-hover > text { font-size: 10px; fill: var(--c-text); }
.spc-hover { visibility: hidden; }
.spc-hover-dot { fill: var(--c-surface); stroke: var(--c-brand); stroke-width: 2; }
svg.spc[data-hover] .spc-hover { visibility: visible; }
svg.spc[data-hover] .spc-box { visibility: hidden; }
.spc-hit { cursor: crosshair; }

/* === Прев'ю: таби, RAL, DXF ============================================== */
/* wrap — не косметика: смужка вкладок стала ТРИКНОПКОВОЮ («Схема | Фото | 3D»,
   09.09), і разом із чипом RAL та лінком DXF вона перестала влазити в 320px —
   заміряно: документ 358 px проти вікна 320, тобто вся сторінка їхала вбік. */
.preview-tabs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-3); }
.tab-group { display: inline-flex; gap: 2px; padding: 2px; background: var(--c-surface-sunk); border: 1px solid var(--c-border); border-radius: var(--r-sm); }
.preview-tab {
    border: none; background: transparent; color: var(--c-muted);
    border-radius: 4px; padding: 0.28rem 0.75rem; font: inherit; font-size: var(--t-sm); cursor: pointer;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.preview-tab:hover { color: var(--c-text); }
/* Рамка на активній пігулці — не прикраса: у темній темі --c-surface майже
   збігається з фоном треку, і стан «Схема|3D» тримався лише на вазі шрифту
   (UI/UX-прохід 18.08). Рамка видима в обох темах без theme-специфічних правил. */
.preview-tab.tab-active { background: var(--c-surface); color: var(--c-text); font-weight: 650; box-shadow: var(--sh-xs); border: 1px solid var(--c-border-strong); }
/* Зразок кольору з кодом. Сам по собі квадратик нічого не повідомляв: базовий
   RAL 9016 — це майже білий на білій картці, тобто читався як дефект верстки. */
.ral-chip {
    display: inline-flex; align-items: center; gap: var(--s-2);
    margin-left: auto; padding: 0.18rem 0.45rem 0.18rem 0.22rem;
    background: var(--c-surface-sunk);
    border: 1px solid var(--c-border); border-radius: var(--r-sm);
}
.ral-swatch { width: 16px; height: 16px; border-radius: 4px; border: 1px solid var(--c-border-strong); }
.ral-code { font-size: var(--t-xs); font-weight: 650; color: var(--c-text-soft); white-space: nowrap; }

/* У вузькій колонці слово «RAL» ховається, а КОД лишається: голий квадратик
   без підпису читався як порожній чекбокс (у темній темі — особливо), і сенс
   елемента зникав разом із тап-ціллю (UI/UX-прохід 18.08). Доти тут ховався
   весь .ral-code. */
@media (max-width: 460px) {
    .ral-word { display: none; }
}

/* ── Вибір кольору ────────────────────────────────────────────────────────
   Було голе поле на чотири цифри: щоб замовити колір, треба знати код
   напам'ять. Тепер поруч із полем — зразок обраного відтінку і палітра
   всього каталогу RAL CLASSIC, а поле лишається для тих, хто код знає. */
.ral-input-row {
    display: grid; grid-template-columns: 42px minmax(0, 1fr) auto;
    gap: var(--s-2); align-items: center;
}
.ral-current {
    width: 42px; height: 42px; border-radius: var(--r-sm);
    border: 1px solid var(--c-border-strong); box-shadow: var(--sh-xs);
}
.ral-toggle { min-height: 42px; white-space: nowrap; }

/* Покриття решітки: фарбування RAL чи анодований профіль (зріз S14). Радіо
   мусить скинути .field input (width: 100%, min-height: 42px) — інакше це
   величезний квадрат над підписом, як колись чекбокс. Тап-ціль тримає
   ПІДПИС (min-height 40px), а не саме коло. */
.ral-finish { display: flex; flex-wrap: wrap; gap: 0 var(--s-4); }
.ral-finish-option {
    display: inline-flex; align-items: center; gap: var(--s-2);
    min-height: 40px; cursor: pointer; font-size: var(--t-sm); color: var(--c-text);
}
.field .ral-finish-option input[type="radio"] {
    width: 1.05rem; height: 1.05rem; min-height: 0; flex: none;
    margin: 0; padding: 0; accent-color: var(--c-brand); cursor: pointer;
}

.field-hint { color: var(--c-muted); font-size: var(--t-xs); }
.field-error { color: var(--err-600); font-size: var(--t-xs); font-weight: 600; }

.ral-palette {
    margin-top: var(--s-2); padding: var(--s-3);
    background: var(--c-surface-sunk);
    border: 1px solid var(--c-border); border-radius: var(--r-md);
}
.ral-groups { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0; }
.ral-group {
    padding: 0.25rem 0.6rem; font: inherit; font-size: var(--t-xs); font-weight: 600;
    color: var(--c-text-soft); background: var(--c-surface); cursor: pointer;
    border: 1px solid var(--c-border-strong); border-radius: var(--r-pill);
}
.ral-group:hover { border-color: var(--c-muted); color: var(--c-text); }
/* Текст активного чипа — роль --c-on-brand, а не #fff: у темній темі бренд
   світлий (#6f9dff), і білий на ньому давав 2,64:1 (A11Y-MOBILE-3, аудит 06.10).
   Під курсором чип лишається активним ЯВНИМ правилом: доти .ral-group:hover
   (0,2,0) перебивав колір .ral-group-on (0,1,0), і текст ставав темним на
   синьому — 2,86:1 у світлій, 2,25:1 у темній (A11Y-MOBILE-X-1). Той самий
   прийом, що в .chip-active:hover. Сторож — CssStateContrastTests (каскадом). */
.ral-group-on { background: var(--c-brand); border-color: var(--c-brand); color: var(--c-on-brand); }
.ral-group-on:hover { background: var(--c-brand-hover); border-color: var(--c-brand-hover); color: var(--c-on-brand); }

/* Висота — рівно на кілька рядів плиток: палітра не має відсувати ціну за екран. */
.ral-scroll { max-height: 300px; overflow-y: auto; overscroll-behavior: contain; }
.ral-section {
    position: sticky; top: 0; z-index: 1;
    margin: var(--s-2) 0 var(--s-1); padding: 2px 0;
    background: var(--c-surface-sunk);
    font-size: var(--t-xs); font-weight: 650; color: var(--c-text-soft);
}
.ral-section:first-child { margin-top: 0; }
.ral-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(54px, 1fr)); gap: var(--s-2); }
.ral-tile {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    padding: 3px; font: inherit; cursor: pointer;
    background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
}
.ral-tile:hover { border-color: var(--c-border-strong); background: var(--c-surface); }
.ral-tile:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 1px; }
.ral-tile-swatch {
    width: 100%; aspect-ratio: 1; border-radius: 4px;
    border: 1px solid var(--c-border-strong);
}
.ral-tile-code {
    font-size: var(--t-2xs); font-weight: 600; color: var(--c-text-soft);
    font-variant-numeric: tabular-nums;
}
.ral-tile-on { border-color: var(--c-brand); background: var(--c-surface); box-shadow: var(--ring); }
.ral-tile-on .ral-tile-code { color: var(--c-brand); }
.ral-empty { font-size: var(--t-sm); color: var(--c-muted); margin: var(--s-3) 0; }
/* Чип IFC стоїть поруч із DXF і мусить виглядати так само: другий стиль того
   самого місця розійшовся б із першим на першій правці. Селектор — ПАРОЮ з
   рядком прев'ю: на «Документі» решітки той самий клас .ifc-link — звичайне
   посилання в реченні, і чип-рамка там була б чужою. */
.dxf-link, .preview-tabs .ifc-link {
    font-size: var(--t-xs); font-weight: 650; color: var(--c-text-soft); text-decoration: none;
    border: 1px solid var(--c-border-strong); border-radius: var(--r-sm);
    padding: 0.3rem 0.55rem; white-space: nowrap; transition: all var(--dur) var(--ease);
}
.dxf-link:hover, .dxf-link:focus-visible,
.preview-tabs .ifc-link:hover, .preview-tabs .ifc-link:focus-visible { border-color: var(--c-brand); color: var(--c-brand); }
/* Двійник лінка на час недонабраного розміру (INSTALLER-9): ті самі розміри
   (клас .dxf-link), але невидимий — місце тримається, дії немає. */
.dxf-link-slot { visibility: hidden; }

.model3d-frame { position: relative; }
.model3d-host {
    width: 100%; aspect-ratio: 4 / 3;
    /* Стеля висоти: без неї на планшеті 4:3 розганяло прев'ю до ~600 px і
       виштовхувало форму за екран. Ширина лишається 100%, кадр просто нижчий. */
    max-height: clamp(200px, 44vh, 460px);
    border-radius: var(--r-md); overflow: hidden;
    /* Тло сцени дає CSS (канвас three.js прозорий) — інакше воно лишалось би
       зашитим у model3d.js і в темній темі виріб стояв у білому боксі. */
    background: radial-gradient(120% 92% at 50% 4%, var(--viewer-1), var(--viewer-2));
}
/* Вьювер став фокусованим (WCAG 2.1.1): без видимої рамки зрячий клавіатурний
   користувач не бачить, що фокус уже на моделі й стрілки поїдуть у неї. */
.model3d-host:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.model3d-host canvas { display: block; width: 100% !important; height: 100% !important; }

/* Кнопки ракурсу/фулскріна — накладкою в кутку кадру. Правила для них не було
   зовсім, хоча `.model3d-frame { position: relative }` існує саме заради них:
   без нього пара іконок просто провалювалась під канвас окремим рядком. */
.model3d-toolbar {
    position: absolute;
    top: var(--s-2);
    right: var(--s-2);
    display: flex;
    gap: var(--s-1);
}

.model3d-toolbar .btn {
    width: 32px; min-height: 32px; padding: 0;
    background: color-mix(in srgb, var(--c-surface) 85%, transparent);
    backdrop-filter: blur(6px);
    border-color: var(--c-border);
    color: var(--c-text-soft);
}

.model3d-toolbar .btn:hover { background: var(--c-surface); color: var(--c-text); }
/* На весь екран іде РАМКА, а полотно лишалось зі стелею 460 px і 4:3 — виріб
   висів маленьким у верхньому куті, а 3/4 екрана були порожні (аудит UI).
   Знімаємо стелю саме у fullscreen; камеру перекадровує model3d.js. */
.model3d-frame:fullscreen { background: var(--viewer-2); display: grid; }
.model3d-frame:fullscreen .model3d-host {
    width: 100%; height: 100%; max-height: none; aspect-ratio: auto; border-radius: 0;
}

/* === Підбір за витратою ================================================== */
.select-page h1 { margin-bottom: var(--s-2); }
.select-intro { color: var(--c-muted); margin-bottom: var(--s-5); max-width: 62ch; }

/* Сторінка «Звідки числа»: текстова довідка, тож ширина рядка — читацька
   (62ch, як .select-intro), а розділи розділені лінією, а не карткою: це не
   набір дій, а документ. */
.methods-block { max-width: 74ch; margin-bottom: var(--s-6); }
.methods-block + .methods-block { border-top: 1px solid var(--c-border); padding-top: var(--s-5); }
.methods-block h2 { margin-bottom: var(--s-3); }
.methods-block p { color: var(--c-text-soft); max-width: 62ch; }
.methods-list { color: var(--c-text-soft); margin: var(--s-3) 0 0; padding-left: var(--s-5); max-width: 62ch; }
.methods-list li + li { margin-top: var(--s-2); }
.methods-dl { margin: var(--s-3) 0 0; max-width: 62ch; }
.methods-dl dt { margin-top: var(--s-3); }
.methods-dl dd { margin: var(--s-1) 0 0; color: var(--c-text-soft); }
.methods-note { font-size: .9em; }
/* Таблиці довідки ширші за текст — числа й цитати ДБН в один рядок. */
.methods-block .table-scroll { max-width: none; margin-top: var(--s-3); }
/* 900, а не 620: зріз 04.08 подвоїв кількість полів (14), і в 620 px вони
   лягали в 3 тісні колонки з наїздами підписів. */
.select-inputs { max-width: 900px; }
/* Лічильник видачі біля фільтрів: тихий рядок, а не кнопка — він ІНФОРМУЄ про
   наслідок правки, і мусить читатись як частина форми, не як заклик до дії. */
.select-count {
    display: inline-block; margin: 0 0 var(--s-3);
    font-size: var(--t-sm); font-weight: 650; color: var(--c-brand);
    text-decoration: none; padding: 0.15rem 0;
}
/* На телефоні лічильник ЛИПНЕ — і рівно в межах панелі фільтрів. Замір 20.08:
   з розкритою панеллю поля займають 1055 px, тож правлячи восьме поле,
   користувач уже не бачив ні видачі, ні числа, що змінюється. Липне під
   топбаром (він сам sticky), а на видачу не заходить: обгортка закінчується
   разом із панеллю. Десктоп не чіпаємо — там поля лягають у три колонки,
   і проблеми виміряно не було. */
@media (max-width: 719px) {
    .select-count {
        position: sticky; top: var(--topbar-h); z-index: 5;
        display: block; margin: 0 0 var(--s-2);
        background: var(--c-bg); padding: var(--s-2) 0;
        border-bottom: 1px solid var(--c-border);
    }
}
/* Розкриття додаткових фільтрів (зріз 39). Кнопка навмисно НЕ .btn: це не дія,
   а перемикач видимості — вигляд посилання з трикутником читається саме так. */
.select-more-toggle {
    display: inline-flex; align-items: center; gap: var(--s-2);
    margin: 0 0 var(--s-4); padding: var(--s-1) 0;
    background: none; border: 0; cursor: pointer;
    color: var(--c-brand); font: inherit; font-size: var(--t-sm); font-weight: 600;
}
.select-more-toggle:hover { text-decoration: underline; }
.select-more-toggle:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.select-more-caret { font-size: var(--t-xs); }
/* Згортання — КЛАСОМ під медіазапитом, а не атрибутом hidden. Два наслідки, і
   обидва потрібні: на широкому екрані правило не діє взагалі (десктоп лишається
   пікселя в піксель таким, як був — за нього платити не треба), а зміна ширини
   вікна не може лишити поля схованими без кнопки, бо і кнопка, і згортання
   живуть в одному медіазапиті. Атрибут hidden цього не дав би: display:grid із
   @supports(subgrid) його перебиває, і згортання не спрацьовувало б саме там,
   де воно й потрібне. */
/* Згортання діє на ВСІХ ширинах (зріз 47).
   Доти на десктопі додаткові фільтри стояли розкритими завжди, а перемикач
   ховався: «контрол, який нічого не міняє, — це шум». Міркування було вірне
   рівно доти, доки не заміряли, що воно коштує. Замір 08.08 на 1280×720: десять
   додаткових полів займають 466 px МІЖ полем витрати й видачею, і разом із
   розрізом кімнати відсували першу картку на y=1311 px — 1,82 екрана, 0 із 8
   карток у полі зору після вводу витрати. Тобто сторінка, чия робота «введи
   число → побач вироби», не показувала жодного виробу.
   Ціна згортання мала й зворотна: значення переживають згортання, один клік
   повертає, а сам перемикач несе «· задано N» — хто прийшов по шер-лінку з
   фільтрами, бачить це, не розкриваючи. */
.select-inputs-more.is-collapsed { display: none; }
/* Телефон: первинні входи — у ДВІ колонки, вступ ховається за наявності видачі.
   Мінімум 200 px на поле (див. вище) ставили за колізії підписів у контейнері
   620 px із чотирма полями — на 390 px він давав одну колонку й 540 px висоти
   самих входів. Замір 08.08 на 390×844: у дві колонки поле = 171 px, ЖОДНОГО
   обрізання ні підпису, ні селекта (scrollWidth = clientWidth у всіх чотирьох),
   а перша картка піднялась із 1046 на 882 px. Разом зі схованим вступом — 727. */
@media (max-width: 719px) {
    .field-row.select-inputs-main { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .select-inputs-main .field { flex-basis: auto; min-width: 0; }
    .select-intro.has-results { display: none; }
}
.assistant-notes { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: 0 0 var(--s-4); max-width: 720px; }
.assistant-note { background: var(--air-50); color: var(--air-700); border-radius: var(--r-pill); padding: 0.22rem 0.7rem; font-size: var(--t-xs); font-weight: 600; }
.assistant-miss { color: var(--c-muted); font-size: var(--t-sm); margin: 0 0 var(--s-4); }

.variant-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(236px, 1fr)); gap: var(--s-4); }
/* Картка більше не обирається кліком (розріз приміщення прибрано в 1.0.0),
   тож і cursor: pointer на ній був би обіцянкою дії, якої немає. */
.variant-card { padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-2); transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease); }
/* Вентилятори на витрату (/select, зріз 27): картки-лінки з фото і
   каталожними даними; біла підкладка фото навмисна і в темній темі. */
.fan-suggest { margin-top: var(--s-6); }
.fan-suggest h2 { font-size: var(--t-lg); margin-bottom: var(--s-3); }
/* inline-block: підзаголовок переноситься ЦІЛИМ — інакше «— каталожні…»
   лишав висяче тире в кінці рядка заголовка (UI/UX-прохід 18.08). */
.fan-suggest h2 small { font-size: var(--t-xs); font-weight: 400; color: var(--c-muted); display: inline-block; }
.fan-suggest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: var(--s-3); }
.fan-suggest-card {
  display: flex; flex-direction: column; gap: var(--s-1);
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-lg); box-shadow: var(--sh-sm);
  padding: var(--s-3); text-decoration: none; color: inherit;
}
.fan-suggest-card:hover { border-color: var(--c-border-strong); }
.fan-suggest-photo { max-height: 96px; object-fit: contain; background: #fff; border-radius: 8px; padding: 4px; align-self: center; max-width: 100%; }
.fan-suggest-title { font-weight: 700; }
.fan-suggest-series { font-size: var(--t-xs); color: var(--c-muted); }
.fan-suggest-specs { display: grid; grid-template-columns: auto auto; gap: 2px var(--s-3); margin: var(--s-1) 0 0; }
.fan-suggest-specs > div { display: contents; }
.fan-suggest-specs dt { color: var(--c-muted); font-size: var(--t-xs); }
.fan-suggest-specs dd { margin: 0; font-size: var(--t-xs); font-weight: 650; text-align: right; }
/* Нульовий запас вентилятора: попередження стоїть рівно біля числа «+0%»,
   бо саме воно й вводило в оману (у мережі з опором такий подасть менше).
   Колір — той самий, що в інших застережень картки, аби не заводити новий. */
.fan-nil-reserve { margin-left: 2px; color: var(--warn-600); cursor: help; }
.fan-suggest-price { margin-top: auto; padding-top: var(--s-2); font-weight: 700; }
.fan-suggest-note { margin-top: var(--s-3); font-size: var(--t-xs); color: var(--c-muted); max-width: 84ch; }
/* Вентилятори для об'єкта на /project — той самий ряд карток, згорнутий у details. */
.project-fans { margin: var(--s-3) 0; }
.project-fans > summary { cursor: pointer; font-weight: 650; }
.project-fans > summary small { font-weight: 400; color: var(--c-muted); }
.project-fans-side { margin-top: var(--s-3); }
/* h2, а не h4: над блоком на /project лише H1 сторінки, і h4 перестрибував два
   рівні (той самий клас, що F-44 на картці «Дані виробника»). */
.project-fans-side h2 { margin: 0 0 var(--s-2); font-size: var(--t-md); }
/* Акустика приміщення: рядок Σ-шуму специфікації (зріз 33). */
.room-noise { margin: var(--s-2) 0 0; font-size: var(--t-sm); }
.room-noise small { color: var(--c-muted); font-size: var(--t-xs); }
/* Нотатка вентиляторів — власний клас, не «пігулка асистента» (аудит). */
.fan-note { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-brand); }
.fan-added { font-size: var(--t-sm); color: var(--c-muted); }

/* Підказка комплектності в КП: текст + кнопка «дібрати» в один ряд. */
.completeness-hint { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
/* Звірка знімків цін із чинним прайсом: робоча смуга менеджера (no-print). */
.reprice-note { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
    margin: var(--s-3) 0; padding: var(--s-2) var(--s-3);
    border: 1px solid var(--c-border); border-left: 3px solid var(--c-brand);
    border-radius: var(--r-sm); font-size: var(--t-sm); }
.reprice-badge { font-size: var(--t-xs); color: var(--c-brand); white-space: nowrap; }
.reprice-badge.reprice-gone { color: var(--c-muted); }
/* Мініатюра виробу в картці підбору. АБСОЛЮТНО в кутку — щоб не додавати
   картці висоти: сторож фолда міряє видимість назви й ціни ПЕРШОЇ картки на
   трьох ширинах, і будь-який блок у потоці зсунув би їх униз. Праве поле дають
   лише три верхні блоки — нижні (чому, аеро, дії) йдуть на всю ширину. */
.variant-card { position: relative; }
.variant-thumb {
    position: absolute; top: var(--s-3); right: var(--s-3);
    width: 124px; height: 76px; display: flex; align-items: center;
    justify-content: flex-end; overflow: hidden; pointer-events: none;
}
/* Ширину задає КАДР, висоту — пропорція креслення (560×250): 96×96 робило з
   двовидового креслення розчавлений квадрат. Фото натомість тягнеться рядком
   ґріда, як у каталозі, — інакше знімок 1000 px був би вчетверо вищим за 100 px. */
.variant-thumb .quote-thumb { width: 124px; }
.variant-thumb .quote-thumb > svg { width: 124px; height: auto; max-height: 76px; }
.variant-thumb .drawing-photo { padding: 0; grid-template-rows: 76px; }
.variant-thumb .drawing-photo figcaption { display: none; }
.variant-thumb .drawing-photo img { padding: 2px; }
.variant-card .variant-tags,
.variant-card .variant-title,
.variant-card .variant-price { padding-right: 132px; }
@media (max-width: 480px) {
    .variant-thumb { width: 96px; height: 60px; }
    .variant-thumb .quote-thumb { width: 96px; }
    .variant-thumb .quote-thumb > svg { width: 96px; max-height: 60px; }
    .variant-thumb .drawing-photo { grid-template-rows: 60px; }
    .variant-card .variant-tags,
    .variant-card .variant-title,
    .variant-card .variant-price { padding-right: 104px; }
}
.variant-card:hover { box-shadow: var(--sh-md); }
.variant-optimal { border-color: var(--c-brand); box-shadow: 0 0 0 1px var(--c-brand), var(--sh-sm); }
.variant-tags { display: flex; gap: var(--s-1); flex-wrap: wrap; min-height: 1.1rem; }
/* В одноколонковій стрічці вирівнювати нема з чим — порожня смуга під бейдж
   лише подовжувала мобільну видачу і виглядала недомальованою (UI/UX 18.08);
   у сітці десктопа min-height лишається — він тримає картки в один рівень. */
@media (max-width: 719px) {
    .variant-tags:empty { display: none; }
}
.variant-tag { font-size: var(--t-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; border-radius: 4px; padding: 0.1rem 0.4rem; }
.tag-optimal { background: var(--c-brand); color: var(--c-on-brand); }
.tag-quietest { background: var(--ok-50); color: var(--ok-600); }
.tag-cheapest { background: var(--warn-50); color: var(--warn-600); }
/* «Чому ця картка тут» — НЕ ярлик: без капсу, без заливки, приглушено.
   Ярлик каже «чим вона краща за решту» і є щонайбільше в трьох карток із
   восьми; підпис відповідає на інше питання й стоїть у КОЖНОЇ. Зрівняти їх
   виглядом означало б перетворити опис місця у видачі на восьму «нагороду». */
.variant-why { margin: 0; font-size: var(--t-xs); color: var(--c-muted); }
/* Заголовок картки/рядка видачі — КНОПКА (клавіатурне обрання варіанта),
   тож знімаємо все, що браузер малює кнопці, лишаючи вигляд заголовка.
   Фокус-рамку не чіпаємо: глобальний :focus-visible вище і є тим, що робить
   обрання видимим для клавіатури. */
.variant-title {
    font-weight: 650; font-size: var(--t-sm);
    display: block; width: 100%; text-align: left;
    background: none; border: 0; margin: 0;
    /* Спадок кнопки (до 1.0.0 заголовок нею був) прибрано: `cursor: pointer` на
       НЕклікабельному тексті — це обіцянка, якої елемент не виконує, і власник
       на неї натрапив. Тепер усередині справжній лінк, і курсор веде саме він. */
    /* Тап-ціль, а не просто текст: рядок заголовка має 20 px, а мінімум за
       WCAG 2.2 (і за сторожем responsive.spec «жодного не-рядкового елемента
       < 24×24 px») — 24. Саме цей сторож і спіймав першу редакцію цієї правки:
       кнопка з padding: 0 давала 324×20 на десяти картках. */
    padding: 3px 0;
}
/* Уся картка — посилання на виріб (розтягнутий псевдоелемент лінка назви).
   Чому не обгортка <a> навколо картки: усередині є КНОПКА «У специфікацію», а
   кнопка в посиланні — недійсна розмітка, і клік по ній перехоплював би лінк.
   Тому клікає лінк назви, а дії підняті над ним z-index'ом. */
.variant-card { position: relative; }
.variant-link { color: inherit; text-decoration: none; }
.variant-link:hover { text-decoration: underline; }
/* Розтягнутий клік — ТІЛЬКИ на картці. Без цієї прив'язки псевдоелемент
   шукає найближчого позиціонованого предка, а в ТАБЛИЧНОМУ виді видачі його
   в комірці немає — і невидиме накриття розповзалось на всю таблицю, ловлячи
   навіть кліки по кнопках сортування заголовка (e2e «табличний режим
   сортується колонками» впав саме на цьому). У таблиці назва лишається
   звичайним посиланням: рядок і так вузький. */
.variant-card .variant-link::after { content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit; }
.variant-card .variant-actions { position: relative; z-index: 1; }
.variant-card:hover { cursor: pointer; }
.variant-price { font-size: var(--t-md); font-weight: 700; color: var(--c-brand); }
.variant-aero { display: grid; grid-template-columns: auto auto; gap: 0.1rem var(--s-3); margin: 0; font-size: var(--t-xs); }
.variant-aero dt { color: var(--c-muted); }
.variant-aero dd { margin: 0; text-align: right; font-weight: 650; }
/* Причина риски дальності (перенос K3 задачі 10) — другим dd під рискою, на всю
   ширину картки: речення в правій колонці чисел ламалось би по слову. */
.variant-aero dd.variant-no-throw { grid-column: 1 / -1; text-align: left; font-weight: 400; color: var(--c-muted); }
.select-mark { color: var(--c-muted); margin-left: 1px; }
.variant-card .btn { justify-content: center; margin-top: var(--s-1); }
.variant-card .aero-rating { margin: var(--s-1) 0 0; font-size: var(--t-xs); }

/* === Пропозиція ========================================================== */
.quote-page h1 { margin-bottom: var(--s-5); }
.quote-empty { color: var(--c-muted); }
.quote-loading { color: var(--c-muted); }
/* Горизонтальний скрол таблиці КП лишається для СЕРЕДНІХ ширин (640–719):
   там колонки вже вміщаються без послівних переносів. Нижче 640 діє карткова
   розкладка (див. нижче) — попередній min-width:640px на вузьких екранах
   ховав колонки «Ціна/шт» і «Сума» правіше краю БЕЗ жодного натяку на скрол:
   документ про гроші не показував грошей, а «Разом | 1» (кількість) читався
   як підсумок «1 грн» (UI/UX-прохід 18.08, HIGH; те саме бачив замовник на
   /kp). У друку скрол зайвий — там A4. */
.table-scroll { overflow-x: auto; }

/* «Назва» в таблицях кабінету: сусідні колонки (числа, дати, кнопки) не
   переносяться, тож auto-layout віддає breakable-колонці лише мінімум одного
   слова — «ЖК / Сонячний, / секція Б» стовпчиком (виміряно: 95 px при таблиці
   803). Floor на саму колонку: назва читається рядком, а таблиця, якій
   забракло місця, стає картками (див. `saved-list` нижче).

   Floor стоїть на комірці НАЗВИ (`data-title`), а не на першій колонці: у
   журналі запитів перша — «Рід», одне слово, і 220 px діставались їй, тоді як
   сама назва об'єкта тиснулась до 81 px (заміряно 29.09 на 1280). */
.saved-table td[data-title] { min-width: 220px; }
/* Контейнер для перемикання на картки: вирішує ширина, яку таблиця справді
   має, а не ширина вікна. На 1024 бічне меню лишає таблиці 728 px — менше,
   ніж на 768 без меню (736), тож поріг вікна тут брехав би в обидва боки. */
.saved-scroll { container: saved-list / inline-size; }
@media print {
    .table-scroll { overflow: visible; }
    .quote-table { min-width: 0; }
}

.quote-table { width: 100%; border-collapse: collapse; overflow: hidden; }
.quote-table th, .quote-table td { padding: var(--s-3); border-bottom: 1px solid var(--c-border); text-align: left; vertical-align: top; font-size: var(--t-sm); }
.quote-table th { background: var(--c-surface-sunk); font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--c-muted); font-weight: 650; }
.quote-table th.num, .quote-table td.num { text-align: right; white-space: nowrap; }
.quote-table tfoot td { border-top: 2px solid var(--c-border-strong); border-bottom: none; font-size: var(--t-base); }
.quote-table tbody tr:last-child td { border-bottom: none; }

/* Падінг таблиць на телефоні — 8 px замість 12. Три колонки коштують 72 px
   самими падінгами, і саме через них таблиці, які МАЙЖЕ вміщаються, вилазять
   за екран останньою колонкою: /norms 394 px у контейнері 358 (бейдж «норма»
   зрізаний), другий столик /methods — 380. Скролиться воно чесно
   (`.table-scroll`), але зрізана колонка виглядає як обрив, а не як натяк
   гортати вбік. Блок стоїть ВИЩЕ карткового: там `.quote-cards td` має
   padding 0 із такою самою специфічністю, тож нижчий перемагає — переставити
   ці два блоки місцями означало б повернути падінги в картки КП. */
@media screen and (max-width: 639px) {
    .quote-table th, .quote-table td { padding: var(--s-3) var(--s-2); }
}

/* КП на вузькому екрані — рядок таблиці стає карткою: назва з розбивкою на
   всю ширину, під нею — кількість, ціна/шт і сума, підписані словами
   (шапки таблиці тут немає).

   ДВІ речі, які тут уже коштували зіпсованого документа замовника.

   1. Блок мусить стояти НИЖЧЕ базових правил `.quote-table`. Специфічність у
      них однакова, тож перемагає те, що нижче у файлі, — а стояв він вище, і
      більшість його декларацій були МЕРТВІ: клітинки лишались із
      border-bottom:1px і padding:12px, підвал діставав border-top на КОЖНУ
      клітинку. На телефоні це давало обірвані риски під «К-ть»/«Ціна»/«Сума»
      і дві паралельні лінії над «Разом» (нижня — з трьох сегментів із
      розривами по 16 px). Заміряно 10.09 попіксельно й двобічно: та сама
      декларація, вставлена в КІНЕЦЬ аркуша, дає 0px — тобто вбивав саме
      ПОРЯДОК.

   2. Клас-опт-ін `.quote-cards`, а не `.quote-table`. Клас `.quote-table`
      носять 22 таблиці застосунку, а гачки td-item/td-qty/td-unit/td-total —
      рівно чотири (КП, КП замовника, історія підборів). Решті 18 картковий
      блок ховав `thead`, не даючи натомість жодного підпису: на /select
      кнопки сортування ставали 0×0 px (клік падав за таймаутом), у кабінеті
      між назвою і сумою стояло неозначене «2», на /methods рядок ріс на 92 %.
      Тобто розкладку, написану для одного документа, мовчки успадкували
      сторінки, які про неї не знали. */
@media screen and (max-width: 639px) {
    .quote-cards thead { display: none; }
    .quote-cards, .quote-cards tbody, .quote-cards tfoot { display: block; }
    .quote-cards tr {
        display: flex; flex-wrap: wrap; align-items: baseline;
        column-gap: var(--s-4); row-gap: var(--s-2);
        padding: var(--s-3); border-bottom: 1px solid var(--c-border);
    }
    /* Падінг саме ГОРИЗОНТАЛЬНИЙ, і це не косметика. Сама таблиця — панель із
       радіусом 14 px і тінню, тож `padding: 12px 0` клав текст РІВНО на її
       межу: заміряно 10.09 — панель 16…374, рядок 17…373, тобто інсет 1 px,
       а кнопка ✕ правим краєм лягала на заокруглений кут. Власник це і
       обвів на скриншоті. */
    .quote-cards td { display: block; border: none; padding: 0; }
    /* Номер позиції на телефоні не вартий колонки — порядок видно і так. */
    .quote-cards .td-no, .quote-cards td:empty { display: none; }
    /* Назва і ✕ — ОДНИМ рядком картки, обоє з `order: -1` (у своїй групі
       порядок лишається за розміткою, тож назва перша, кнопка друга). Це не
       смак: горизонтальний падінг забрав у рядка 24 px, і четвірка чисел із
       кнопкою (107 + 107 + 86 + 40 + проміжки) перестала вміщатись — ✕ падав
       ОКРЕМИМ рядком до лівого краю картки, під назву. Кут картки — де його й
       шукають; заразом рядок чисел лишається цілим (316 із 332). */
    .quote-cards .td-item { order: -1; flex: 1 1 calc(100% - 48px); }
    /* Підпис картки бере слова З РОЗМІТКИ, а не зі свого літерала. Доти тут
       лежала ДРУГА копія назв колонок — і копія вже розійшлася з оригіналом:
       шапка казала «Ціна/шт, грн», картка «Ціна/шт». Заміряно 10.09 на /kp із
       двома позиціями: 1280 px — два носії «грн» на сторінці, 390 px — НУЛЬ, і
       слова «грн» немає в document.body.innerText узагалі. Тобто документ, який
       замовник відкриває з телефона, не називав валюти ніде. */
    /* Специфікація («Проєктую») має ДВІ власні колонки — артикул і розмір.
       Без підпису вони ставали безіменними рядками поруч із «К-ть»: рівно той
       дефект, через який картковий блок і зробили опт-іном (у кабінеті між
       назвою і сумою стояло неозначене «2»). Артикул бере ВЛАСНИЙ рядок, бо
       вчотирьох вони в картку не влазять — заміряно 10.09: артикул 159 +
       розмір 84 + к-ть 91 + ✕ 40 = 374 при ширині картки 356, і ✕ падав
       третім рядком до ЛІВОГО краю, відірваний від своєї позиції. */
    /* Артикул і «розмір» у картці ПЕРЕНОСЯТЬСЯ. `nowrap` (таблиця на широкому
       екрані, де код не сміє ламатись посеред рядка) на телефоні обрізав його
       мовчки: заміряно 15.09 на 390 px — «АРТИКУЛ DUCT-FIT Клапан дросельний
       КДК ф100 (» замість «… (2)», тобто зникали останні символи КОДУ, який
       людина диктує виробнику. Три крапки були б не кращі: вони теж ховають
       символи, лише чесніше виглядають. У родин без габариту «розмір» — це та
       сама довга мітка моделі, тож правило те саме. */
    .quote-cards.spec-table .td-article,
    .quote-cards.spec-table .td-size { flex: 1 1 100%; white-space: normal; overflow-wrap: anywhere; }
    .quote-cards td[data-label]::before {
        content: attr(data-label) " ";
        font-size: var(--t-xs); color: var(--c-muted);
        font-weight: 400; text-transform: uppercase; letter-spacing: .04em;
    }
    /* «✕» стоїть у РЯДКУ ЧИСЕЛ, і рядок під нього стиснуто. Дві редакції до
       цієї заміряні й відкинуті: у потоці з проміжком 16 px четвірка не
       вміщалась (106,6 + 107,5 + 86 + 40 + 48 = 388 > 358 px картки) і кнопка
       з'їжджала окремим рядком, лишаючи 32 px порожнечі; винесення в кут
       картки абсолютом вимагало падінга на НАЗВУ, і та переносилась на другий
       рядок — 83 px замість 63,5, тобто порожнеча просто змінила місце.
       Робоче: проміжок 8 px і вужче поле кількості (заміряно 348 px із 358).
       Ціль дотику при цьому росте з 28×30 до 40×40 — доти це був найдрібніший
       контрол сторінки, у 8 разів менший за сусідні кнопки. */
    .quote-cards tr { column-gap: var(--s-2); }
    .quote-cards .qty-input { width: 56px; min-width: 0; }
    .quote-cards .td-actions { order: -1; flex: 0 0 40px; align-self: start; }
    .quote-cards .td-actions .btn { min-width: 40px; min-height: 40px; }
    /* Сума позиції — під сумою підсумку, а не в лівому краю картки: інакше
       «Сума 541.00» і «Разом 1 378.00» стоять у різних колонках (заміряно
       217,5 px один від одного), і арифметику документа не звірити оком. */
    .quote-cards .td-total { margin-left: auto; text-align: right; }
    /* У специфікації правий стовпчик — кількість (суми там немає), і підсумок
       «К-ть 2» стоїть саме там. Без цього рядка числа позицій тулились до
       розміру, а підсумок — до краю: одна колонка документа в двох місцях. */
    .quote-cards.spec-table .td-qty { margin-left: auto; }
    /* Смуга приміщення: фон переїжджає на рядок — клітинки тепер без падінгів. */
    .quote-cards .quote-room-row { background: var(--c-surface-sunk); padding: var(--s-2); }
    .quote-cards .quote-room-row td { background: transparent; }
    /* Підсумки: «Разом» ліворуч, сума праворуч; риска зверху — одна на блок,
       а не на кожну клітинку-блок. */
    .quote-cards tbody tr:last-child { border-bottom: none; }
    .quote-cards tfoot { border-top: 2px solid var(--c-border-strong); }
    .quote-cards tfoot td { border-top: none; }
    .quote-cards tfoot tr { border-bottom: none; padding: var(--s-2) var(--s-3); }
    .quote-cards tfoot .td-label { flex: 1 1 auto; }
    .quote-cards tfoot .td-total { margin-left: auto; }
    /* Кнопки документа — на всю ширину, а не драбиною по контенту: заміряно
       чотири кнопки замовлення з чотирма різними ширинами, правий край гуляв
       на 133,6 px. Скоуп саме на сторінку КП: клас .actions спільний. */
    .quote-page .actions > * { flex: 1 1 100%; justify-content: center; }
    /* Кнопка IFC сидить у формі з display: contents — для flex вона дитина
       .actions, а для селектора «> *» онука: без цього рядка на 390 px вона
       єдина лишалась по контенту (136 px серед смуг по 358; знімок F7b). */
    .quote-page .actions > .ifc-form > .btn { flex: 1 1 100%; justify-content: center; }
    /* ...але не ВСІ шість однаковими смугами. Заміряно 10.09 на 390 px: шість
       кнопок по 358 px одна під одною, і єдине, чим вони різняться, — підпис;
       два виходи документа (ТЗ 1.0.0, п. 3) тонули серед допоміжних. Другорядні
       йдуть парами: ширина в парі однакова, тож правий край лишається рівним —
       саме те, заради чого правило вище й з'явилось. */
    /* Перенос у ДРУГОРЯДНІЙ кнопці — не косметика: без нього пара документів
       замовлення розсипалась саме там, де правило мало її зібрати. Флекс-елемент
       за дефолтом має `min-width: auto`, тобто не стискається вужче за власний
       вміст, а `.btn` заборонена переносити текст (`white-space: nowrap`) — тож
       «Файл замовлення (Excel)» вимагав 222 px, сусід лишався 171, разом 401 при
       контейнері 358, і кожен ставав на ВЛАСНИЙ рядок із рваним правим краєм
       (заміряно 16.09 на 390 px). З дозволеним переносом мінімальна ширина — це
       вже найдовше СЛОВО, обидві беруть по 171, а підпис іде у два рядки.

       `min-width: 0` тут теж пробувався й ВИЯВИВСЯ ЗАЙВИМ: сам по собі він
       рядок збирає, але пускає текст ЗА МЕЖІ кнопки (scrollWidth 179 проти
       clientWidth 169), а разом із переносом уже нічого не міняє — мутація
       «прибрати min-width» лишає сторожа зеленим. Прибрано, щоб наступний
       читач не вважав його чинною межею. */
    .quote-page .actions > .btn-ghost { flex: 0 1 calc(50% - var(--s-2)); white-space: normal; }
}

/* Кабінет, якому бракує ширини: рядок таблиці — картка. Заміряно 10.09: таблиця
   збережених пропозицій має 971 px при контейнері 358 — 2,7 екрана. Видно
   «Назва | Позицій | С…», а сума, дата й УСІ кнопки рядка («Відкрити»,
   «PDF», «Лінк», «Видалити») лежать за правим краєм: дістатись до них можна
   лише здогадавшись гортати вбік, бо смуга прокрутки всередині картки на
   телефоні не малюється взагалі.

   Підпис колонки тут АТРИБУТОМ `data-label`, а не класом, як у КП: там
   гачків чотири на дві таблиці, а тут колонок п'ять–сім і вони РІЗНІ в
   трьох таблицях — правило на кожну означало б п'ятнадцять правил, які
   мовчки розходяться з розміткою (клас лишається в CSS і після того, як
   колонку прибрали з Razor).

   І головна відмінність від блоку КП вище: шапку тут НЕ ховаємо. У ній
   живуть кнопки сортування — сховавши її, телефон втратив би сортування
   зовсім; натомість вона стає рядком чипів над картками. У КП сортування
   немає взагалі, тому там `thead { display: none }` нічого не коштує.

   Поріг — ширина КОНТЕЙНЕРА (`saved-list`), а не вікна (п. 9 беклогу
   четвертого кола). Доти картки вмикались на вікні < 640, а таблиці на
   десктопі їхали вбік: на 1024 бічне меню лишає 728 px, а мінімальна ширина
   таблиць із рядком «+ 1 на прорахунку» була 956 / 868 / 1126 (проєкти /
   специфікації / журнал) — «Видалити» й «Повторити замовлення» лежали за
   краєм на 768–1100, журнал — ще й на 1280 і 1366. Після правок вмісту
   (назва замість першої колонки під floor, сума двома нерозривними шматками,
   дата з переносом, відступ сортованої шапки один раз, перенос підпису
   кнопки й роду) найширший реальний вміст — журнал із замовленням і запитом,
   934 px; поріг 960 дає запас на п'ятизначну суму. Нижче — картки, вище —
   таблиця без бічного скролу (сторож: cabinet.spec, «жодна таблиця кабінету
   не їде вбік», обхід ширин по обидва боки порогу). */
@media screen {
@container saved-list (max-width: 959px) {
    .saved-cards, .saved-cards thead, .saved-cards tbody, .saved-cards tr { display: block; }
    .saved-cards thead tr {
        display: flex; flex-wrap: wrap; gap: var(--s-1);
        padding: var(--s-3) var(--s-3) var(--s-2); border-bottom: none;
    }
    .saved-cards thead th { display: block; padding: 0; background: none; border: none; }
    /* Чип сортування — власним відступом чипа, а не табличним. */
    .saved-cards thead th > .select-sort { padding: 0.45rem 0.6rem; }
    /* Колонка кнопок заголовка не має — без цього над картками висів би
       порожній чип завширшки з решту рядка. */
    .saved-cards thead th:empty { display: none; }
    /* Шапка лишається РІВНО там, де вона щось робить. У журналі запитів
       сортування немає — там її слова («Рід», «Об'єкт», «Позицій»…) просто
       дублювали б підписи, які кожна картка вже несе в data-label, тобто
       рядок чипів був би чистим шумом. Питаємо не назву таблиці, а
       ВЛАСТИВІСТЬ: чи є в шапці кнопка. Прибрали сортування — рядок зникає
       сам, і сторожа для цього не треба. */
    .saved-cards thead:not(:has(button)) { display: none; }
    .saved-cards thead:has(button) tr::before {
        content: "Сортувати:"; font-size: var(--t-xs); color: var(--c-muted);
        text-transform: uppercase; letter-spacing: .04em; align-self: center;
    }
    .saved-cards tbody tr {
        display: flex; flex-wrap: wrap; align-items: baseline;
        column-gap: var(--s-3); row-gap: var(--s-1);
        padding: var(--s-3); border-bottom: 1px solid var(--c-border);
    }
    .saved-cards td { display: block; border: none; padding: 0; }
    /* Floor у 220 px на назву (щоб вона не сипалась стовпчиком у широкій
       таблиці) у картці означав би рівно те, від чого лікуємось, — рядок,
       ширший за екран. Специфічність навмисно та сама, 0-2-1. */
    .saved-cards td[data-title] { flex: 1 1 100%; font-weight: 650; min-width: 0; }
    .saved-cards td[data-label]::before {
        content: attr(data-label) " ";
        font-size: var(--t-xs); color: var(--c-muted); font-weight: 400;
        text-transform: uppercase; letter-spacing: .04em;
    }
    /* Кнопки рядка — власним рядком картки й з переносом: їх буває чотири,
       а в стані підтвердження ще й речення поруч. */
    .saved-cards .saved-actions, .saved-cards .row-actions {
        flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: var(--s-2);
    }
    .saved-cards tbody tr:last-child { border-bottom: none; }
}
}
/* Одиниця біля кількості («30 м»). Приглушена й меншим кеглем: число тут
   головне, одиниця лише не дає прочитати метри як штуки. Проміжок — власним
   відступом, бо сусід зліва це або поле вводу, або текст друку. */
.qty-unit { margin-left: 0.25em; font-size: var(--t-xs); color: var(--c-muted); }
/* Одиниця біля ЦІНИ («492.00 /уп.», DEALER-4): колонка — «Ціна за од.», і без
   одиниці ціна упаковки читалась як ціна штуки. Генерованим вмістом, а не
   текстом клітинки: Ctrl+A /quote і /kp вертається через /import, а ::after у
   буфер не копіюється — розбір рядка «к-ть ціна сума» лишається тим самим.
   Той самий приглушений вигляд, що в одиниці кількості. */
.unit-price[data-per]::after { content: "\00a0/" attr(data-per); font-size: var(--t-xs); color: var(--c-muted); }
/* Реквізити продавця на сторінці лінка — рядками, як у шапці PDF. Приглушено:
   головне в документі — вироби й сума, а це відповідь на «від кого воно». */
.shared-quote-seller { margin: calc(-1 * var(--s-2)) 0 var(--s-4); color: var(--c-muted); font-size: var(--t-sm); }
.shared-quote-seller-line { display: block; }
/* Усе, до чого браузер ДОВОДИТЬ прокрутку, зупиняється ПІД липким топбаром —
   і секція, до якої скролить сам застосунок, і елемент, що дістав фокус із
   клавіатури (WCAG 2.2, 2.4.11 Focus Not Obscured — пара до
   `scroll-padding-bottom` смуги ціни нижче: там рівно той самий механізм,
   лише знизу).

   Доти тут стояв `scroll-margin-top` на чотирьох id (#request,
   #select-results, #main-content, #quote-export): заміряно 10.09 на 390 px,
   що заголовок форми після «Оформити замовлення» ставав на y=17,2 під бар до
   y=60, і 23.09 — що блок #quote-export ставав на y=0. Правило лікувало
   ЦІЛІ, а не прокрутку: фокус, який браузер підводить у видиму зону, про бар
   не знає. Заміряно 26.09 обходом Shift+Tab (локальна Debug-збірка):
   /grille?…&f=500 — 6 контролів ПОВНІСТЮ під баром на 390 і 7 на 1280
   (кнопки сортування таблиці серії, «Відкрити»), /ldt на 390 — 5 (чипи
   щілин, «Палітра», код RAL), /select?f=500 — 4 на 390 і 3 на 1280
   («У специфікацію», поле витрати, вкладки виду; різних контролів, без
   повторів обходу по колу); /catalog — 17 і 18 зупинок частково. З цим
   рядком — 0 повністю схованих на всіх чотирьох сторінках обох ширин.

   Один рядок на ЕЛЕМЕНТІ ПРОКРУТКИ замість переліку id: `scroll-padding`
   діє на scrollIntoView (ventcalc.focusSection), перехід до фрагмента
   (quote#quote-export) і доведення фокусу однаково. Точкові
   `scroll-margin-top` зняті свідомо: вони ДОДАЮТЬСЯ до padding (вийшло б
   144 px над кожною ціллю), а перелік id застаріває мовчки — #series-data-
   title, куди скролить «Таблиця даних», у ньому вже й не стояв.
   Запас `--s-3` понад висоту бару: ціль не має торкатись його краю.
   Сторожі: a11y.spec («фокус не ховається під топбаром», обидві ширини),
   responsive.spec (#request-title), product-tabs.spec (#quote-export) і
   grille-table.spec (#series-data-title на 390). */
html { scroll-padding-top: calc(var(--topbar-h) + var(--s-3)); }

/* Найдрібніші контроли на телефоні дістають 40 px висоти. Заміряно 10.09:
   вкладки прев'ю 30,4 px і лише 2,0 px між сусідніми (три цілі впритул —
   «Схема | Фото | 3D»), лінк DXF 29,6, а розкривачі <summary> взагалі
   18–22,5 px: смуга влучання менша за подушечку пальця, і промах на 12 px не
   робить нічого. Це найчастіші дотики на сторінці виробу й у кабінеті.

   Розкривачу додаємо ПАДІНГ, а не display: flex: flex прибирає стандартний
   трикутник розкриття в WebKit, тобто ціль стала б більшою, а ознака
   «це розкривається» — зникла. */
@media screen and (max-width: 639px) {
    .preview-tab, .dxf-link, .preview-tabs .ifc-link { min-height: 40px; display: inline-flex; align-items: center; }
    summary { padding-block: 11px; }
}

.quote-breakdown { font-size: var(--t-xs); color: var(--c-muted); margin-top: var(--s-1); }
/* Артикул позиції в КП (ТЗ 1.0.0). Той самий приглушений рівень, що розбивка
   ціни: у документі головне — назва й сума, артикул читають при звірці. */
.quote-article { font-size: var(--t-xs); color: var(--c-muted); margin-top: var(--s-1); }
.quote-item-link { font-weight: 600; }
.quote-request-cell { color: var(--warn-600); font-style: italic; }
.quote-item-caveat { margin-top: 2px; color: var(--warn-600); font-size: var(--t-xs); line-height: 1.35; }
/* Смуга приміщення в КП (зріз 2 проєкту): токен поверхні, а не свій колір, —
   темна тема успадковується сама; у PDF та сама роль — Palette.Band (--n-100). */
.quote-room-row td { background: var(--c-surface-sunk); color: var(--c-text-soft); font-weight: 650; font-size: var(--t-sm); }
.quote-note { color: var(--c-muted); font-size: var(--t-xs); margin-top: var(--s-3); }
.quote-hint { margin: var(--s-3) 0 0; font-size: var(--t-sm); color: var(--c-text-soft); }
.quote-badge { background: var(--c-brand); color: var(--c-on-brand); border-radius: var(--r-pill); font-size: var(--t-xs); font-weight: 650; padding: 0.05rem 0.45rem; margin-left: var(--s-1); }
/* Мініатюра схеми в рядку КП. 190 px на 560-одиничний viewBox — це масштаб 0.34,
   тобто розмірні написи виходили ~4 px: не підпис, а шум. Збільшувати шрифт у
   координатах SVG не можна — підписи розставлені під 12–14 одиниць і почали б
   налазити один на одного. Тому міняємо тільки кадр: трохи ширший на екрані
   (рядок лишається компактним) і помітно ширший на друк, де сторінка все одно
   віддається під документ для замовника. */
/* Мініатюра малює ГЕОМЕТРІЮ, а не креслення: підписи в ній не читаються ЗА
   ПОБУДОВОЮ. Заміряно 10.09 (ефективний кегль = кегль × масштаб CTM):
   картка /select — 2,21…2,66 px на 76 написах, картка каталогу — те саме,
   мініатюра в КП — 4,11…4,93 px на екрані й 5,89…7,07 px у друку. Тобто
   помаранчеві заголовки видів («Вид знизу», «Півпереріз по осі») тягли око
   до тексту, якого прочитати неможливо, а розмірні написи лягали сірим
   шумом поверх самої геометрії.
   Розмір позиції при цьому не зникає, і це перевірено по ВСІХ п'яти
   рендерерах прев'ю (Grille/Diffuser/Anemostat/Linear/Accessory): кожен друкує
   під кресленням звичайний HTML-підпис із ключовими розмірами («Шийка: Ø299 мм
   · панель: Ø407 мм · …»), а він не SVG і сюди не потрапляє. У картці видачі
   підпис сховано окремим правилом, і там розмір несе НАЗВА картки.
   І це ПРИВОДИТЬ екран до того, що вже робить PDF: QuoteThumbSvg малює
   один-єдиний <text> — підпис розміру — і жодного розмірного напису.
   Перелік класів не з пам'яті: обхід 8 сторінок дав рівно чотири родини
   (dwg-title 334, dwg-label 382, dwg-label air-text 54, dwg-label dwg-mark
   147, dwg-units 106) і жодного тексту без класу. */
.quote-thumb .dwg-title, .quote-thumb .dwg-label, .quote-thumb .dwg-units { display: none; }
/* Разом із написами йде і РОЗМІРНА ОБВ'ЯЗКА: винесені лінії, розмірні лінії
   й стрілки без чисел нічого не означають — вони лишались сірою решіткою
   поверх виробу (у півперерізі дифузора її було більше, ніж самої деталі).
   Осьові (dwg-axis) і повітря (dwg-air) лишаються: перші дають виробу вісь,
   другі показують напрям потоку — це малюнок, а не позначення розміру. */
.quote-thumb .dwg-dim, .quote-thumb .dwg-ext, .quote-thumb .dwg-arrow { display: none; }

.quote-thumb { max-width: 230px; margin-top: var(--s-2); opacity: .95; }
@media print { .quote-thumb { max-width: 330px; opacity: 1; } }
.quote-print-header {
    display: flex; align-items: center; gap: var(--s-2);
    margin-bottom: var(--s-4); font-size: var(--t-sm); color: var(--c-muted);
}
/* Підпис авторства притиснутий до правого краю аркуша — навпроти назви застосунку. */
.designed-by-print { margin-left: auto; }

/* === Запит на прорахунок (зріз 38) ====================================== */
/* Секція живе під діями /quote і має власну межу зверху: це окремий крок
   воронки, а не ще один рядок кнопок. */
.quote-request {
    margin-top: var(--s-5); padding-top: var(--s-4);
    border-top: 1px solid var(--c-border);
}
.quote-request h2 { margin: 0 0 var(--s-2); font-size: var(--t-lg); }
.quote-request-lead { margin: 0 0 var(--s-3); color: var(--c-muted); max-width: 72ch; }
.quote-request-fields { margin-top: var(--s-2); }
/* Роль alert — на обгортці, список усередині (A11Y-MOBILE-4): відступ маркерів
   живе на ul, а не на коробці повідомлення, тож вигляд той самий, що доти. */
.quote-request-problems { margin: var(--s-3) 0 0; padding-left: 0; }
.quote-request-problems ul { margin: 0; padding-left: var(--s-5); }
.quote-request-problems li { margin: 2px 0; }
.quote-request-actions { margin-top: var(--s-3); }
.quote-request-hint { margin: var(--s-2) 0 0; color: var(--c-muted); font-size: var(--t-xs); }
.quote-request-outcome { margin-top: var(--s-3); }
.quote-request textarea { width: 100%; resize: vertical; }

/* Профіль кабінету (зріз 41): згорнутий, коли вже заповнений — це налаштування,
   а не щоденна дія; порожній розгортається сам, бо саме тоді його й треба. */
.account-profile, .account-password, .account-danger { margin-bottom: var(--s-5); padding-bottom: var(--s-4); border-bottom: 1px solid var(--c-border); }
.account-profile > summary, .account-password > summary, .account-danger > summary { cursor: pointer; font-weight: 650; }
.account-profile > summary small, .account-password > summary small, .account-danger > summary small { color: var(--c-muted); font-weight: 400; }
.account-profile .field-row, .account-password .field-row, .account-danger .field-row { margin-top: var(--s-3); }
.account-profile-hint { margin: var(--s-2) 0 0; color: var(--c-muted); font-size: var(--t-xs); max-width: 76ch; }
/* Попередження перед видаленням акаунта: звичайний текст, не error-плашка —
   помилки тут ще немає, є незворотність, і її пояснює зміст, а не колір. */
.account-danger-warn { margin: var(--s-3) 0 0; max-width: 76ch; }
.account-danger > summary { color: var(--err-600); }
/* Поля секцій кабінету на вузькому екрані — В ОДНУ колонку: базовий field-row
   (flex-basis 140px) ставив «Чинний/Новий пароль» парою по ~150 px — пароль не
   набрати й не перевірити оком, а «Телефон» лишався сиротою в другому ряду
   (UI/UX-прохід 18.08).

   10.09 той самий симптом знайшовся у формі ЗАМОВЛЕННЯ на /quote, і правило
   його не лікувало лише тому, що перелічувало імена руками: `.account-*`
   було, `.quote-request-fields` — ні. Заміряно на 390 px: дві колонки по
   171 px, «Телефон» сам у другому ряду, праворуч 187 px порожнечі (52 %
   ряду), а email у 171 px не прочитати цілком. */
@media (max-width: 480px) {
    /* Розкладку field-row насправді веде ҐРІД із блоку @supports(subgrid)
       (auto-fit minmax(140px,1fr) — дві колонки по ~171px на 390px, виміряно
       наживо), тож головний рядок тут — grid-template-columns; flex-* — фолбек
       для браузерів без subgrid, де field-row лишається флексом. */
    .account-profile .field-row, .account-password .field-row, .account-danger .field-row,
    .quote-request-fields {
        grid-template-columns: 1fr;
        flex-direction: column; align-items: stretch;
    }
}

.save-quote { display: flex; align-items: center; gap: var(--s-2); margin-top: var(--s-5); flex-wrap: wrap; padding-top: var(--s-4); border-top: 1px solid var(--c-border); }
.save-quote-title { flex: 1 1 240px; width: auto; }
/* Той самий інпут усередині label.field (колонковий flex): flex-basis 240px там
   стає ВИСОТОЮ, і однорядкове поле «Об'єкт / замовник» розтягувалось у
   псевдо-textarea на чверть екрана (спіймано скриншотом зрізу 2). */
.field .save-quote-title { flex: none; width: 100%; }
/* Збереження проєкту: поле з підписом вище за кнопку, тож вирівнюємо по низу. */
.save-project { align-items: end; }
.save-project .object-field { flex: 1 1 260px; margin: 0; }
.save-project-hint { flex-basis: 100%; color: var(--c-muted); }
.save-quote-shared { flex-basis: 100%; margin: var(--s-3) 0 0; color: var(--c-muted); font-size: var(--t-sm); }
/* Кнопки, чий підпис несе ДАНІ, а не сталу, мусять переносити текст.
   `.btn` заборонено переносити (`white-space: nowrap`) — це правильно для
   «Друк» чи «PDF», але тут підпис збирається з того, що людина вписала сама:
   «Оновити «{назва об'єкта}»» на /quote і /project, а замість нього — нотатки
   відмови й конфлікту вкладок на пів речення. Заміряно 29.09 (R4-raster-1):
   назва на 60 символів робила кнопку 589 px, документ — 605 px на 390-екрані;
   мобільний браузер через це зменшував УСЮ сторінку, а сусідні кнопки лягали
   під поле листа й не натискались. Той самий клас — кнопка надсилання
   запиту: «… менеджеру {ім'я}» задає власник на /accounts, тобто довжину
   теж ніхто не обмежує.

   Для підписів, що вміщаються, це нічого не міняє: у рядку з `flex-wrap`
   кнопка й так бере ширину свого тексту; перенос і стеля 100 % спрацьовують
   лише тоді, коли одна кнопка ширша за рядок. `anywhere`, а не `break-word`:
   лише він зменшує мінімальну ширину, тобто назва одним словом без пробілів
   теж не розпирає сторінку. Обрізати назву на сервері не можна — вона ключ
   об'єкта в кабінеті, і людина мусить бачити, ЩО саме оновить.
   Сторож — responsive.spec «кнопка «Оновити «…»» з довгою назвою…». */
.save-quote .btn, .quote-request-actions .btn {
    max-width: 100%; white-space: normal; overflow-wrap: anywhere; text-align: center;
}
.save-quote-hint { margin-top: var(--s-5); padding-top: var(--s-4); border-top: 1px solid var(--c-border); color: var(--c-muted); font-size: var(--t-sm); }

/* === Кабінет і вхід ====================================================== */
.auth-page { max-width: 400px; margin: var(--s-8) auto; }
.auth-page h1 { margin-bottom: var(--s-2); }
.auth-note { color: var(--c-muted); font-size: var(--t-sm); margin-bottom: var(--s-5); }
.auth-form { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--s-5); box-shadow: var(--sh-sm); }
.auth-form .btn { width: 100%; margin-top: var(--s-2); }
/* Лінки під формою — окремими рядками: «Немає акаунта? Зареєструватись Забули
   пароль?» упритул читалось одним реченням, а тап-цілі злипались (UI/UX 18.08). */
.auth-switch { margin-top: var(--s-4); font-size: var(--t-sm); color: var(--c-muted); text-align: center; }
.auth-switch > span { display: block; }
.auth-switch > span + span { margin-top: var(--s-2); }

/* Колонка «Акаунт» сторінок решіток (AccountColumn; реліз 1.3.0, спек §6.6).
   Праворуч (з 1536 px) вона завширшки --account-w; під змістом (нижче) — не
   ширша за форму /login, щоб два поля не тяглися через увесь зміст. Картка —
   та сама, що .auth-form: одна форма входу, один вигляд. */
.account-quote, .account-box { width: 100%; max-width: 400px; }
/* «Обрані вироби (N)» у колонці 260 px: на 200 % тексту підпис кнопки (.btn — nowrap)
   обрізався («(0)» за краєм: 39 px з Inter, 57 з DejaVu — хвиля 3, M-3b,
   w3-200text-probe.txt) — переноситься, як кнопки дій /quote. */
.account-quote { white-space: normal; text-align: center; }
.account-box {
    margin-top: var(--s-4);
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-lg); padding: var(--s-5); box-shadow: var(--sh-sm);
}
.account-title { margin-bottom: var(--s-3); font-size: var(--t-md); }
/* Ім'я з профілю чи адреса входу — без пробілів буває довшою за колонку. */
.account-name { font-weight: 650; overflow-wrap: anywhere; margin-bottom: var(--s-2); }
.account-form .btn { width: 100%; }
.account-links { margin: var(--s-3) 0 0; font-size: var(--t-sm); }

.account-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); flex-wrap: wrap; margin-bottom: var(--s-6); }
.logout-form { display: flex; align-items: center; gap: var(--s-3); }
.account-email { color: var(--c-muted); font-size: var(--t-sm); }
.saved-table { width: 100%; border-collapse: collapse; overflow: hidden; }
.saved-table th, .saved-table td { padding: var(--s-3); border-bottom: 1px solid var(--c-border); text-align: left; font-size: var(--t-sm); }
.saved-table th { background: var(--c-surface-sunk); font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--c-muted); }
/* Сортована шапка: відступ несе КНОПКА (уся клітинка клікабельна), а не
   клітинка навколо неї. Доти рахувались обидва — 12 px клітинки плюс 9,6 px
   кнопки з кожного боку, — і кожна сортована колонка коштувала таблиці зайві
   19 px (п'ять колонок проєктів — 95 px, 921 → 844 заміряно 29.09), а текст
   шапки стояв на 9,6 px правіше за текст комірок. Сторож — пара «перша літера
   шапки ↔ перша літера комірки» в cabinet.spec (мутація дає 9,59). */
.saved-table th:has(> .select-sort) { padding: 0; }
.saved-table th > .select-sort { padding: var(--s-3); }
.saved-table tbody tr:last-child td { border-bottom: none; }
/* З переносом і на десктопі: у «Проєктую» рядок збереженої специфікації має
   П'ЯТЬ кнопок («Excel без цін» поруч із «PDF КП», R4-roles-7), і без
   переносу таблиця на 1280 із меню виходила за контейнер на ~110 px — «Лінк»
   і «Видалити» ховались за бічним скролом (заміряно знімком 29.09). */
.saved-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); }
/* Підпис кнопки рядка може перенестись: інакше мінімальну ширину колонки дій
   задає найдовший ПІДПИС («Файл замовлення (XLSX)», «Excel без цін»), а не
   найдовше слово, і таблиця, що вміщалась би, їде вбік. Той самий прийом, що
   в кнопках /quote на телефоні. */
.saved-table .saved-actions .btn, .saved-table .row-actions .btn { white-space: normal; }
.account-hint, .account-empty { color: var(--c-muted); font-size: var(--t-sm); }
/* Числа й одиниці таблиць кабінету не ламаються всередині
   («770 м³/ год», «ПОЗИЦІЙ КП») — перенос між словами тут гірший за скрол.
   Дата — без класу: єдиний пробіл у «29.09.2026 17:59» стоїть між датою і
   часом, і саме там перенос нічого не ламає. Сума — два нерозривні шматки
   (число з «грн» і доданок «+ N на прорахунку», див. розмітку): колонка
   вужча на 80 px, ніж зі 230 px фрази в один рядок, а фраза не рветься
   посередині. */
.saved-table .no-wrap { white-space: nowrap; }

/* Шер-лінк збереженої КП (зріз 54): панель під рядком + бейдж чинного лінка. */
.share-panel { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
/* Моноширинний і на всю вільну ширину: токен довгий, і обрізаний хвіст має бути
   видно ЯК обрізаний, а не читатись як повний URL. */
.share-url { flex: 1 1 260px; min-width: 0; font-family: var(--font-mono, monospace);
    font-size: var(--t-xs); padding: var(--s-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm, 6px); background: var(--c-surface-sunk); color: var(--c-text); }
.share-until { color: var(--c-muted); font-size: var(--t-xs); flex-basis: 100%; }
.share-badge { color: var(--c-muted); font-size: var(--t-xs); margin-top: var(--s-1); }

/* Журнал запитів (зріз 85). Три підсумки розрізняються КОЛЬОРОМ і словом одразу:
   «Взято вручну» — не невдача (текст складено й показано), але й не «надіслано»,
   тож нейтральний muted, а не зелений і не червоний. Той самий поділ, що бейджі
   aero-badge-cat / aero-badge-est: тон каже про клас надійності твердження. */
.request-outcome { font-size: var(--t-xs); font-weight: 600; white-space: nowrap; }
.request-outcome-sent { color: var(--ok-600); }
.request-outcome-copied { color: var(--c-muted); }
.request-outcome-failed { color: var(--err-600); }
.request-comment { color: var(--c-muted); font-size: var(--t-xs); margin-top: var(--s-1);
    overflow-wrap: anywhere; }

/* Публічний перегляд КП за лінком (/kp/{token}). */
.shared-quote-meta { color: var(--c-text-soft); font-size: var(--t-sm); margin-bottom: var(--s-4); }
.shared-quote-footer { color: var(--c-muted); font-size: var(--t-xs); margin-top: var(--s-4); }

/* === Бічна навігація ======================================================
   Глобально, а не scoped: .nav-link рендерить компонент NavLink, і scoped-атрибут
   NavMenu на його <a> не потрапляє (для цього й існує ::deep). */

.nav-brand {
    display: flex; align-items: center;
    height: var(--topbar-h);
    padding: 0 var(--s-4);
    border-bottom: 1px solid var(--c-border);
}

/* Кнопка закриття шухляди. На широкому екрані панель стоїть постійно —
   закривати нема чого, тож кнопки немає (див. NavMenu.razor: ЧОМУ вона взагалі
   потрібна). Ціль 40×40, як решта тап-цілей документа. */
.nav-close {
    display: none; margin-left: auto;
    width: 40px; height: 40px; padding: 0;
    background: none; border: 0; border-radius: var(--r-sm);
    color: var(--c-muted); font-size: var(--t-md); line-height: 1; cursor: pointer;
}
.nav-close:hover { background: var(--c-surface-sunk); color: var(--c-text); }
@media (max-width: 1000px) { .nav-close { display: inline-flex; align-items: center; justify-content: center; } }

.nav-brand-link {
    display: inline-flex; align-items: center; gap: var(--s-2);
    color: var(--c-text); text-decoration: none;
}

/* Знак несе власний градієнт і фон-плитку — обгортка-підкладка більше не потрібна. */
.brand-logo { display: block; flex: none; }

/* «Vent» — вагою, «Calc» — кольором. Доти жирними були обидві половини:
   акцент нічого не виділяв, бо виділяти було нічого. */
.nav-brand-text { font-weight: 700; font-size: var(--t-md); letter-spacing: -0.015em; }
.nav-brand-accent { color: var(--c-brand); font-weight: 400; }

/* Перемикач режиму «Проєктую / Монтую» (1.0.0) — сегментований, як вибір теми
   в підвалі, але вгорі: це не оформлення, а те, ЩО показує меню нижче. Дві
   GET-форми стоять поруч; активний сегмент — aria-pressed, малює CSS. */
.mode-switch {
    display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
    margin: 0 var(--s-3) var(--s-2); padding: 2px;
    background: var(--c-surface-sunk);
    border: 1px solid var(--c-border); border-radius: var(--r-sm);
}
.mode-switch form { display: contents; }
/* Рамка 1 px є в ОБОХ станах (прозора в неактивного): поява рамки на обраному
   не зсуває сусіда. */
.mode-option {
    border: 1px solid transparent; background: transparent; color: var(--c-muted);
    border-radius: 4px; padding: 0.4rem 0.5rem;
    font: inherit; font-size: var(--t-sm); font-weight: 650;
    cursor: pointer;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.mode-option:hover { color: var(--c-text); }
/* Обраний режим — НЕ лише відтінком (A11Y-MOBILE-2, аудит 06.10): фон
   --c-surface на треку --c-surface-sunk дає 1,03:1, тож сегменти розрізняв
   тільки колір тексту. Тепер ще рамка кольором бренду (≥ 6:1 до треку в обох
   темах) і вага 700 проти 650 — ознаки форми, які видно без розрізнення
   кольорів. Стан — від aria-pressed, тобто показане й оголошене читаються з
   одного джерела. Сторож — CssStateContrastTests (каскадом). */
.mode-option[aria-pressed="true"] {
    background: var(--c-surface); color: var(--c-brand); box-shadow: var(--sh-sm);
    border-color: var(--c-brand); font-weight: 700;
}

/* --- підвал меню: вибір теми --- */

.nav-foot {
    margin-top: auto;
    /* Підпис авторства став третім елементом підвалу — сітка замість flex-рядка,
       щоб «designed by ТЕЗ» стояв власним рядком над вибором теми. */
    display: grid; grid-template-columns: auto 1fr; align-items: center;
    gap: var(--s-2) var(--s-3);
    padding: var(--s-3) var(--s-4) calc(var(--s-4) + env(safe-area-inset-bottom));
    border-top: 1px solid var(--c-border);
}
.designed-by-nav { grid-column: 1 / -1; }
/* Версія релізу під підписом авторства: свій рядок, дрібним, без акценту —
   довідка, а не заклик. */
.nav-version {
    grid-column: 1 / -1;
    font-size: var(--t-xs); color: var(--c-muted);
    font-variant-numeric: tabular-nums;
}

/* === Підпис авторства ==================================================== */
.designed-by {
    display: inline-flex; align-items: center; gap: 0.45em;
    /* Два знаки з підписами не влазять в один рядок бічної панелі (248 px) —
       перенос на другий рядок, а не переповнення. */
    flex-wrap: wrap; row-gap: 0.35em;
    font-size: var(--t-xs); color: var(--c-muted);
}
.designed-by-label { letter-spacing: .02em; }
/* inline-flex, щоб анкер не додавав базлайнового просвіту під растром і межа
   кліку збігалася з межею знака, а не з рядком тексту.
   min-height — не косметика: сам знак 15 px заввишки, а це МЕНШЕ за палець, і
   тап-сторож responsive.spec.ts на /quote це впіймав одразу, як анкер з'явився.
   Ціль ростимо до 24 px (WCAG 2.5.8), знак лишається 15 — align-items центрує
   його в цілі, тобто видимий розмір не змінюється, змінюється зона дотику. */
.designed-by-link, .powered-by-link {
    display: inline-flex; align-items: center; min-height: 24px;
    border-radius: 2px;
}
.designed-by-sep { opacity: .6; }
/* Підпис + знак — нерозривна пара; у бічній панелі пари стають у стовпчик,
   і роздільник між ними там зайвий. */
.designed-by-item { display: inline-flex; align-items: center; gap: 0.45em; white-space: nowrap; }
.designed-by-nav .designed-by-sep { display: none; }
/* Кути знака Центру вентиляції заокруглені в самому растрі (прозорі), тож
   border-radius тут лише для клік-фокуса. */
.designed-by .powered-by-link img { border-radius: 4px; }
/* Висота задає розмір, ширину рахує браузер із width/height у розмітці —
   без стрибка розкладки на завантаженні. */
.designed-by img {
    /* 20 px на обидва знаки: ТЕЗ і Центр вентиляції — пара однієї висоти
       (перевірено прев'ю 09.09 на світлому й темному тлі). */
    height: 20px; width: auto; display: block;
    border-radius: 2px;
    /* Плашка знака чорна: на світлому тлі це його рідний вигляд, на темному
       вона зливається з тлом і лишається саме напис — обидва стани навмисні. */
}

.nav-foot-label {
    font-size: var(--t-xs); font-weight: 650;
    text-transform: uppercase; letter-spacing: .06em;
    color: var(--c-muted);
}

.theme-picker {
    display: inline-flex; gap: 2px; padding: 2px;
    background: var(--c-surface-sunk);
    border: 1px solid var(--c-border); border-radius: var(--r-sm);
}

/* Та сама підлога 40 px, що й у чіпів: перемикач теми живе в шухляді, тобто
   натискають його пальцем, а заміряно було 26 px. */
.theme-option {
    border: 1px solid transparent; background: transparent; color: var(--c-muted);
    border-radius: 4px; padding: 0.25rem 0.5rem;
    display: inline-flex; align-items: center; min-height: 40px;
    font: inherit; font-size: var(--t-xs); font-weight: 600;
    cursor: pointer;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.theme-option:hover { color: var(--c-text); }

/* Активний варіант — від атрибута на <html>, а не від класу, доданого скриптом:
   переживає перебудову піддерева на гідратації і не потребує interop.
   Рамка й вага — той самий захід, що в перемикача режиму вище (A11Y-MOBILE-2):
   прийом «фон на треку» тут той самий, і відтінок 1,03:1 теж. */
:root:not([data-theme]) .theme-option[data-mode="system"],
:root[data-theme="light"] .theme-option[data-mode="light"],
:root[data-theme="dark"] .theme-option[data-mode="dark"] {
    background: var(--c-surface);
    color: var(--c-text);
    box-shadow: var(--sh-xs);
    border-color: var(--c-brand);
    font-weight: 700;
}

.nav-scrollable { padding: var(--s-4) var(--s-3) var(--s-8); }

.nav-section {
    margin: var(--s-4) var(--s-2) var(--s-2);
    font-size: var(--t-xs); font-weight: 650;
    text-transform: uppercase; letter-spacing: .06em;
    color: var(--c-muted);
}
.nav-section:first-child { margin-top: 0; }

.nav-link {
    display: flex; align-items: center; gap: var(--s-3);
    padding: 0.5rem var(--s-3);
    margin-bottom: 2px;
    border-radius: var(--r-sm);
    color: var(--c-text-soft);
    font-size: var(--t-sm); font-weight: 550;
    text-decoration: none;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-link:hover { background: var(--c-surface-sunk); color: var(--c-text); }

/* NavLink додає .active поточному пункту */
.nav-link.active {
    background: var(--brand-50);
    color: var(--c-brand);
    font-weight: 650;
}

.nav-ico { width: 20px; height: 20px; flex: none; opacity: .85; }
.nav-link.active .nav-ico { opacity: 1; }

.nav-count {
    margin-left: auto;
    font-size: var(--t-xs); font-weight: 650;
    color: var(--c-muted);
    background: var(--c-surface-sunk);
    border: 1px solid var(--c-border);
    border-radius: var(--r-pill);
    padding: 0 0.4rem;
}
.nav-link.active .nav-count { color: var(--c-brand); border-color: color-mix(in srgb, var(--c-brand) 30%, transparent); background: var(--c-surface); }

/* --- дерево категорій (зріз S10): групи <details> із листками-серіями ---
   Розкривач — рідний <summary>, без скрипта: група працює ще до гідрації, а
   шухляда на телефоні живе без Blazor навмисно (див. MainLayout).
   min-height 40 px — не косметика: <summary> стоїть під сторожем «дрібних
   цілей» responsive.spec (поріг 40, як у решти кнопок), і сторож міряє й
   закриту шухляду (visibility: hidden лишає розміри). */
.nav-group { margin-bottom: 2px; }
.nav-group-summary {
    display: flex; align-items: center; gap: var(--s-3);
    min-height: 40px; padding: 0 var(--s-3);
    border-radius: var(--r-sm);
    color: var(--c-text-soft);
    font-size: var(--t-sm); font-weight: 550;
    cursor: pointer; list-style: none;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-group-summary::-webkit-details-marker { display: none; }
.nav-group-summary:hover { background: var(--c-surface-sunk); color: var(--c-text); }
/* Шеврон — ПІСЛЯ лічильника (той притиснутий праворуч margin-left: auto):
   стан «розкрито/згорнуто» читається з того самого місця в кожному рядку. */
.nav-group-summary::after {
    content: ""; flex: none;
    width: 6px; height: 6px; margin: 0 2px 2px 0;
    border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg); opacity: .7;
    transition: transform var(--dur) var(--ease);
}
.nav-group[open] > .nav-group-summary::after { transform: rotate(45deg); }
.nav-group-current > .nav-group-summary { color: var(--c-brand); font-weight: 650; }
.nav-group-current > .nav-group-summary .nav-ico { opacity: 1; }

/* Листки відступлені рівно під текст підпису групи (padding + іконка + gap),
   тож дерево читається деревом без жодних ліній. */
.nav-leaves {
    list-style: none; margin: 0 0 var(--s-1);
    padding: 0 0 0 calc(var(--s-3) + 20px + var(--s-3) - var(--s-2));
}
.nav-leaf {
    display: flex; align-items: center;
    min-height: 32px; padding: 0.3rem var(--s-2);
    margin-bottom: 1px;
    border-radius: var(--r-sm);
    color: var(--c-text-soft);
    /* Звичайна вага, а не 550 пунктів: листок — другий рівень, і однакова
       вага з групою (знімок 23.09) злипала дерево в один суцільний список. */
    font-size: var(--t-xs); font-weight: 450; line-height: 1.3;
    text-decoration: none;
}
.nav-leaf:hover { background: var(--c-surface-sunk); color: var(--c-text); }
.nav-leaf.active { background: var(--brand-50); color: var(--c-brand); font-weight: 650; }

/* «Монтую»: група стоїть ПІД своїм розділом прайсу — без іконки, дрібніше, з
   тим самим відступом, що й листки, щоб не змагатися з пунктом розділу. */
.nav-group-sub > .nav-group-summary {
    padding-left: calc(var(--s-3) + 20px + var(--s-3) - var(--s-2));
    font-size: var(--t-xs);
}
.nav-group-sub > .nav-leaves { padding-left: calc(var(--s-3) + 20px + var(--s-3)); }

/* Заголовок групи — ПОСИЛАННЯ на сторінку категорії (задача 13: «Алюмінієві решітки»
   → /grilles). Воно СУСІД <summary>, а не нащадок: усередині summary (роль button) це
   axe nested-interactive, serious на кожній сторінці.
   Посилання стоїть У ПОТОЦІ й саме задає висоту рядка: іконка й назва — в ньому, а
   праворуч воно лишає місце розкривачеві. Розкривач (summary: лічильник і шеврон)
   накладений на це місце абсолютно. Перша редакція робила навпаки — накладала
   посилання на рядок summary висотою 40 px, і при більшому тексті (масштаб тексту,
   інтервали WCAG 1.4.12, довша назва) посилання виростало за рядок і накривало
   перший листок (рев'ю задачі 13, Minor 2). Тепер рядок росте з назвою, а листки
   йдуть під ним. Запас праворуч — у rem, як і сам розкривач (padding, лічильник),
   тож росте разом із масштабом тексту; overflow-wrap — щоб слово, ширше за вузьку
   панель на 200 %, переносилось, а не лізло під лічильник. Висота ≥ 40 px, як у
   summary (сторож «дрібних цілей» responsive.spec). */
.nav-group-row { position: relative; margin-bottom: 2px; }
.nav-group-link {
    display: flex; align-items: center; gap: var(--s-3);
    min-height: 40px; padding: var(--s-1) calc(2 * var(--s-3) + 3.5rem) var(--s-1) var(--s-3);
    border-radius: var(--r-sm);
    color: var(--c-text-soft);
    font-size: var(--t-sm); font-weight: 550; line-height: 1.3;
    overflow-wrap: anywhere;
    text-decoration: none;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* Рядок підсвічується як ОДИН — і над назвою (посилання), і над розкривачем: доти
   курсор над назвою підсвічування рядка гасив (Minor 3). */
.nav-group-link:hover,
.nav-group-row:has(> .nav-group > .nav-group-summary:hover) > .nav-group-link {
    background: var(--c-surface-sunk); color: var(--c-text);
}
.nav-group-row > .nav-group { margin-bottom: 0; }
.nav-group-row > .nav-group > .nav-group-summary {
    position: absolute; top: 0; right: 0; z-index: 1;
    padding: 0 var(--s-3);
    background: transparent;
}
.nav-group-row-current > .nav-group-link { color: var(--c-brand); font-weight: 650; }
.nav-group-row-current > .nav-group-link .nav-ico { opacity: 1; }
.nav-group-row-sub > .nav-group-link {
    padding-left: calc(var(--s-3) + 20px + var(--s-3) - var(--s-2));
    font-size: var(--t-xs);
}
/* Посилання на розділ сторінки категорії під листками («Пленум-бокси до решіток»):
   метрика листка, текст — урівень із листками, але приглушений — це не серія, і
   лічильник групи його не рахує. */
.nav-group-more {
    display: flex; align-items: center;
    min-height: 32px; padding: 0.3rem var(--s-2);
    margin: 0 0 var(--s-1) calc(var(--s-3) + 20px + var(--s-3) - var(--s-2));
    border-radius: var(--r-sm);
    color: var(--c-muted);
    font-size: var(--t-xs); font-weight: 450; line-height: 1.3;
    text-decoration: none;
}
.nav-group-more:hover { background: var(--c-surface-sunk); color: var(--c-text); }
.nav-group-sub > .nav-group-more { margin-left: calc(var(--s-3) + 20px + var(--s-3)); }

/* === Мобільний sticky-бар ціни =========================================== */
.price-sticky { display: none; }
/* Лише одноколонкова розкладка: від 720 px картка ціни стоїть у правій колонці
   й видима без прокрутки, тож дублювати її смугою внизу нема сенсу. */
@media (max-width: 719px) {
    .price-sticky {
        display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
        position: fixed; inset: auto 0 0 0; z-index: 40;
        padding: var(--s-3) var(--s-4);
        background: var(--c-surface); border-top: 1px solid var(--c-border);
        box-shadow: var(--sh-lg);
        padding-bottom: max(var(--s-3), env(safe-area-inset-bottom));
    }
    /* Було .price-total — такого класу в sticky немає (price-sticky-total). */
    .price-sticky-total { font-size: var(--t-lg); font-weight: 700; letter-spacing: -0.02em; }
    /* Сума й позначка «перевірте розмір» — стовпчиком (INSTALLER-9): у ряд
       позначка стискала суму до двох рядків, і смуга виростала за 88 px. */
    .price-sticky-sum { display: flex; flex-direction: column; min-width: 0; }
    /* Лише коли sticky реально в DOM: інакше /catalog /select /quote мали
       мертву смугу 76 px без бару (аудит UI). */
    /* 88, не 76: із двома рядками тексту бар вищий, і останній елемент форми
       розминався з ним на ~10 px (замір по /accessory, UI/UX-прохід 18.08). */
    body:has(.price-sticky) { padding-bottom: 88px; }
    /* ФОКУС не сміє ховатись ЗА смугою — WCAG 2.2, 2.4.11 Focus Not Obscured.
       padding-bottom вище дає місце ВНИЗУ СТОРІНКИ, але браузер, доводячи
       сфокусований елемент у видиму зону, про фіксовану смугу не знає й
       підводить його рівно під неї. Заміряно 06.09 на 390×844: 15 контролів
       на п'яти сторінках конфігуратора ховались ПОВНІСТЮ (/grille 4,
       /anemostat 2, /diffuser 2, /ldt 6, /accessory 1) — тобто людина, що
       ходить Tab-ом, не бачила поля, у яке пише.
       axe цього не ловить і не міг: він дивиться на розмітку, а тут винен
       СКРОЛ. Висота та сама, що в padding, — бар і відступ мусять
       мінятись разом. */
    html:has(.price-sticky) { scroll-padding-bottom: 88px; }
}

/* === Друк ================================================================ */
@media print {
    /* Селектор навмисно повторює темний (`:root:not([data-theme="light"])`, 0-1-1):
       голий `:root` (0-0-1) програвав йому за специфічністю, тож у користувача з
       темною системною темою пропозиція йшла на друк ТЕМНОЮ — світлий текст на
       білому папері. Медіазапит специфічності не додає. */
    :root, :root:not([data-theme="light"]), :root[data-theme="dark"] {
        color-scheme: light;
        --c-bg: #fff; --c-surface: #fff; --c-surface-sunk: #fff;
        --c-text: #000; --c-text-soft: #222; --c-muted: #444;
        --c-border: #bbb; --c-border-strong: #999;
        --dwg-ink: #363f4d; --dwg-line: #5b6673; --dwg-thin: #97a3b4;
        --dwg-fill: #f5f7fa; --dwg-void: #fff;
        --dwg-mark: #b3543a; --dwg-air: #0a6b6a;

        /* Семантичні акценти — теж. Доти блок скидав лише НЕЙТРАЛЬНІ ролі, і на
           папері лишалися темними 24 токени з 40, які перевизначає темна тема:
           «на прорахунку» й застереження про вихід за паспорт друкувались
           блідо-помаранчевим #f0a94e на білому — контраст 2,00:1 замість 5,02:1.
           Коментар у design-tokens.css при цьому стверджував, що сторінки друку
           завжди світлі; вимір це спростував (аудит 05.09).
           Перелік не пишеться руками: CssTokenTests звіряє його з темним блоком
           механічно й падає, щойно там з'явиться токен без пари тут. */
        --c-brand: var(--brand-600); --c-brand-hover: var(--brand-700);
        --c-on-brand: var(--n-0);
        /* Скрим на папері не малюється взагалі (і шухляда, і модал — no-print),
           але скидається так само механічно: сторож звіряє ПЕРЕЛІК токенів
           темної теми з цим блоком, і виняток «цей не потрібен» довелося б
           тримати в голові. */
        --c-scrim: rgba(19, 25, 32, .45);
        --brand-50: #eef4ff; --brand-100: #dbe6fe;
        --ok-600: #067647; --ok-50: #e9f7ef;
        --warn-600: #b45309; --warn-700: #8a4708; --warn-50: #fff4e5;
        --err-600: #b42318; --err-50: #fdecea;
        --info-600: #1a56db; --info-50: #eef4ff;
        --air-50: #e8f7f7; --air-500: #0e9594; --air-700: #0a6b6a;
        --ring: 0 0 0 3px var(--brand-100);
        --sh-xs: 0 1px 2px rgba(19, 25, 32, .06);
        --sh-sm: 0 1px 3px rgba(19, 25, 32, .08), 0 1px 2px rgba(19, 25, 32, .04);
        --sh-md: 0 4px 12px rgba(19, 25, 32, .08), 0 1px 3px rgba(19, 25, 32, .05);
        --sh-lg: 0 12px 32px rgba(19, 25, 32, .12);
        --viewer-1: var(--n-200); --viewer-2: var(--n-300);
    }
    .app-sidebar, .app-topbar, .no-print, .price-sticky { display: none !important; }

    /* Сторінка виробу на папері — це паспорт, а не форма: кнопки, вкладки прев'ю,
       палітра, майстер приміщення й поради на аркуші не діють, а місце з'їдають
       (аудит UI: Ctrl+P на конфігураторі давав друковані контроли). Значення
       полів лишаються — саме вони й описують виріб. */
    .btn, .preview-tabs, .room-panel, .advice-card, .chips-hint,
    .ral-palette, .ral-search, .preset-chips, .size-filter { display: none !important; }

    /* На папері типоряд — це ОДИН рядок: обрана модель. Доти тут друкувався
       `<select size=8>`, тобто вісім чужих моделей поруч із обраною; список
       без цього правила надрукував би всі 185. Сам список не ховається (як
       чіпи й фільтр вище) — саме він і несе значення поля. */
    .model-list { max-height: none; overflow: visible; border: none; background: none; }
    /* !important — не з розпачу: базові правила списку стоять НИЖЧЕ в файлі, а
       специфічність однакова, тож без нього виграє джерельний порядок. Перша
       редакція цього блоку саме так і мовчала: на папері друкувались усі 46
       моделей DUCT-FIT (у FAN-BATH було б 185) — перевірено emulateMedia. */
    .model-row { display: none !important; }
    .model-row-active {
        display: grid !important; background: none !important; color: inherit !important;
        padding-left: 0; border-top: none;
    }

    /* Поля без рамок і підкладки читаються як рядки специфікації. */
    .field input, .field select, .field textarea {
        border: none !important; box-shadow: none !important; background: none !important;
        padding-left: 0 !important; min-height: 0 !important; font-weight: 600;
        appearance: none; -webkit-appearance: none;
    }

    /* Порожня картка ADPI (довжину не вводили) — на аркуші сам заголовок ні про що. */
    .adpi-card:not(:has(.adpi-verdict)) { display: none !important; }

    /* Каркас на друк розкладає MainLayout.razor.css: `.app` описаний там, тобто
       зі scope-атрибутом, і голий `.app` звідси програвав би за специфічністю. */
    .app-main, .app-content { padding: 0 !important; margin: 0 !important; }
    .quote-table, .saved-table { box-shadow: none; }
    a { text-decoration: none; color: #000; }
}
/* Показуємо ЛИШЕ на друк. Значення `revert`, а не block/inline: клас носять і
   <header class="quote-print-header print-only">, і <span class="print-only"> у
   комірці кількості — жорстке значення завжди ламало б один із двох. Так і було:
   правило оголошувалось ДВІЧІ (тут `block`, нижче по файлу `inline`), пізніше
   перемагало за порядком, і шапка друкованої КП ставала інлайновою — gap,
   align-items, margin-bottom і притиснутий праворуч підпис ТЕЗ не діяли.
   `.quote-print-header` перевизначається ТУТ ЖЕ, після `.print-only`: у нього
   та сама специфічність (один клас), тож виграє те, що стоїть пізніше. */
.print-only { display: none; }
@media print {
    .print-only { display: revert; }
    .quote-print-header { display: flex; }
}

/* === Сторінка помилки ==================================================== */
.error-lead { color: var(--c-text-soft); max-width: 62ch; margin: var(--s-3) 0 var(--s-5); }
.error-request-id { color: var(--c-muted); font-size: var(--t-sm); margin-bottom: var(--s-5); word-break: break-all; }
.error-request-id code {
    font-family: var(--font-mono); font-size: var(--t-xs);
    background: var(--c-surface-sunk); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); padding: 0.05rem 0.35rem;
}

/* Blazor-помилка: не з дефолтного шаблону, а в мові інтерфейсу */
#blazor-error-ui {
    display: none; position: fixed; inset: auto 0 0 0; z-index: 1000;
    padding: var(--s-3) var(--s-5); background: var(--warn-50); color: var(--warn-600);
    border-top: 1px solid var(--warn-600); box-shadow: var(--sh-lg); font-size: var(--t-sm);
}
/* Кнопка, а не посилання (клавіатура) — тож знімаємо типове тло й рамку,
   щоб вигляд лишився тим самим хрестиком. */
#blazor-error-ui .dismiss { cursor: pointer; float: right; background: none;
  border: 0; padding: 0; color: inherit; font: inherit; line-height: 1; }

/* Модал перепідключення — НАШ (див. довгий коментар у MainLayout.razor).
   Класи вмикає сам рантайм на елементі #components-reconnect-modal; тексту він
   на цій гілці не пише жодного, тож усі слова наші. */
/* Базовий стан — на КЛАСІ, а не на id, і це не косметика: id — контракт із
   рантаймом, тож мутація «модал не наш» його забирає. Доки «сховано» висіло на
   id, така мутація робила нашу розмітку ВИДИМОЮ, і сторож червонів на порожньому
   знімку замість твердження «модал наш». */
.reconnect-modal {
    display: none; position: fixed; inset: 0; z-index: 1100;
    align-items: center; justify-content: center;
    background: var(--c-scrim); padding: var(--s-5);
}
/* Показуємо на ВСІХ станах, які фреймворк умикає. «hide» свідомо не тут:
   саме він і означає «зв'язок повернувся». */
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-paused,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-resume-failed,
#components-reconnect-modal.components-reconnect-rejected { display: flex; }

.reconnect-card {
    max-width: 30rem; padding: var(--s-5) var(--s-6);
    background: var(--c-surface); color: var(--c-text);
    border: 1px solid var(--c-border); border-radius: var(--r-lg);
    box-shadow: var(--sh-lg); font-size: var(--t-sm); text-align: center;
}
.reconnect-line { display: none; margin: 0; }
.components-reconnect-show .reconnect-line-retry,
.components-reconnect-retrying .reconnect-line-retry,
.components-reconnect-paused .reconnect-line-paused,
.components-reconnect-failed .reconnect-line-failed,
.components-reconnect-resume-failed .reconnect-line-failed,
.components-reconnect-rejected .reconnect-line-rejected { display: block; }

/* Лічильники має сенс показувати лише з ДРУГОЇ спроби: доти рантайм тримає
   в них 1 і 0, тобто «наступна через 0 с» — рядок ні про що. */
.reconnect-attempt { display: none; color: var(--c-muted); }
.components-reconnect-retrying .reconnect-attempt { display: block; }

.reconnect-card .reload { display: inline-block; margin-top: var(--s-3); }

/* === Поради в конфігураторі =============================================== */

.advice-card {
    padding: var(--s-5);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
}

.advice-card h2 { margin-bottom: var(--s-3); font-size: var(--t-md); }
.advice-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }

.advice-item {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--s-3);
    padding-left: var(--s-3);
    border-left: 3px solid var(--c-border-strong);
}

.advice-item .btn { flex: none; }
.advice-text { font-size: var(--t-sm); color: var(--c-text-soft); line-height: var(--lh-base); }

.advice-quieter { border-left-color: var(--ok-600); }
.advice-cheaper { border-left-color: var(--c-brand); }
.advice-oversized { border-left-color: var(--warn-600); }

/* === Підбір за фото ====================================================== */

.photo-panel {
    max-width: 620px;
    margin-bottom: var(--s-6);
    padding: var(--s-5);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
}

/* Падінги успадковуються від `.field input` — власні робили поле файлу на 7 px
   вищим за кожен інший контрол у застосунку (49 проти 42). Своє тут лише те,
   що справді відрізняє зону завантаження: пунктир і втоплене тло. */
.photo-drop input[type="file"] {
    border-style: dashed;
    background: var(--c-surface-sunk);
    cursor: pointer;
}

.photo-drop input[type="file"]:hover { border-color: var(--c-brand); }
.photo-drop small { display: block; margin-top: var(--s-1); color: var(--c-muted); font-size: var(--t-xs); }

.photo-result {
    display: flex; align-items: center; gap: var(--s-3);
    margin: var(--s-4) 0 var(--s-3);
}

.photo-swatch {
    width: 44px; height: 44px; flex: none;
    border-radius: var(--r-md);
    border: 1px solid var(--c-border-strong);
}

.photo-ral { font-weight: 650; font-size: var(--t-md); }
.photo-shape { font-size: var(--t-sm); color: var(--c-muted); }
.photo-heading { margin-bottom: var(--s-4); font-size: var(--t-md); }
.photo-why { font-size: var(--t-xs); color: var(--c-muted); }

/* === Каталог ============================================================= */
.catalog-search { max-width: 460px; margin-bottom: var(--s-4); }
/* Стрибок до розділу: чипи одним рядком зі скролом — на телефоні каталог
   ~11 екранів, і до «Автоматики»/«Монтажу» інакше десяток свайпів наосліп
   (UI/UX-прохід 18.08). */
/* Ряд розділів каталогу. Заміряно 10.09: дев'ять чипів дають 1640 px, і ряд
   не вміщався НА ЖОДНІЙ ширині — 656 px за кадром навіть на 1280, 1282 на
   390 px, причому афордансу не було ніякого (смуга 0 px, згасання none), тобто
   про існування решти розділів дізнатись не було з чого.

   Ширший за 640 px екран переносить ряд (78 px у два ряди на 1280, 117 у три
   на 768) — усі розділи видно одразу. Телефон лишається стрічкою: перенос там
   коштує 273,7 px, тобто третину екрана до першої картки. Стрічка — рівно та
   сама, що для чипів типорозмірів: згасання + snap + текстова підказка, бо
   згасання на світлому тлі майже не видно (те саме, що вже заміряно там). */
.catalog-nav { display: flex; gap: var(--s-2); overflow-x: auto; padding-bottom: var(--s-2); margin-bottom: var(--s-4); }
@media (min-width: 640px) {
    .catalog-nav { flex-wrap: wrap; overflow-x: visible; }
    /* Підказка «гортайте» на цій ширині — неправда: ряд перенесено.
       Специфічність 0-2-0 навмисно вища за `.chips-hint`, щоб не залежати від
       порядку правил у файлі (у цьому файлі порядок уже тричі коштував
       розкладки). */
    .catalog-nav-wrap .chips-hint { display: none; }
}
@media (max-width: 639px) {
    .catalog-nav {
        scroll-snap-type: x proximity; scrollbar-width: thin;
        mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
        /* Та сама причина, що в .preset-chips: чіп «Анемостати» у фокусі на
           390 px лишався за згасанням (80 з 137 px, останній — 33 з 244). */
        scroll-padding-inline: 0 28px;
        padding-inline-end: 28px;
    }
    .catalog-nav-chip { scroll-snap-align: start; }
}
/* ПАЛЕЦЬ ТОЙ САМИЙ НА БУДЬ-ЯКІЙ ШИРИНІ, тож підлога тап-цілі тут без
   медіазапиту. Чіп розділу — це ВКЛАДКА каталогу за призначенням (на телефоні
   вона і є навігація розділами), а для вкладок, розкривачів і DXF у нас уже
   стоїть межа 40 px — просто чіпів не було в переліку, який її стереже.
   Заміряно 16.09 на 390 px: чіпи 31 px при ряді, який ще й ГОРТАЄТЬСЯ вбік,
   тобто промах по цілі означає не «нічого», а прокрутку. Це та сама пастка,
   що вже записана в CLAUDE.md, — сторож стояв нерухомо в переліку елементів,
   а не в ширині.
   inline-flex потрібен, щоб min-height центрував текст: на інлайновому <a>
   висота рахується від рядка, і межа нічого б не змінила. */
.catalog-nav-chip {
    flex: 0 0 auto; font-size: var(--t-sm); padding: 0.3rem 0.7rem;
    display: inline-flex; align-items: center; gap: 0.35rem; min-height: 40px;
    background: var(--c-surface-sunk); color: var(--c-text-soft);
    border: 1px solid var(--c-border); border-radius: var(--r-pill); cursor: pointer;
}
.catalog-nav-chip:hover { border-color: var(--c-border-strong); color: var(--c-text); }
.catalog-nav-chip .catalog-count { font-size: var(--t-xs); }
.catalog-group { margin-bottom: var(--s-8); }
.catalog-group h2 { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-4); font-size: var(--t-md); }
/* Лічильник родин у заголовку й у чипі розділу — «пігулка» з числом.
   Дужка тут колись лишилась незакритою, і Chromium прочитав це ЯК ВКЛАДЕНІСТЬ
   (CSS Nesting): правило нижче стало `.catalog-count .catalog-group-split`,
   тобто перестало збігатися взагалі. Заміряно 10.09: уточнення групи
   рендерилось 14,17 px кольором основного тексту вагою 650 — тобто рівно як
   другий заголовок, проти якого й написано коментар нижче. Зламану дужку не
   видно ані в збірці, ані в лінтері: невалідний CSS браузер просто ковтає. */
.catalog-count {
    font-size: var(--t-xs); font-weight: 650; color: var(--c-muted);
    background: var(--c-surface-sunk); border: 1px solid var(--c-border);
    border-radius: var(--r-pill); padding: 0 0.45rem;
}

/* Уточнення групи каталогу («— 4 у конфігураторі «Решітка», 2 в аксесуарах»):
   тонше й тихіше за сам заголовок — це пояснення числа, а не другий заголовок. */
.catalog-group-split {
    margin-left: var(--s-2); color: var(--c-muted);
    font-size: var(--t-xs); font-weight: 400;
}
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--s-3); }
/* Сторінка власника /accounts і блок «Від дилера» в кабінеті (1.0.0). */
.accounts-table input { width: 100%; min-width: 7rem; }
.accounts-table .qty-input { min-width: 5rem; }
.account-row-meta { color: var(--c-muted); font-size: var(--t-xs); margin-top: 0.15rem; }
.account-row-note { margin-top: var(--s-1); }
.account-dealer { border: 1px solid var(--c-border); border-radius: var(--r-md); padding: var(--s-3) var(--s-4); margin: 0 0 var(--s-4); background: var(--c-surface); }
.account-dealer h2 { font-size: var(--t-md); margin: 0 0 var(--s-2); }
.account-dealer p { margin: 0 0 var(--s-1); }
.account-owner-links { display: flex; gap: var(--s-2); flex-wrap: wrap; margin: 0 0 var(--s-3); }
.account-benefits { margin: 0 0 var(--s-3); padding-left: 1.2rem; }
.mkt-owner-links { margin: 0 0 var(--s-3); }
.order-terms { margin-bottom: var(--s-3); }
.catalog-card {
    display: grid; gap: 0.15rem;
    padding: var(--s-4);
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-md); box-shadow: var(--sh-xs);
    text-decoration: none; color: var(--c-text);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.catalog-card:hover { border-color: var(--c-brand); box-shadow: var(--sh-md); transform: translateY(-1px); color: var(--c-text); }
/* Мініатюра картки каталогу — той самий .quote-thumb, що в КП, лише вписаний
   у сітку картки: фіксована висота вирівнює ряд, схема без габаритів чесно
   лишає порожньо (компонент нічого не рендерить). Прямі нащадки svg — та
   сама пастка вкладених видів, що .preview > svg. */
.catalog-thumb { min-height: 84px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
/* Позначка «образу немає» на плитці каталогу: тихий знак, а не порожня клітка
   (29 порожніх із 60 на запиті «100», замір 09.09). Розмір і колір навмисно
   слабші за справжню мініатюру — плитка зі знімком мусить лишатись помітнішою. */
.quote-thumb-none { display: flex; color: var(--c-muted); opacity: .35; }
.quote-thumb-none svg { width: 40px; height: 40px; }

/* Каталог «Монтую» (1.0.0): фото — першим планом і більше. Де фото немає,
   схема лишається; заглушка «габаритів прайс не друкує» теж. */
.catalog-photos .catalog-thumb { min-height: 140px; }
.catalog-photos .catalog-thumb .drawing-photo { grid-template-rows: 136px; }
.catalog-photos .catalog-thumb .quote-thumb > svg { max-height: 136px; }

/* Телефон: дві картки в ряд, а не одна — 34 родини в одну колонку давали
   сторінку на 11 тис. px (скриншот 09.09 на 390 px); фото й так менше ряду.

   Блок стоїть НИЖЧЕ базових .catalog-card і .catalog-photos навмисно: коли
   він лежав вище, з чотирьох його правил працювало ОДНЕ (сітка), а падінг
   картки й висота мініатюри лишались десктопними — специфічність однакова,
   тож перемагало те, що нижче. Ціна мовчазної поразки заміряна 10.09: сторінка
   каталогу на 390 px була на 1147 px довша, ніж хотів автор, і кожна картка
   з'їдала 8 px ширини під назву родини. */
@media (max-width: 719px) {
    .catalog-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-2); }
    .catalog-card { padding: var(--s-3); }
    .catalog-photos .catalog-thumb { min-height: 96px; }
    .catalog-photos .catalog-thumb .drawing-photo { grid-template-rows: 92px; }
}
.catalog-nav-chip.catalog-nav-active { border-color: var(--c-brand); color: var(--c-brand); }
/* Велике фото у вкладці «Фото» конфігуратора («Монтую»). */
/* Велике фото на сторінці виробу — те саме правило, лише вище полотно:
   висота задається РЯДКОМ ґріда, а не max-height картинки, інакше знімок
   1000 px знову був би вчетверо вищим за знімок 100 px. */
.drawing-photo-large { grid-template-rows: var(--preview-large-h) auto; }

/* Текстова специфікація креслення в мініатюрі каталогу дублює картку і
   роздуває її — в КП вона доречна (документ), тут лише схема. */
/* Рядок специфікації під кресленням — лише там, де є місце його прочитати:
   у КП і на сторінці виробу. У мініатюрах (каталог, картка підбору) він або
   лізе поверх малюнка, або витісняє його зовсім — саме це й сталося, щойно
   .variant-thumb з'явився з правилом тільки для каталогу. Перелік контекстів
   тримається ОДНИМ рядком: третій сурогат цього правила означав би, що
   наступна поверхня з мініатюрою знову покаже текст замість образу. */
.catalog-thumb .drawing-spec, .variant-thumb .drawing-spec, .grilles-thumb .drawing-spec { display: none; }
/* Підписи схем у мініатюрі — ГЕТЬ, і це не смак: кегль 7.5 юніт при
   масштабі мініатюри дає ~2,5 px, а браузер мовчки піднімає його до свого
   мінімуму — текст росте ВІДНОСНО геометрії, розповзається за кадр виду і
   правим краєм обрізається контейнером (кліп «Переріз А-А…», борг 18.08,
   доведено елемент-знімками ART-1 до/після). Нечитабельний текст у 84-px
   мініатюрі — шум; геометрія без нього читається як іконка. КП-мініатюр
   (.quote-thumb поза каталогом) не чіпаємо — там масштаб більший.  */
.catalog-thumb .drawing text { display: none; }
.catalog-thumb .quote-thumb { max-width: 200px; margin: 0; }
/* Тільки ЗОВНІШНІЙ svg креслення. Селектор «> * > svg» діставав ще й ВКЛАДЕНІ
   види (DwgView малює кожен вид власним <svg>), і width:100% перебивав їхні
   власні x/width: другий вид (переріз А-А) виїжджав за правий край плитки й
   обрізався — на картках ART-1/1N/2/1F від нього лишалась сама вертикальна
   риска. Заміряно в браузері: плитка 286 px, вкладений вид 241…290. */
.catalog-thumb .quote-thumb > svg { width: 100%; height: auto; display: block; max-height: 84px; }
.catalog-code { font-weight: 700; font-size: var(--t-sm); color: var(--c-brand); font-variant-numeric: tabular-nums; }
.catalog-name { font-size: var(--t-sm); line-height: 1.35; }
.catalog-size { font-size: var(--t-xs); color: var(--c-muted); font-variant-numeric: tabular-nums; }
.catalog-price { font-size: var(--t-sm); margin-top: var(--s-1); font-variant-numeric: tabular-nums; }
/* Бейджем, а не сірим курсивом: «на прорахунку» — змістовна відмінність картки
   (ціну дасть виробник), а дрібний текст читався як другорядна примітка і
   губився при скануванні каталогу (зовнішній UX-аудит 03.08). Палітра — та
   сама, що в .price-card.quote: один стан — один колір усюди. */
/* Чому позиція у видачі: збіг через транслітерацію чи одруживку інакше читається
   як випадковість. Дрібним і приглушеним — це довідка, а не назва. */
.catalog-why {
    font-size: var(--t-xs); color: var(--c-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.catalog-why:empty { display: none; }

.catalog-noprice {
    display: inline-block; padding: 0.1rem 0.5rem;
    font-size: var(--t-xs); font-style: normal; font-weight: 600;
    color: var(--warn-600); background: var(--warn-50);
    border: 1px solid color-mix(in srgb, var(--warn-600) 30%, transparent);
    border-radius: var(--r-pill);
}
.series-note { margin-top: var(--s-2); }
.series-note > summary {
    cursor: pointer; color: var(--c-muted); font-size: var(--t-xs);
    list-style: none;
}
.series-note > summary::-webkit-details-marker { display: none; }
.series-note > summary::before { content: "▸ "; }
.series-note[open] > summary::before { content: "▾ "; }
.series-note .face-note { display: block; margin-top: var(--s-1); }
.preview-schematic-note {
    margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); line-height: 1.35;
}
.catalog-cat { font-size: var(--t-xs); color: var(--c-muted); }
.aero-source { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); line-height: 1.4; }
/* Каталожні характеристики читаються як тверде число, оцінка — як застереження. */
.aero-source-catalogue { color: var(--ok-600); }

/* Позначка походження біля КОЖНОЇ величини: паспорт TEZ і інженерна оцінка
   законно живуть в одній картці, тож розрізняти їх мусить кожен рядок. */
.aero-badge {
    display: inline-block; margin-left: var(--s-1); padding: 0 var(--s-1);
    border: 1px solid transparent;
    border-radius: var(--r-sm); font-size: var(--t-2xs); font-weight: 600;
    text-transform: none; letter-spacing: .02em; vertical-align: middle;
}
/* Фон бейджа — СУЦІЛЬНИЙ токен, а не домішка до прозорого.

   Доти обидва бейджі малювались як color-mix(... , transparent), тобто їхній
   фактичний фон залежав від того, ЩО ПОЗАДУ, — а з ним і контраст тексту.
   Заміряно 20.08: «каталог» на картці (--c-surface) дає 4.65, а на фоні сторінки
   (--c-bg) уже 4.36 при потрібних 4.5 — тобто той самий бейдж читабельний в
   одному місці й ні в іншому. Axe це показав аж на сторінці «Звідки числа»: на
   /select і / бейджі не потрапляли у свіп, бо картки там з'являються лише після
   введення витрати.

   Тепер контраст детермінований і не залежить від батька: «каталог» 5.15 у
   світлій темі й 9.03 у темній, «оцінка» 5.69 і 6.16. Рамка — щоб тихий бейдж
   оцінки лишався бейджем на білій картці, а не плямою фону. */
.aero-badge-cat {
    background: var(--ok-50); color: var(--ok-600);
    border-color: color-mix(in srgb, var(--ok-600) 30%, transparent);
}
.aero-badge-est {
    background: var(--c-surface-sunk); color: var(--c-muted);
    border-color: var(--c-border);
}
.aero-envelope { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); font-variant-numeric: tabular-nums; }
/* Рядок «звідки саме число»: той самий рівень, що .aero-envelope — це не
   застереження (issue) і не футер, а цитата другої таблиці ТОГО САМОГО каталогу.
   Виділити його сильніше означало б натякнути, що крива менш надійна за таблицю. */
.aero-detail-note { margin: var(--s-1) 0 0; font-size: var(--t-xs); color: var(--c-muted); line-height: 1.4; }

/* ── Пленум-бокс PB (макет ТЗ 4.1.1) ──
   Та сама рамка, що в ADPI-картки: це ще одна картка правої колонки, а не
   окремий блок сторінки. Таблиця — .dim-table, як на вкладці «Розміри»
   решітки: одна мова для «літера ↔ число» на всіх сторінках виробів. */
.plenum-card { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--s-4); margin-top: var(--s-3); }
.plenum-card h2 { font-size: var(--t-md); margin: 0 0 var(--s-3); }
.plenum-card .drawing-scroll { margin-top: var(--s-3); }
/* Той самий мінімум, що в прев'ю (.preview .drawing-scroll .drawing): без
   нього на 390 креслення стискалось до 324 px, і підписи «Ø 125», «L 1002»
   падали нижче читабельного (заміряно першим знімком зрізу S09). */
.plenum-card .drawing-scroll .drawing { min-width: 450px; }

/* ── Пленум-бокси №1–3 решітки АРТ (задача 15, макети 4–4.1.1) ──
   Вкладка «Аксесуари»: картки боксів зі схемою і під-вид бокса (.plenum-view).
   Класи власні, а не .plenum-card: та — секція LDT зі своїм мобільним order і
   своїм min-width креслення.
   Креслення тут БЕЗ min-width і без прокрутної обгортки: кадр GrillePlenumPreview
   420 одиниць (а не 560), тож на 390 px підпис ≈ 9,3 px і так (знімок і
   обхід підписів — grille-plenum.spec, 390 і 1536). */
.plenum-types-title, .plenum-view-title { font-size: var(--t-md); margin: 0; }
.plenum-types { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
/* Уся картка клікабельна розтягнутим ::after посилання — ПАРОЮ з
   позиціонованою карткою (CLAUDE.md: без прив'язки до картки накриття
   розповзається на сусідів). */
.plenum-type-card {
    position: relative; display: grid; gap: var(--s-2);
    background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg);
    padding: var(--s-3) var(--s-4);
}
.plenum-type-card:hover { border-color: var(--c-brand); }
.plenum-type-card .plenum-type-link { font-weight: 650; }
.plenum-type-card .plenum-type-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.plenum-type-card .plenum-drawing { max-width: 360px; }
.plenum-view { display: flex; flex-direction: column; gap: var(--s-3); }
/* «Додати пленум-бокс у специфікацію» — довгий підпис кнопки (.btn — nowrap): на 200 %
   тексту він один штовхав сторінку вбік (хвиля 3, M-3a: +120 px на 1280 і +119 на 390 з
   Inter, +143 і +143 з DejaVu — proofs/t16-estimate-tail/w3-200text-probe.txt).
   Переноситься, як кнопки дій /quote (.quote-page .actions > .btn-ghost). Межі ширини
   (max-width) тут немає свідомо: перенесений підпис і так не ширший за колонку
   (мутація «без max-width» лишилась зеленою — мертва межа, m125). А сторож міряє ще й
   ОБРІЗАННЯ: кнопка, якій ширину обмежено, підпис не переносить, а ріже (заміряно з
   nowrap під max-width: 78 px «специфікацію» за краєм на 1280, m124). */
.plenum-add { white-space: normal; text-align: center; }

/* ── Категорія /grilles (задача 13; спек §6.1 — клон категорії KO «Air grilles») ──
   Квадратні картки-посилання: мініатюра зверху, смуга з назвою знизу; наведення —
   тінь (як .catalog-card). Мініатюра — .quote-thumb, тобто БЕЗ підписів і розмірних
   ліній (правила вище): у картці ~200 px кегль креслення дав би ≈ 6 px, а числа
   бокса живуть на під-виді, куди картка веде. Svg вписується в квадрат цілком
   (height: 100% при viewBox — «meet»), тож широка схема решітки стоїть смугою
   посередині, як фото в картці KO. */
.grilles-grid {
    list-style: none; margin: 0 0 var(--s-5); padding: 0;
    /* Рівно чотири в ряд, як у KO (чотири серії — один ряд): auto-fill на 768 давав
       три й лишав АРТ-1F саму в другому ряду (знімок 14.10). */
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-3);
}
.grilles-grid > li { display: flex; min-width: 0; }
.grilles-card {
    flex: 1; min-width: 0; aspect-ratio: 1 / 1;
    display: grid; grid-template-rows: minmax(0, 1fr) auto;
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-md); box-shadow: var(--sh-xs); overflow: hidden;
    text-decoration: none; color: var(--c-text);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.grilles-card:hover { border-color: var(--c-brand); box-shadow: var(--sh-lg); color: var(--c-text); }
.grilles-thumb { min-height: 0; display: flex; align-items: center; justify-content: center; padding: var(--s-3); }
.grilles-thumb .quote-thumb { width: 100%; height: 100%; max-width: none; margin: 0; }
.grilles-thumb .quote-thumb > svg { width: 100%; height: 100%; display: block; }
.grilles-thumb .drawing-photo figcaption { display: none; }
.grilles-band {
    padding: var(--s-2) var(--s-3);
    background: var(--c-surface-sunk); border-top: 1px solid var(--c-border);
    font-size: var(--t-sm); font-weight: 650; line-height: 1.3;
}
.grilles-card:hover .grilles-band { color: var(--c-brand); }
.grilles-section-title { font-size: var(--t-lg); margin: var(--s-5) 0 var(--s-3); }
.grilles-plenum-note { max-width: 72ch; margin-top: calc(-1 * var(--s-3)); }
/* Телефон: дві картки в ряд (як каталог) — одна квадратна картка на всю ширину
   дала б сторінку з семи екранів. */
@media (max-width: 719px) {
    .grilles-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-2); }
    .grilles-thumb { padding: var(--s-2); }
    .grilles-band { font-size: var(--t-xs); }
}
.plenum-back, .plenum-source, .plenum-order-code, .plenum-actions { margin: 0; }
.plenum-source { font-size: var(--t-xs); color: var(--c-muted); }
.plenum-view .plenum-size .field { margin-bottom: 0; }

/* ── ADPI-вердикт ── */
.adpi-card { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--s-4); margin-top: var(--s-3); }
.adpi-card h2 { font-size: var(--t-md); margin: 0 0 var(--s-3); }
.adpi-verdict { margin: var(--s-2) 0 0; font-size: var(--t-sm); line-height: 1.45; font-variant-numeric: tabular-nums; }
.adpi-comfort { color: var(--ok-600); }
/* --warn-700 тепер справжній токен (обидві теми) — фолбек-хардкод не адаптувався. */
.adpi-tooshort, .adpi-toolong { color: var(--warn-700); }
.adpi-source { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--c-muted); line-height: 1.4; }

/* ── Панель «Приміщення»: розумний вхід витрати ── */
.room-panel { border: 1px dashed var(--c-border-strong); border-radius: var(--r-md); padding: var(--s-2) var(--s-3); margin-bottom: var(--s-4); background: color-mix(in srgb, var(--c-brand) 4%, transparent); }
.room-panel summary { cursor: pointer; display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: baseline; font-size: var(--t-sm); }
.room-panel summary::-webkit-details-marker { display: none; }
.room-title { font-weight: 600; color: var(--c-brand); }
.room-panel summary small { color: var(--c-muted); }
/* align-items:end — як у .field-row: поле з дворядковим підписом («Висота, м
   (порожньо = 2.7)») зсувало СВІЙ інпут нижче за сусідні, і ряд читався
   зламаним (UI/UX-огляд 03.08). */
.room-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--s-3); margin-top: var(--s-3); align-items: end; }
.room-grid .field { margin-bottom: 0; }
/* Тип приміщення — на весь рядок: у третині панелі селект різав найдовшу
   опцію до «Житлова кімна…», а вибраного типу не було видно повністю. */
.room-grid .field:first-child { grid-column: 1 / -1; }
.room-result { margin-top: var(--s-2); }
.room-flow { margin: 0 0 var(--s-1); font-size: var(--t-sm); }
/* Свій маркер уже стоїть у тексті кожного рядка («✓» — вимога, що визначила
   результат; «·» — решта), тож дефолтний буліт давав подвійний: «• ✓ На людей…». */
.room-notes { margin: 0 0 var(--s-2); padding-left: 0; list-style: none; font-size: var(--t-xs); color: var(--c-muted); line-height: 1.5; }

/* ── Підбір: розкладка, фільтри ── */
.units-note { display: block; margin: 0 0 var(--s-3); }

/* ── Проєкт об'єкта (мультикімнатний) ── */
.project-room {
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-lg); box-shadow: var(--sh-sm);
    padding: var(--s-4); margin-bottom: var(--s-4); max-width: 980px;
}
.project-room .field-row .field { margin-bottom: var(--s-2); }
.project-room-fields { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); column-gap: var(--s-3); }
.project-room-result { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin-top: var(--s-2); }
.project-airflow { font-size: var(--t-sm); color: var(--c-text-soft); }
.project-airflow strong { font-size: var(--t-lg); color: var(--c-text); }
.project-airflow-empty { color: var(--c-muted); }
.project-remove { margin-left: auto; color: var(--c-muted); }
.project-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-4); margin-top: var(--s-4); }
.project-total { margin: 0; }
/* Що вже в Пропозиції під приміщенням (зріз 3): рядок під результатом кімнати.
   Кольори бейджів — токени станів (ok/warn), темна тема успадковується сама. */
.project-quote-load { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-text-soft); display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.project-load-badge { border-radius: var(--r-pill); font-size: var(--t-xs); font-weight: 650; padding: 0.05rem 0.5rem; }
/* Пояснення балансу об'єкта. Раніше обидва тексти жили в title, тобто на дотику
   не існували; тепер вони в потоці — і мусять читатись як примітка, а не як ще
   один рядок підсумку. */
.project-balance-note { font-size: var(--t-xs); color: var(--c-muted); margin: var(--s-2) 0 0; max-width: 68ch; }
.project-balance-warn { color: var(--warn-700); }
.project-balance-why { margin-top: var(--s-2); }
.project-balance-why summary {
    font-size: var(--t-xs); color: var(--c-muted); cursor: pointer;
    width: fit-content; padding: 0.1rem 0;
}
.project-balance-why summary:hover { color: var(--c-text-soft); }

/* «Як пораховано?» — розгортка правил норми під числом приміщення. Доти вона
   жила в title, тобто на дотику не існувала, хоч це обґрунтування числа, яке
   веде весь проєкт. */
.project-advice { margin-top: var(--s-2); }
.project-advice summary {
    font-size: var(--t-xs); color: var(--c-muted); cursor: pointer;
    width: fit-content; padding: 0.1rem 0;
}
.project-advice summary:hover { color: var(--c-text-soft); }
.project-advice-notes {
    margin: var(--s-2) 0 0; padding-left: var(--s-4);
    font-size: var(--t-xs); color: var(--c-text-soft); max-width: 68ch;
}
.project-advice-notes li + li { margin-top: var(--s-1); }
.project-load-ok { background: color-mix(in srgb, var(--ok-600) 14%, transparent); color: var(--ok-600); }
/* Текст — warn-700, НЕ warn-600: на 14%-підмісі warn-600 дає 3,87:1 (axe
   serious; зріз 14 — бейдж дефіциту припливу перший потрапив під axe, але
   битими були ВСІ badge-part). Темна тема має власний warn-700. */
.project-load-part { background: color-mix(in srgb, var(--warn-600) 14%, transparent); color: var(--warn-700); }
/* Звіт автопідбору: що саме додано (або чому ні) — на весь рядок під кнопками. */
.project-auto-note { flex-basis: 100%; color: var(--c-muted); }
/* Місток панелі приміщення в дугу об'єкта. */
.room-project-link { margin: var(--s-3) 0 0; font-size: var(--t-sm); color: var(--c-muted); }
/* Швидкий старт порожнього проєкту: підпис + кнопки шаблонів у ряд. */
.project-templates { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-4); }
.project-templates-label { color: var(--c-muted); font-size: var(--t-sm); }

/* Табличний режим видачі /select (порівняння й пресети прибрано в 1.0.0). */
.select-view-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2); margin: var(--s-3) 0 var(--s-2); }
.select-view { display: inline-flex; gap: 2px; }
.select-table th { padding: 0; }
.select-sort {
    all: unset; cursor: pointer; display: block; width: 100%; box-sizing: border-box;
    padding: 0.45rem 0.6rem; font-weight: 650; white-space: nowrap;
}
.select-sort:hover { color: var(--c-brand); }
.select-sort:focus-visible { outline: 2px solid var(--c-brand); outline-offset: -2px; }
.select-sort-on { color: var(--c-brand); }
.select-row-actions { display: flex; gap: var(--s-1); }
/* Нижня межа діапазону — 640 px: нижче неї таблиця вже НЕ таблиця (картки
   .quote-cards), і min-width лише виштовхував колонки за екран — заміряно
   10.09 на 390 px: ΔP не видно взагалі (100 % ширини), «Швидкість» зрізана
   на 83 %, кнопка «У специфікацію» на 39 %. */
/* Кнопка «В обрані» ЗАВЖДИ в екрані — навіть коли таблиця гортається вбік.
   Заміряно 10.09: у табличному виді видачі таблиця має 709 px при контейнері
   358, і кнопка додавання стоїть на x=639, тобто на 281 px за правим краєм.
   Смуга прокрутки всередині картки на телефоні не малюється, тож ЄДИНА дія
   рядка була невидима й недосяжна — користувач бачив перелік виробів, який
   нічого не робить. Липка колонка лишає дію на місці, а решту таблиці
   гортають під нею; на широкому екрані таблиця вміщається й липкість не
   вмикається взагалі. */
@media screen and (max-width: 639px) {
    /* `overflow: hidden` на самій таблиці (воно там заради заокруглених кутів)
       робить ТАБЛИЦЮ контейнером прокрутки для sticky — і липкість рахується
       від неї, а таблиця не гортається. Перша редакція цього правила через це
       не робила нічого: кнопка лишалась на x=639 (заміряно). */
    .select-table { overflow: visible; }
    .select-table td:last-child, .select-table th:last-child {
        position: sticky; right: 0; z-index: 1;
        /* Непрозоре тло обов'язкове: під липкою колонкою проїжджають числа
           сусідніх колонок, і без фону вони проступали б крізь кнопку. */
        background: var(--c-surface);
        box-shadow: -8px 0 8px -8px rgba(19, 25, 32, .28);
    }
    .select-table th:last-child { background: var(--c-surface-sunk); }
    /* Фокус Tab-ом гортає таблицю до заголовка — але липка колонка дій
       (146 px на 390, заміряно) накривала його: «ΔP» і «Дальн.» у фокусі були
       видимі на 0 px зі 103 (клавіатурний прохід 30.09). Відступ прокрутки —
       ширина липкої колонки з запасом. */
    .table-scroll:has(> .select-table) { scroll-padding-inline-end: 160px; }
}

@media (min-width: 640px) and (max-width: 719px) { .select-table { min-width: 640px; } }
/* Вихід із «0 результатів»: кнопки зняття обмежень (транш 2). */
.relax-row { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-2); }
/* margin-top:auto — кнопки притиснуті до низу картки: у сітці картки одного
   ряду однакової висоти (stretch), але різне перенесення підписів лишало
   кнопки на різних рівнях — ряд виглядав розхристаним і заважав порівнювати
   картки очима по горизонталі (UI/UX-прохід 18.08). */
.variant-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; margin-top: auto; }
/* Другий захід тієї ж пастки (борг п. 1): auto тримав рівними ЛИШЕ кнопки, а
   смуга «Тихо/Норма» лишалася плавати одразу під dl — заміряний розкид у ряді
   сягав 36 px саме від перенесення підписів. Якір переїжджає НА смугу (вона в
   картці завжди є), кнопки йдуть за нею впритул: обидва низові блоки рівні по
   всьому ряду. Обидва auto лишати не можна — вільне місце поділилося б між
   двома проміжками порівну, і смуга зависла б посередині картки. */
.variant-card .aero-rating { margin-top: auto; }
.variant-card .aero-rating + .variant-actions { margin-top: 0; }
/* Довгий типоряд: рядок — це ДВІ клітинки, тож ціна не зникає першою. Доти тут
   стояв `<select size=8>`, а `<option>` не переноситься й не має «…» — зрізало
   мовчки, і першою йшла ціна (чому саме так і скільки коштувало — у коментарі
   розмітки AccessoryPage). Вікно ≈ вісім рядків: рівно стільки показував той
   селект, тобто список не став ані довшим, ані коротшим за звичний. */
.model-list {
    max-height: 16rem; overflow-y: auto;
    background: var(--c-surface);
    border: 1px solid var(--c-border-strong); border-radius: var(--r-sm);
}
/* position: relative — не косметика: радіо всередині сховане канонічним
   .visually-hidden (а це position: absolute), і без позиціонованого рядка
   браузер прокручував би до фокуса В ІНШЕ МІСЦЕ сторінки — тобто забрав би
   рівно те, заради чого тут узяте нативне радіо. */
.model-row {
    position: relative;
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s-2); align-items: baseline;
    padding: 0.35rem 0.6rem; cursor: pointer;
    border-top: 1px solid var(--c-border);
}
.model-row:first-child { border-top: none; }
/* Радіо невидиме, але РОЗМІРОМ У РЯДОК, а не канонічним 1×1 .visually-hidden.
   Причина заміряна: браузер прокручує до ФОКУСОВАНОГО елемента, тож із точкою
   1×1 у лівому верхньому куті рядок на 80 px (довга назва на 390 px — це чотири
   рядки тексту) вилазив за вікно списку на 36 px, і хвіст назви лишався за
   кадром до ручної прокрутки. pointer-events: none — щоб клік ішов у label
   (він однаково перемикає радіо), а не в прозору накривку. */
.model-row input {
    position: absolute; inset: 0; width: 100%; height: 100%;
    margin: 0; opacity: 0; pointer-events: none;
}

.model-row:hover { background: var(--c-surface-sunk); }
/* БЕЗ font-weight: вибір не сміє міняти РОЗКЛАДКУ. Заміряно: з 650 назва
   «KIT RC HTB (пульт дистанційного керування)» на 390 px перескакувала в
   зайвий рядок і сам рядок ріс 57 → 80 px — але вже ПІСЛЯ того, як браузер
   прокрутив до нього, тож обраний стрілкою рядок вилазив за вікно списку на
   21 px. Вибір несуть підкладка, колір і `checked` самого радіо. */
.model-row-active { background: var(--c-brand); color: var(--c-on-brand); }
/* Фокус видно НА РЯДКУ: сам інпут схований, тож інакше обхід стрілками з
   клавіатури був би невидимим. */
.model-row:has(input:focus-visible) { outline: 2px solid var(--c-focus); outline-offset: -2px; }
/* minmax(0, 1fr) вище + перенос тут: довга назва йде В ДРУГИЙ РЯДОК, а не під ніж.
   Ціна не переноситься ніколи — вона й є те, що доти зникало. */
.model-name { min-width: 0; overflow-wrap: anywhere; }
.model-price { white-space: nowrap; }
.size-filter { width: 100%; margin-bottom: var(--s-2); padding: 0.45rem 0.7rem; font: inherit; color: var(--c-text); background: var(--c-surface); border: 1px solid var(--c-border-strong); border-radius: var(--r-sm); }
.qty-input { width: 4.5rem; padding: 0.25rem 0.4rem; font: inherit; text-align: right; color: var(--c-text); background: var(--c-surface); border: 1px solid var(--c-border-strong); border-radius: var(--r-sm); }
@media print { .no-print-input { display: none; } }
/* Номер документа під заголовком КП: помітний, але не кричить — його диктують
   у телефон, тож головне читабельність, а не вага. */
.quote-doc-number { margin: 0 0 var(--s-3); color: var(--c-muted); }
.quote-doc-number small { margin-left: var(--s-2); }
/* Умови (ПДВ, строк дії) і підпис автора — вигляд той самий, що в примітки про
   прайс, але КЛАС власний: «.quote-note» означає рівно одну примітку, і другий
   елемент із тим самим класом одразу поклав чотири e2e на strict mode. */
.quote-doc-terms,
.quote-doc-author { color: var(--c-muted); font-size: var(--t-xs); margin-top: var(--s-2); }
.quote-doc-terms span { display: block; }

.discount-field { max-width: 340px; margin-top: var(--s-3); }
/* На телефоні стеля знімається: заміряно 10.09 — поле 340 px проти 358 у
   сусідніх, тобто 18 px рваного краю рівно посередині документа. Правило
   мусить стояти ПІСЛЯ базового (специфічність та сама, перемагає порядок) —
   перша редакція лежала у мобільному блоці КП вище й була мертва. */
/* 639, а не 640: саме на цій межі документ стає картками (усі .quote-cards),
   і поле мусить розширюватись РАЗОМ із ними, а не на піксель раніше. Дві
   сусідні межі для одного наміру — це те, через що потім довго шукають
   «чому на 640 виглядає інакше». (Два старіші 640 у цьому файлі — не мої
   й не цього зрізу.) */
@media screen and (max-width: 639px) { .discount-field { max-width: none; } }
/* Підпис джерела знижки: підставлену з профілю видно як підставлену, а не як
   набрану — мовчазна зміна суми документа неприпустима (зріз 41). */
.discount-from-profile { display: block; margin-top: var(--s-1); color: var(--c-muted); }
.quote-discount-row td { color: var(--c-muted); }

/* === Доступність ========================================================= */
/* Перший таб-стоп: без нього кожна сторінка починалася з 14 пунктів навігації
   (WCAG 2.4.1). Видимим стає лише у фокусі. */
.skip-link {
    position: absolute; left: -9999px; top: var(--s-3); z-index: 100;
    padding: var(--s-2) var(--s-4); border-radius: var(--r-sm);
    background: var(--c-brand); color: var(--c-on-brand); font-weight: 650;
    text-decoration: none;
}
.skip-link:focus-visible { left: var(--s-4); outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* Текст для скрін-рідера, невидимий на екрані (той самий clip-патерн, що в
   прихованому файловому інпуті). !important — не з розпачу: клас надягають і
   на FORM-елементи (textarea-буфер листа в QuoteRequestPanel), а видові
   правила форм (.quote-request textarea { width:100% }) специфічніші за
   одинарний клас — так «прихований» буфер ставав 390px завширшки, і БУДЬ-ЯКА
   мобільна КП з позицією скролилась убік (заміряно живим DOM 31.08: документ
   405 px на 390-екрані; сторож циклу responsive цього не бачив — він ходить
   по порожній КП). Канонічні sr-only пишуться з !important рівно тому. */
.visually-hidden {
    position: absolute !important; width: 1px !important; height: 1px !important;
    padding: 0 !important; margin: -1px !important;
    overflow: hidden !important; clip: rect(0 0 0 0) !important;
    white-space: nowrap !important; border: 0 !important;
}

/* Паспортний діапазон моделі до введення витрати — головна відповідь на «що ця
   решітка витягне», яку раніше давали лише постфактум. */
.passport-hint {
    margin: 0 0 var(--s-4); padding: var(--s-3);
    font-size: var(--t-sm); color: var(--c-text-soft);
    background: var(--c-surface-sunk); border-radius: var(--r-sm);
    border-left: 3px solid var(--ok-600);
}
.passport-hint small { color: var(--c-muted); }

/* === Головна за ролями ==================================================== */
/* Дві рольові картки — головний вибір сторінки, тож вони мусять читатись як
   рівноцінні: однакова ширина, однакова висота дій унизу (тому auto-fit і
   flex-колонка з margin-top:auto на .role-actions). З 1.0.0 картка — це
   GET-форма з однією великою кнопкою режиму. */
.role-grid {
    display: grid; gap: var(--s-4); margin-bottom: var(--s-8);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.role-card {
    display: flex; flex-direction: column; gap: var(--s-3);
    padding: var(--s-5);
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-lg); box-shadow: var(--sh-sm);
}
.role-title { font-size: var(--t-lg); }
/* «З чим ви прийшли» — це те, за чим людина себе впізнає, тож воно стоїть
   ПЕРЕД кнопкою. */
.role-have { margin: 0; color: var(--c-text-soft); }
.role-actions {
    display: flex; flex-wrap: wrap; gap: var(--s-2);
    margin-top: auto; padding-top: var(--s-2);
}
/* Кнопка режиму — ВЕЛИКА: це головна дія сайту, і на телефоні вона мусить
   бути ціллю для пальця, а не звичайною кнопкою в кутку картки. */
.btn-big { width: 100%; padding: 0.9rem 1.2rem; font-size: var(--t-md); }

/* Екран входу (1.0.0): дві можливості — без реєстрації або зареєструватись. */
.home-entry { margin-bottom: var(--s-8); }
.home-h2 { margin-bottom: var(--s-2); }
.entry-grid {
    display: grid; gap: var(--s-4); margin-bottom: var(--s-3);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.entry-card {
    display: flex; flex-direction: column; gap: var(--s-2);
    padding: var(--s-5);
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-lg); box-shadow: var(--sh-sm);
    text-decoration: none; color: var(--c-text);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.entry-card:hover { border-color: var(--c-brand); box-shadow: var(--sh-md); color: var(--c-text); }
.entry-card-primary { border-color: var(--c-brand); }
.entry-title { font-weight: 700; font-size: var(--t-lg); }
.entry-note { color: var(--c-text-soft); }
.home-quick-note { margin: 0; font-size: var(--t-sm); color: var(--c-muted); }
/* Підвал головної: тихий рядок під розвилкою — посилання далі, знак ТЕЗ і
   версія; на телефоні ланки переносяться, мета-рядок іде під них. */
.home-foot {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--s-3); margin-top: var(--s-8); padding-top: var(--s-4);
    border-top: 1px solid var(--c-border); font-size: var(--t-sm); color: var(--c-muted);
}
.home-foot-links { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); }
.home-foot-links a { color: var(--c-text-soft); }
.home-foot-meta { display: inline-flex; align-items: center; gap: var(--s-3); }
.designed-by-home { font-size: var(--t-xs); }
/* Порожня жива область історії підборів не займає місця (вузол мусить існувати
   ще до повідомлення — LiveRegionTests). */
.quote-history-status:empty { display: none; }
.history-delete-status:empty { display: none; }
/* IFC специфікації (F7b): форма з прихованими полями не має власного блоку —
   її кнопка мусить стояти в тому самому ряду flex, що й сусідні вивантаження,
   тож сама форма «розчиняється» (display: contents). Статус і «Як відкрити в
   Revit» — дрібним тихим текстом під кнопками, як підказки поруч. */
.ifc-form { display: contents; }
.ifc-status { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-text-soft); max-width: 76ch; }
/* Знижка, яку несе «Excel з цінами» специфікації без видимого поля (задача 16):
   той самий тихий рядок під кнопками, що й статус IFC, — підказка до кнопки,
   на яку вона посилається (aria-describedby), а не попередження. */
.export-discount-note { margin: var(--s-2) 0 0; font-size: var(--t-sm); color: var(--c-text-soft); max-width: 76ch; }
.ifc-howto { margin-top: var(--s-2); }
.ifc-howto summary { font-size: var(--t-sm); color: var(--c-muted); cursor: pointer; width: fit-content; padding: 0.1rem 0; }
.ifc-howto summary:hover { color: var(--c-text-soft); }
.ifc-howto ol { margin: var(--s-2) 0 0; padding-left: var(--s-5); font-size: var(--t-sm); color: var(--c-text-soft); max-width: 76ch; }
.ifc-howto li + li { margin-top: var(--s-1); }

/* === /import: вставлена специфікація ======================================
   Двоколонкова панель уводу (вставка | файл) і таблиця розбору. Кольорів тут
   немає — лише токени: сирий колір у цьому файлі заборонений, і одрук в імені
   токена мовчки повертає елемент до успадкування (CssTokenTests). */
.import-panel {
    display: grid; gap: var(--s-4); margin: var(--s-4) 0;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}
@media (max-width: 720px) {
    .import-panel { grid-template-columns: minmax(0, 1fr); }
}

.import-paste textarea {
    width: 100%; font-family: var(--font-mono); font-size: var(--t-sm);
    line-height: var(--lh-base); resize: vertical;
}

/* Пунктирна зона — той самий словник, що на /photo: межа «куди тягнути» має
   читатися до наведення, а не лише під час перетягування. */
.import-drop {
    border: 2px dashed var(--c-border-strong); border-radius: var(--r-md);
    padding: var(--s-3); text-align: center; cursor: pointer;
    transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.import-drop.drop-over { border-color: var(--c-brand); background: var(--c-surface-sunk); }
.import-drop input[type="file"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
/* Кільце фокуса — на видимій «кнопці»: сам input прозорий, і фокус на ньому
   не видно було НІЯК (різниця знімків у фокусі й без — нуль пікселів,
   клавіатурний прохід 30.09). Те саме правило, що .photo-pick на /photo. */
.import-drop input[type="file"]:focus-visible ~ .import-pick {
    outline: 2px solid var(--c-brand); outline-offset: 2px;
}

.import-summary { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: var(--s-3) 0; }
.import-count {
    padding: var(--s-1) var(--s-2); border-radius: var(--r-pill);
    font-size: var(--t-xs); background: var(--c-surface-sunk); color: var(--c-text-soft);
}
.import-count-matched { background: var(--ok-50); color: var(--ok-600); }
.import-count-ambiguous { background: var(--warn-50); color: var(--warn-600); }
.import-count-unrecognized { background: var(--info-50); color: var(--info-600); }

.import-table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.import-table th, .import-table td {
    padding: var(--s-2); border-bottom: 1px solid var(--c-border); vertical-align: top;
    text-align: left;
}
.import-num { color: var(--c-muted); font-variant-numeric: tabular-nums; }
/* overflow-wrap: anywhere, а не break-word: лише «anywhere» зменшує
   min-content клітинки, тобто дозволяє автотаблиці стиснути колонку.
   INSTALLER-3 (аудит 06.10): на 390 px одне довге слово рядка
   («пусконалагодження») розпирало таблицю до 398 px при обгортці 358, і
   колонка «Сума» обрізалась усередині .table-scroll, де ширину документа
   сторож не бачить.
   Правило стоїть на КЛІТИНЦІ, а не на самому тексті: властивість
   успадковується, тож і чипи «чому», і нотатки під рядком перестають бути
   жорсткими. Перша редакція ставила його лише на .import-raw-text, і CI 1235
   (ранер без Inter — DejaVu Sans) дав таблицю 363 px при обгортці 358, хоч
   локально з Inter та сама сторінка давала 354: чип «ізольований» у
   моноширинному 12px — 87,5 px в обох шрифтах (моно той самий), а колонки
   бейджа й суми з DejaVu ширші на 4,5 і 5,1 px. Колонка вільного тексту
   мусить поглинати різницю метрик шрифту ЦІЛКОМ — інакше кожен шрифт
   потребує окремого заміру. Заміряно обома шрифтами 06.10. */
.import-raw { overflow-wrap: anywhere; }
.import-raw-text { display: block; }
.import-price { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* «Чому» — токени рядка, які влучили. Дрібним, але поруч із рядком: це
   пояснення рішення, а не журнал для розробника. */
.import-why { display: block; margin-top: var(--s-1); font-size: var(--t-xs); color: var(--c-muted); }
.import-token {
    display: inline-block; padding: 0 var(--s-1); margin-right: var(--s-1);
    border-radius: var(--r-sm); background: var(--c-surface-sunk); font-family: var(--font-mono);
}
.import-note { display: block; margin-top: var(--s-1); font-size: var(--t-xs); color: var(--warn-700); }

.import-verdict {
    display: inline-block; padding: 0 var(--s-2); border-radius: var(--r-pill);
    font-size: var(--t-xs); white-space: nowrap;
}
.import-verdict-matched { background: var(--ok-50); color: var(--ok-600); }
.import-verdict-ambiguous { background: var(--warn-50); color: var(--warn-600); }
.import-verdict-unrecognized { background: var(--info-50); color: var(--info-600); }
/* «Обрано» — окремий стан від «впізнано»: колір той самий (робити нічого не
   треба), а слово інше, бо відповідальність інша — вибір зробила людина. */
.import-verdict-chosen { background: var(--ok-50); color: var(--ok-600); }
.import-single { display: block; margin-top: var(--s-1); color: var(--c-text-soft); }
/* Ехо обраного варіанта. НЕ приглушене до --c-muted, як «чому»: тут стоїть
   модель і ціна за штуку, тобто те, за чим менеджер звіряє рядок із прайсом —
   у закритому селекті поруч цього не видно (74,7 px на 390 px). Перенос
   дозволений навмисно: рядок довгий, а таблиця мусить лишитись у 390. */
.import-picked {
    display: block; margin-top: var(--s-1); color: var(--c-text-soft);
    font-size: var(--t-xs); white-space: normal; overflow-wrap: anywhere;
}
/* width:100% + min-width:0, а не max-width: селект не сміє ДИКТУВАТИ ширину
   таблиці — його природна ширина дорівнює найдовшій опції («FAN-DUCT ·
   TD-500/… — 14,082.59 ₴»), і max-width:100% у авто-лейауті циклічний
   (100% від колонки, яку розтягнув сам селект): на 390px таблиця займала
   782. Текст закритого селекта кліпає браузер, у розгорнутому списку опції
   видно повністю. Заміряно живим DOM 31.08: 782 → 376. */
.import-choice { display: block; margin-top: var(--s-1); width: 100%; min-width: 0; }

.import-qty-input { width: 5rem; }

/* Вузький екран ділить шість колонок 390 пікселями: типові падінги й поле
   кількості з'їдали ~80 зайвих, а nowrap у ціні («на прорахунку у
   виробника») і бейджі тримав ще ~50. Мета та сама, що в КП (UI/UX-прохід
   18.08, HIGH): ГРОШІ видно без бічного скролу — .table-scroll вище лише
   страховка на екстремальний вміст, не спосіб їх сховати. */
@media (max-width: 640px) {
    .import-table th, .import-table td { padding: var(--s-1); }
    /* 3rem, не 3.5: на 390 px жорсткі колонки (селект «оберіть», поле
       кількості, гроші без переносу) у сумі давали таблицю 362 px при обгортці
       358 — «Сума» ховалась за внутрішнім скролом (заміряно 06.10 на доказовому
       прогоні 1.2.1, INSTALLER-3); чотири цифри кількості в 3rem уміщаються. */
    .import-qty-input { width: 3rem; }
    .import-price, .import-verdict { white-space: normal; }
    /* Бейдж вердикту на телефоні — з вужчими полями (4 px замість 8): з 8 px
       «на прорахунок» займав 90 px, і колонці «Рядок» лишалось менше за
       найширший чип «чому» (87,5 + 4 px відступу) — чип рвався б по літерах
       рівно там, де фіксовану розкладку відкинуто за це саме. Заміряно 06.10
       на DejaVu Sans (шрифт ранера CI): з 4 px «Рядок» дістає ≥ 100 px. */
    .import-verdict { padding: 0 var(--s-1); }
    /* 30 % колонці «Рядок», бо надлишок автотаблиця віддає НЕ їй: щойно
       anywhere на клітинці зробив її min-content ≈ 20 px, Chromium роздав
       358 px селекту «Що це» (99,1) і чекбоксу (32), лишивши рядку 86,8 — чип
       «ізольований» (87,5) порвало на два рядки (висота 36 замість 18) навіть
       з Inter. 30 % = 107 px при 358; коли решта колонок на мінімумах лишає
       менше, колонка бере що є (DejaVu: 102), і це ≥ 99,5, потрібних
       найширшому чипу з відступом. Заміряно обома шрифтами 06.10. */
    .import-raw { width: 30%; }
    /* Порядковий номер на телефоні — найдешевша жертва: без нього ціна
       влазить в екран ЦІЛОЮ (374 ≤ 390 замість 391,5), а порядок рядків
       видно й так. Ховається за КЛАСОМ, а не nth-child: у таблиці
       приміщень «№» — перша колонка, і позиційний селектор зняв би їй
       «Приміщення». Фіксовану розкладку з явними ширинами колонок міряно
       і ВІДКИНУТО: числа зелені, але слова рвались по літерах
       («Решітк-а», «найбли-жчі») — сторож був би зелений, око ні. */
    .import-num { display: none; }
}
.import-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin: var(--s-4) 0; }
.import-added { color: var(--ok-600); }
.import-take { width: 2rem; }
.import-take-input { margin: 0; }

/* === Специфікація та історія підборів (1.0.0) ============================= */
.spec-table .td-article { font-variant-numeric: tabular-nums; white-space: nowrap; }
.spec-table .td-size { color: var(--c-text-soft); white-space: nowrap; }
.quote-back { margin: 0 0 var(--s-3); }
.history-entry {
    margin-bottom: var(--s-3); padding: var(--s-3) var(--s-4);
    background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-md);
}
.history-entry > summary { cursor: pointer; display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-3); align-items: baseline; }
.history-title { font-weight: 650; }
.history-meta { color: var(--c-muted); font-size: var(--t-sm); }
.history-actions { margin-top: var(--s-3); }

/* === Режим примусових кольорів (Windows High Contrast) =====================
   A11Y-MOBILE-1, аудит 06.10: у CSS не було ЖОДНОГО правила під цей режим.
   Браузер у ньому підміняє кольори автора системними, прибирає box-shadow і
   градієнтні фони (заміряно в headless Chromium з --force-high-contrast:
   градієнт стрілки селекта → none, інлайновий фон зразка RAL → білий Canvas,
   прозора рамка → чорна). Тож усе, що позначало СТАН фоном чи тінню, ставало
   однаковим: обраний режим і тема, обраний чип і рядок моделі, активна
   вкладка; зразки RAL і стрілки селектів зникали, фокус поля (box-shadow
   --ring при outline: none) не було видно.

   Блок — ОСТАННІМ у файлі: правило верхнього рівня з тією самою
   специфічністю, що стоїть нижче, тихо перебило б правило режиму. Стан
   читається з ARIA (aria-pressed, aria-selected), тобто показане й оголошене
   мають одне джерело. Сторож — ForcedColorsTests (каскадом режиму). */
@media (forced-colors: active) {
    /* Обраний перемикач і рядок моделі — СИСТЕМНИМ «обраним», а не рамкою.
       forced-color-adjust: none обов'язковий: без нього режим кладе під текст
       підкладку Canvas, і системний світлий текст на ній зникає (заміряно).
       Рамка-outline зливалась би з рамкою фокуса — натиснутий і сфокусований
       перемикач виглядали б однаково. Highlight — запас для браузерів без
       SelectedItem (невідоме значення відкидається, лишається попереднє).
       !important — свідомо й лише тут: правила станів специфічніші за
       [aria-pressed] (.mode-option[aria-pressed] — (0,2,0), активна тема —
       (0,4,0)), і з відмовою від підміни їхні авторські кольори показувались
       би як є — білий сегмент поверх системної палітри людини (спіймав
       ForcedColorsTests каскадом). У цьому режимі системне «обране» мусить
       перемагати будь-яке правило стану, хоч би якої специфічності. */
    [aria-pressed="true"],
    .model-row-active {
        forced-color-adjust: none;
        background-color: Highlight !important; background-color: SelectedItem !important;
        border-color: Highlight !important; border-color: SelectedItem !important;
        color: HighlightText !important; color: SelectedItemText !important;
    }
    /* forced-color-adjust успадковується: без цього код обраної плитки RAL
       (.ral-tile-on .ral-tile-code — бренд, (0,2,0)) і ціна в обраному рядку
       лишались би авторським кольором на системному «обраному». */
    [aria-pressed="true"] *,
    .model-row-active * { color: HighlightText !important; color: SelectedItemText !important; }

    /* Активна вкладка — підкресленням: форма, яку режим не забирає. */
    [role="tab"][aria-selected="true"] {
        text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: 0.3em;
    }

    /* Зразок кольору ПОКАЗУЄ колір: інакше інлайновий фон стає Canvas. */
    .ral-swatch, .ral-current, .ral-tile-swatch, .photo-swatch { forced-color-adjust: none; }

    /* Системна стрілка: власна намальована градієнтом фону, а градієнт режим прибирає. */
    .field select { appearance: auto; }

    /* Фокус поля — системним контуром: --ring це box-shadow, а тіні режим прибирає. */
    .field input:focus, .field select:focus, .field textarea:focus, .save-quote-title:focus {
        outline: 2px solid Highlight; outline-offset: 1px;
    }
}
