/* === homepage/css/fonts.css === */
/*
 * Шрифты: те же файлы, что и в вёрстке, но в правильном порядке форматов.
 *
 * ЧТО БЫЛО. `style.min.css` объявляет каждое начертание так:
 *
 *     src: url(...woff) format("woff"),
 *          url(...woff2) format("woff2"),
 *          url(...ttf) format("truetype");
 *
 * Браузер берёт ПЕРВЫЙ формат, который умеет, и дальше по списку не смотрит.
 * Умеют все — значит скачивается `.woff`, а лежащий рядом `.woff2` не трогает
 * никто. Замер на «О компании»: 402 КБ шрифтов на страницу вместо 319 КБ, при
 * том что нужные файлы уже в репозитории. Иконочному шрифту в вёрстке `woff2`
 * не объявлен вовсе — оттуда TTF на 38 КБ вместо 17 КБ.
 *
 * ПОЧЕМУ ЭТО ЛЕЧИТСЯ ЗДЕСЬ, А НЕ ТАМ. `style.min.css` не редактируется (§8).
 * Для одинаковых `font-family` + `font-weight` + `font-style` побеждает
 * ПОСЛЕДНЕЕ объявление, а наша сборка подключается после вёрстки — значит
 * достаточно повторить те же правила с обратным порядком форматов. Проверено
 * подменой: если направить правило на файл Condensed, весь текст страницы
 * становится узким, то есть побеждает именно оно.
 *
 * REGULAR И BOLD ОСТАЮТСЯ НА `.woff` — И ЭТО НЕ НЕДОСМОТР.
 * Для этих двух начертаний Chrome скачивает ОБА файла: наш `.woff2` и
 * перекрытый вендорный `.woff`. Рисует при этом наш (проверено подменой на
 * Condensed: весь текст становится узким), а вендорный не достаётся ни одному
 * видимому элементу — это внутреннее поведение движка при двух одинаково
 * описанных начертаниях одного семейства, и из CSS его не отменить.
 * Перекрытие пробовалось и со списком запасных форматов, и с одним только
 * `woff2`; вендорный файл всё равно приезжает — у Bold отдельным запросом уже
 * после `DOMContentLoaded` (замер: старт 1865 мс при готовых стилях на 889 мс),
 * то есть это не гонка загрузки, а выбор движка. Остальные два начертания и
 * иконки такого дубля не дают.
 *
 * ЗАМЕР ПО ГЛАВНОЙ (запросов шрифтов / вес):
 *
 *     ничего не перекрыто        6 / 402 КБ
 *     перекрыты все четыре       7 / 388 КБ (Regular качается дважды)
 *     перекрыт Bold              6 / 410 КБ (Bold качается дважды)
 *     Regular и Bold не перекрыты 5 / 339 КБ  ← так и оставлено
 *
 * Правило звучит странно ровно до тех пор, пока не посмотришь на числа: строка
 * ради самого ходового начертания делает страницу тяжелее, а не легче. Bold
 * стоит на `.woff` (92 КБ) вместо `.woff2` (70 КБ), и эти 22 КБ — цена того,
 * что вендорное объявление из `style.min.css` не убрать, не редактируя сам
 * файл. Меньше только если выкинуть `@font-face` из вёрстки на сборке — это
 * отдельная работа, и она трогает доставку всей вёрстки, а не одну строку.
 *
 * `font-display` сохранён как в оригинале: `swap` у текстовых начертаний (текст
 * читается сразу и переверстается на своём шрифте) и `block` у иконочного —
 * подменять иконки глифами системного шрифта нельзя, вместо стрелки покажется
 * буква.
 *
 * `.ttf` в запасных не нужен: его понимают ровно те браузеры, которые понимают
 * `.woff`, и лишняя строка только удлиняет список.
 */

@font-face {
    font-family: Roboto;
    font-display: swap;
    src: url(../fonts/Roboto-ExtraBold/Roboto-ExtraBold.woff2) format("woff2"),
         url(../fonts/Roboto-ExtraBold/Roboto-ExtraBold.woff) format("woff");
    font-weight: 800;
    font-style: normal;
}

@font-face {
    font-family: RobotoCondensed;
    font-display: swap;
    src: url(../fonts/RobotoCondensed-Regular/RobotoCondensed-Regular.woff2) format("woff2"),
         url(../fonts/RobotoCondensed-Regular/RobotoCondensed-Regular.woff) format("woff");
    font-weight: 400;
    font-style: normal;
}

/*
 * Иконочный шрифт. Запрос с тем же `?xgjht2`, что и в оригинале: это версия
 * сборки иконок, и менять её значит сбрасывать кеш без причины.
 */
@font-face {
    font-family: icons;
    font-display: block;
    src: url(../fonts/icons/icons.woff2?xgjht2) format("woff2"),
         url(../fonts/icons/icons.woff?xgjht2) format("woff");
    font-weight: 400;
    font-style: normal;
}

/* === homepage/css/table-cards.css === */
/*
 * Mobile table cards — WebKit fix.
 *
 * The design paints the rounded white card behind the price/kits tables with an
 * absolutely positioned pseudo-element that is anchored to the table row group:
 *
 *   @media (max-width:1100px) {
 *     .table thead { position: relative }
 *     .table tbody { position: relative }
 *     .table thead::before,
 *     .table tbody::after { position:absolute; top:-6px; bottom:-6px;
 *                           left:-16px; right:-16px; background:#fff; z-index:0 }
 *   }
 *
 * Relative positioning of table row groups (<thead>/<tbody>) is undefined in
 * CSS, and engines disagree: Blink (desktop Chrome, and therefore DevTools
 * mobile emulation) treats them as a containing block, WebKit does not. On
 * WebKit — which is every browser on iOS, Safari and Chrome alike — the
 * pseudo-element escapes to the initial containing block, so `top/bottom/left/
 * right: -6px/-16px` stretch it across the WHOLE document and fill it with
 * white. The page then looks blank from the header down to the first element
 * with a higher z-index, even though the layout underneath is perfectly correct.
 *
 * That is why the defect appeared on a real iPhone in both Safari and Chrome but
 * never in desktop emulation, why measurements showed correct geometry with
 * visible/opaque text, and why hit-testing returned <tbody> at every point on
 * screen.
 *
 * Fix: paint the card on the row groups themselves. A background/border on an
 * element cannot escape its own box in any engine, the look is unchanged, and
 * nothing depends on undefined behaviour any more. Scoped to the same
 * breakpoint the original rules use; desktop is untouched.
 */

@media only screen and (max-width: 1100px) {
    .table thead::before,
    .table tbody::after,
    .kits-table thead::before,
    .kits-table tbody::after,
    /*
     * `thead::after` — тот самый «остаток той же болезни», записанный в
     * CLAUDE.md §8. Полоска фона в 8 px, растянутая на `left:-16px;right:-16px`
     * от группы строк: в Blink она вылезает на 16 px за таблицу и даёт
     * горизонтальную прокрутку на ровном месте (измерено: контейнер 343,
     * прокрутка 359), в WebKit — уходит к начальному содержащему блоку и красит
     * страницу целиком. Карточку рисуют настоящие фоны на самих `thead`/`tbody`
     * ниже, поэтому полоска не нужна вовсе.
     *
     * Гасится только у таблицы сравнения: у остальных `.table` та же строка
     * остаётся жить, и лечить их нужно отдельно и со своей проверкой.
     */
    .kits-table thead::after {
        display: none;
    }

    .table thead,
    .kits-table thead {
        background: #fff;
        border: 1px solid #f5f5f5;
        border-radius: 6px 6px 0 0;
    }

    .table tbody,
    .kits-table tbody {
        background: #fff;
        border: 1px solid #f5f5f5;
        border-top: 0;
        border-radius: 0 0 6px 6px;
    }
}

/* === homepage/css/header-contacts.css === */
/*
 * Header contact channels.
 *
 * Loaded after style.min.css so the original design file stays untouched.
 * Button sizes, colours and gaps are inherited from the base stylesheet — this
 * file only adds layout rules for a variable number of icons, the MAX brand
 * button and the channel block inside the mobile menu.
 *
 * Desktop breakpoint is the project's existing one: 1100px.
 */

/* ---------------------------------------------------------------- desktop -- */
@media only screen and (min-width: 1101px) {
    /*
     * Pin the icon row to the phone block: all free space in .header-middle
     * collects before .btns, so the row hugs the phone and grows leftwards as
     * channels are added. The gap to the phone is the flex gap of the header and
     * never changes; the phone block itself never moves. No fixed widths.
     */
    .btns {
        margin-left: auto;
    }

    /* A city with no channels must not leave an empty column (and its gap). */
    .btns--no-channels {
        display: none;
    }
}

/* ----------------------------------------------------------------- mobile -- */
@media only screen and (max-width: 1100px) {
    /*
     * Only the first icons (messengers first, then email if slots remain) stay
     * next to the hamburger; the rest are still rendered for desktop and remain
     * reachable in the menu below. Selecting which ones is done server-side by
     * App\Support\ContactChannels — this rule only hides the overflow.
     */
    .btns__item.is-hidden-mobile {
        display: none;
    }

    /* The hamburger must always stay on screen, whatever the icon count. */
    .btns__item--menu {
        flex: 0 0 auto;
    }
}

/* -------------------------------------------------------------- MAX brand -- */
/*
 * Official MAX colours (brandbook go.max.ru/brandbook):
 * #471AFF "Синий гигант" -> #9500FF "Пурпурная туманность".
 * The mark itself is the official white logo stored in
 * public/homepage/img/icon-max.svg — proportions are preserved.
 */
.btns__item--max {
    background: linear-gradient(135deg, #471AFF 0%, #9500FF 100%);
}

.btns__item--max:hover {
    background: linear-gradient(135deg, #3a13d6 0%, #7d00d6 100%);
}

.btns__icon-img {
    display: block;
    width: 22px;
    height: 22px;
    object-fit: contain;
}

@media only screen and (max-width: 1100px) {
    .btns__icon-img {
        width: 19px;
        height: 19px;
    }
}

/* ------------------------------------------- channels in the mobile menu -- */
/* Hidden on desktop: .wrap-catalog-menu is the normal bottom nav bar there. */
.menu-contacts {
    display: none;
}

@media only screen and (max-width: 1100px) {
    /*
     * Inside the open hamburger the menu is a column; `order` puts the channel
     * block directly after the navigation and before the promo widgets, without
     * touching the site's own menu-building script.
     */
    .wrap-catalog-menu {
        display: flex;
        flex-direction: column;
    }

    .wrap-catalog-menu > .wrap-menus {
        flex: 0 0 auto;
    }

    .menu-contacts {
        display: block;
        order: 1;
        margin: 20px 0 0;
    }

    .wrap-widget-info {
        order: 2;
    }

    .menu-contacts__title {
        font-size: 14px;
        line-height: 1.2;
        color: #667085;
        margin: 0 0 10px;
    }

    /* Wraps onto a second line instead of stretching the menu. */
    .menu-contacts__list {
        display: flex;
        align-items: center;
        flex-wrap: wrap;
        gap: 7px;
    }

    /*
     * Phone numbers read as text, not as square icon buttons: each is its own
     * tappable line, so two numbers never get squeezed together.
     */
    .menu-phones {
        display: flex;
        flex-direction: column;
        gap: 8px;
        margin: 12px 0 0;
    }

    .menu-phones__item {
        display: block;
        color: #111;
    }

    .menu-phones__item:hover,
    .menu-phones__item:focus {
        text-decoration: none;
    }

    .menu-phones__label {
        display: block;
        font-size: 12px;
        line-height: 1.2;
        color: #667085;
    }

    .menu-phones__number {
        display: inline-block;
        font-weight: 700;
        font-size: 17px;
        line-height: 1.2;
        color: #ff491a;
    }

    .menu-phones__item:hover .menu-phones__number,
    .menu-phones__item:focus .menu-phones__number {
        text-decoration: underline;
    }
}

/* === homepage/css/header-partner.css === */
/*
 * Partner strip above the header — desktop only.
 *
 * Loaded after style.min.css, which stays untouched. There the strip has no
 * vertical padding of its own (.header-top is padding: 0 20px) while the button
 * is a fixed height: 46px, so the button alone defines the height of the strip
 * and its border sits exactly on both edges — no breathing room at all. The
 * corners are square too (border-radius: 0) on a site that rounds every other
 * control by 6px.
 *
 * The fix moves the height decision to the row: the row gets real padding and
 * the button sizes itself, which is the same shape the mobile plate already has
 * in mobile-menu.css. Total height stays 46px (34 + 6 + 6), so the header below
 * does not move and the measured --promo-h keeps its value.
 *
 * Mobile (<= 1100px) is styled in mobile-menu.css and is not touched here.
 */

@media only screen and (min-width: 1101px) {
    .header-top {
        padding: 6px 20px;
    }

    .header-top__partner {
        height: auto;
        min-height: 34px;
        padding: 6px 16px;
        border-radius: 6px;
        background: #fff;
        font-size: 14px;
        line-height: 1.1;
    }
}

/* === homepage/css/hero-photo.css === */
/* ==========================================================================
   Первый экран: фотография настоящей картинкой, а не фоном CSS

   ЗАЧЕМ. Google обнаруживает изображения по `src` реального <img> (в том числе
   внутри <picture>) и прямо пишет, что фоновые картинки CSS не индексирует.
   Пока кадр стоял в `background-image`, самая крупная и самая репрезентативная
   фотография страницы не существовала ни для Google Images, ни для превью в
   выдаче, ни для микроразметки — описывать было нечего.

   ВЁРСТКА ПРИ ЭТОМ НЕ МЕНЯЕТСЯ. Картинка лежит абсолютом под содержимым и
   растягивается `object-fit: cover` — тем же способом, каким это делал
   `background-size: cover`. Геометрия, отступы и высоты остаются макетными.
   ========================================================================== */

/*
   СВОЙ КОНТЕКСТ НАЛОЖЕНИЯ — КЛЮЧЕВАЯ ЧАСТЬ. `z-index: 0` у позиционированного
   блока создаёт контекст, и тогда картинке хватает `z-index: -1`, чтобы
   уехать ПОД содержимое, но остаться внутри блока: отрицательный слой рисуется
   сразу после фона своего контекста и раньше любого содержимого. Без контекста
   он ушёл бы к ближайшему предку и оказался за фоном страницы — то есть исчез.

   Поднимать вместо этого содержимое (`position: relative` на .calc) нельзя:
   тогда ближайшим позиционированным предком картинки станет `.calc`, и она
   перестанет закрывать отступы `.wrap-calc` — фон обрежется по содержимому.
*/
.wrap-calc {
    position: relative;
    z-index: 0;
}

/*
   ФОН МАКЕТА ВЫКЛЮЧЕН ДВУМЯ СЕЛЕКТОРАМИ, А НЕ ОДНИМ. Сборка вешает на страницу
   класс `.webp` и вторым правилом подменяет тот же фон webp-версией —
   `.webp .wrap-calc` весит больше одиночного класса и выигрывал у выключения:
   картинка уже лежала <img>, а фон продолжал рисоваться под ней. Видно это
   только замером `background-image`, глазами — нет: под одинаковой картинкой
   такая же.
*/
.wrap-calc,
.webp .wrap-calc {
    background-image: none;
}

/*
   ГДЕ ИМЕННО ЛЕЖИТ КАДР, РЕШАЕТ НЕ МЕДИАЗАПРОС, А ПРЕДОК С `position`.
   На desktop `.calc__left` не позиционирован, и картинка внутри него
   раскрывается на весь `.wrap-calc` — ровно как макетный фон. На мобильном
   hero-mobile.css делает `.calc__left` позиционированным (`isolation: isolate`
   ради вуали), и тот же элемент сам собой ограничивается им — а именно туда
   макет и переносит фон ниже 760px. Одна картинка, две геометрии, ноль
   дубликатов разметки.
*/
.hero-photo {
    position: absolute;
    inset: 0;
    z-index: -1;
    display: block;
    /* Кадр decorative по роли, но смысловой по содержанию: alt описывает его,
       поэтому от указателя и выделения он не нужен. */
    pointer-events: none;
}

.hero-photo__img {
    display: block;
    width: 100%;
    height: 100%;
    /* `width`/`height` в разметке стоят против сдвигов вёрстки; без этой пары
       браузер нарисовал бы кадр в его собственных пикселях. */
    object-fit: cover;
    object-position: center;
}

@media only screen and (max-width: 760px) {
    /* Ниже этой ширины макет переносит фон на `.calc__left` — выключаем и его,
       обоими селекторами по той же причине, что и выше. */
    .calc__left,
    .webp .calc__left {
        background-image: none;
    }
}

/* === homepage/css/hero-mobile.css === */
/*
 * Mobile hero readability.
 *
 * On mobile the hero photo moves onto .calc__left, so the H1 (including the
 * orange "до 30%"), the checkmark list and the form text sit directly on the
 * image. Different cities use different photos, so the fix must not touch the
 * image files — a translucent white overlay is added between the background and
 * the content instead.
 *
 * Desktop is untouched: everything below lives in the hero's existing mobile
 * breakpoint (760px), the same one that moves the photo onto .calc__left.
 *
 * Density is tuned with the custom properties below — one number per zone.
 */

@media only screen and (max-width: 760px) {
    .calc__left {
        /* Denser where the title and the advantages are, a little more
           transparent towards the bottom so the photo stays visible. */
        --hero-veil-top: 0.78;      /* H1 zone */
        --hero-veil-list: 0.72;     /* checkmark list */
        --hero-veil-form: 0.68;     /* form intro text (#535353, lowest contrast) */
        --hero-veil-bottom: 0.50;   /* bottom edge of the photo */

        position: relative;
        /* Keeps the veil and the raised content inside this element's own
           stacking context, so the calculator card (.calc__right, pulled up by
           -100px) still paints above the hero. */
        isolation: isolate;
    }

    .calc__left::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: 0;
        pointer-events: none;
        background: linear-gradient(
            to bottom,
            rgba(255, 255, 255, var(--hero-veil-top)) 0%,
            rgba(255, 255, 255, var(--hero-veil-list)) 35%,
            rgba(255, 255, 255, var(--hero-veil-form)) 70%,
            rgba(255, 255, 255, var(--hero-veil-bottom)) 100%
        );
    }

    /* Content sits above the veil. No per-item cards, no layout changes:
       sizes, grid, spacing, brand colours and the form position stay as they are.

       `:not(.hero-photo)` — фотография первого экрана тоже прямой потомок
       `.calc__left`, и общее правило поднимало её в поток: кадр вставал
       обычной картинкой между шапкой и заголовком, раздвигая экран, вместо
       того чтобы лежать под содержимым. Своя геометрия у неё в hero-photo.css. */
    .calc__left > *:not(.hero-photo) {
        position: relative;
        z-index: 1;
    }
}

/* === homepage/css/mobile-menu.css === */
/*
 * Mobile hamburger menu: header + tabs, and the removal of the hardcoded
 * header-height constants. Desktop (>1100px) is untouched.
 *
 * The original stylesheet pins the menu panel with literal numbers that assume
 * the 34px partner banner is always present:
 *   .wrap-catalog-menu   top: 64px; height: calc(100vh - 64px)
 *   .wrap-header.is-open-catalog  margin: -34px 0 0
 * Cities with the banner switched off have a shorter header, so the panel no
 * longer meets it and the dark .wrap-header-bottom (#333) shows through as an
 * empty strip. Both numbers are replaced by the real, measured header height
 * (--mobile-header-h, published by mobile-menu.js) instead of being masked.
 */

/*
 * The one rule here that is deliberately NOT mobile-only.
 *
 * Slider controls (.swiper-pagination, .swiper-button-*) sit at z-index:10 while
 * the sticky header is at 9, so a slider scrolling under the header painted its
 * bullets and arrows over the logo. The header belongs above page content at any
 * width; 12 clears the slider layer and stays far below Fancybox, which renders
 * in the top layer via <dialog> and ignores z-index entirely.
 */
.wrap-header {
    z-index: 12;
}

@media only screen and (max-width: 1100px) {
    /* Panel starts exactly at the bottom of the header, whatever its height. */
    .wrap-catalog-menu {
        top: var(--mobile-header-h, 64px);
        height: calc(100vh - var(--mobile-header-h, 64px));
        height: calc(100dvh - var(--mobile-header-h, 64px));
    }

    /*
     * The sticky header may only scroll away by the height of the partner
     * banner. The base stylesheet hardcodes `top:-34px`, which is that banner's
     * height — but cities with the banner switched off have no banner to hide,
     * so the header clipped 34px of itself (the logo got cut in half) as soon as
     * the page was scrolled. The offset now follows the measured banner height,
     * which is 0 when there is no banner.
     */
    .wrap-header {
        top: calc(-1 * var(--promo-h, 0px));
    }

    /* No banner -> nothing to pull out of view, so no negative shift. */
    .wrap-header.has-no-promo.is-open-catalog {
        margin: 0;
    }

    /*
     * Header and page are both white, so a scrolled sticky header blends into
     * the content. A soft shadow appears only once the page has moved — at the
     * very top the header stays flush, exactly as designed. Colours, sizes and
     * spacing are untouched.
     */
    .wrap-header {
        transition: box-shadow .2s ease-in-out;
    }

    .wrap-header.is-scrolled,
    body.is-menu-locked .wrap-header {
        box-shadow: 0 2px 12px rgba(16, 24, 40, .10);
    }

    /* ---- open state: header pinned, page behind locked ------------------- */
    /*
     * While the menu is open the whole header sits at the top of the screen and
     * the page behind does not move. `body.top` is set from JS to the saved
     * scrollY, so the user sees the same place and returns to it on close.
     */
    body.is-menu-locked {
        position: fixed;
        left: 0;
        right: 0;
        width: 100%;
        overflow: hidden;
    }

    body.is-menu-locked .wrap-header {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
    }

    /*
     * Slider pagination sits at z-index:10 while the header and the menu panel
     * are at 9, so the bullets of whatever slider happens to be behind the menu
     * showed through it. Lift the menu above page content — only while it is
     * open, so the sliders keep their normal stacking the rest of the time.
     */
    body.is-menu-locked .wrap-catalog-menu {
        z-index: 100;
    }

    body.is-menu-locked .wrap-header {
        z-index: 101;
    }

    /* Beats the vendor rule that shifts the header up by the banner height. */
    body.is-menu-locked .wrap-header.is-open-catalog {
        margin: 0;
    }

    /*
     * The partner plate belongs to the top of the page: once the page has been
     * scrolled it is gone, and opening the menu must not resurrect it. The class
     * is set by mobile-menu.js only when the page was not at the top.
     */
    .wrap-header.is-promo-hidden .wrap-header-top {
        display: none;
    }

    /* The header's dark bottom bar has nothing to show while the menu is open;
       it is what used to appear as an empty grey strip in the gap. */
    body.is-menu-locked .wrap-header-bottom {
        background: none;
    }

    /*
     * Layout of the open menu: head on top, promo plate at the bottom, and the
     * navigation between them taking the remaining height. Only the middle
     * scrolls, and its height comes from flexbox rather than from any constant.
     */
    .wrap-catalog-menu {
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }

    .menu-scroll {
        flex: 1 1 auto;
        min-height: 0;          /* lets the flex item actually shrink and scroll */
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
    }

    /* The inner list must not keep its own viewport-based height, which used to
       reserve empty space at the bottom. */
    .wrap-catalog-menu .wrap-menus {
        height: auto;
        overflow: visible;
    }

    /* ---- partner plate above the header ---------------------------------- */
    /*
     * Its own block above the dark header: real breathing room, a clear gap
     * between the text and the button, and a button that reads as a button.
     * Wraps onto two lines on very narrow screens instead of overlapping.
     */
    .wrap-header-top {
        background: #f5f5f5;
    }

    .header-top {
        flex-wrap: wrap;
        gap: 8px 12px;
        padding: 10px 16px;
    }

    .header-top__invite {
        flex: 1 1 auto;
        min-width: 0;
        gap: 8px;
    }

    .header-top__right {
        flex: 0 0 auto;
    }

    .header-top__partner {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        height: auto;
        min-height: 34px;
        padding: 7px 14px;
        border: 1px solid #ff491a;
        border-radius: 6px;
        background: #fff;
        color: #ff491a;
        font-size: 13px;
        line-height: 1.1;
        white-space: nowrap;
    }

    .header-top__partner:hover,
    .header-top__partner:focus {
        background: #ff491a;
        color: #fff;
        text-decoration: none;
    }

    /* ---- mobile header: logo + quick call + hamburger --------------------- */
    /* Messenger/email icons live in the menu now, not next to the hamburger. */
    .btns__item--channel {
        display: none;
    }

    /* Quick-call button, same size and weight as the hamburger next to it. */
    .btns__item--call-now {
        background: #1f7ae0;
    }

    .btns__item--call-now:hover {
        background: #1660b4;
    }

    .btns__item--call-now .btns__btn {
        color: #fff;
    }

    .btns__item--call-now svg {
        width: 18px;
        height: 18px;
        display: block;
    }

    /* ---- compact promo strip under the header ----------------------------- */
    /*
     * Same structural idea as the partner plate above the header: one dense row,
     * not a card. It sits inside the sticky header, so it stays visible while the
     * partner plate scrolls away, and the navigation starts right underneath it.
     */
    /*
     * `display: block` is required, not cosmetic: the shared `is-hidden-pc`
     * helper sets `display: initial` on mobile, and `initial` for display is
     * `inline`. Flex children (like the hamburger) get blockified anyway, but
     * this strip is a direct child of the block header — left inline it paints
     * no background and its content overlaps the page.
     */
    .wrap-promo-bar {
        display: block;
        background: #fff7f5;
        border-top: 1px solid #ffe0d8;
        border-bottom: 1px solid #ffe0d8;
    }

    .promo-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
        padding: 7px 16px;
    }

    .promo-bar__content {
        min-width: 0;
    }

    .promo-bar__line {
        display: block;
        font-size: 13px;
        line-height: 1.25;
        color: #111;
    }

    .promo-bar__title {
        font-weight: 700;
        color: #ff491a;
    }

    .promo-bar__text {
        font-weight: 700;
    }

    .promo-bar__note {
        display: block;
        font-size: 11px;
        line-height: 1.2;
        color: #667085;
    }

    .promo-bar__btn {
        flex: 0 0 auto;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 32px;
        padding: 6px 12px;
        border-radius: 6px;
        background: #ff491a;
        color: #fff;
        font-weight: 700;
        font-size: 12px;
        line-height: 1.1;
        white-space: nowrap;
    }

    .promo-bar__btn:hover,
    .promo-bar__btn:focus {
        background: #de370c;
        color: #fff;
        text-decoration: none;
    }

    /* Gentle pulse to draw the eye — a slow glow, not a flashing element. */
    .promo-bar__btn {
        animation: promo-pulse 2.2s ease-in-out infinite;
    }

    @keyframes promo-pulse {
        0%, 100% {
            box-shadow: 0 0 0 0 rgba(255, 73, 26, .55);
        }
        50% {
            box-shadow: 0 0 0 6px rgba(255, 73, 26, 0);
        }
    }

    /* Respect users who ask for less motion. */
    @media (prefers-reduced-motion: reduce) {
        .promo-bar__btn {
            animation: none;
        }
    }

    @media only screen and (max-width: 400px) {
        .promo-bar__note {
            display: none;      /* keep the strip to a single dense row */
        }
    }

    /* ---- head: title + tabs ---- */
    .menu-head {
        order: 0;
        flex: 0 0 auto;
        margin: 0 0 14px;
    }

    .menu-head__title {
        font-weight: 700;
        font-size: 23px;
        line-height: 100%;
        color: #111;
        margin: 0 0 12px;
    }

    .menu-tabs {
        display: flex;
        align-items: stretch;
        gap: 4px;
        border-bottom: 2px solid #efefef;
    }

    .menu-tabs__tab {
        flex: 1 1 auto;
        min-width: 0;
        padding: 8px 6px 10px;
        border: 0;
        background: none;
        font: inherit;
        font-size: 15px;
        line-height: 120%;
        color: #535353;
        text-align: center;
        white-space: nowrap;
        cursor: pointer;
        position: relative;
        transition: color .2s ease-in-out;
    }

    .menu-tabs__tab::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        bottom: -2px;
        height: 2px;
        background: transparent;
        transition: background .2s ease-in-out;
    }

    .menu-tabs__tab.is-active {
        color: #ff491a;
        font-weight: 700;
    }

    .menu-tabs__tab.is-active::after {
        background: #ff491a;
    }

    .menu-tabs__tab:focus-visible {
        outline: 2px solid #ff491a;
        outline-offset: -2px;
    }

    /* ---- panels ---- */
    .menu-panels {
        order: 0;
        flex: 0 0 auto;
    }

    .menu-panels__panel[hidden] {
        display: none;
    }

    /* ---- expandable sections in the "По сайту" tab ---- */
    /*
     * A section that really has children gets a chevron that points down and
     * flips when open; a plain link keeps the right arrow from the base design.
     * The expand/collapse itself is the site's own handler — we only make the
     * affordance readable and indent the revealed children.
     */
    /* U+E904 is the design's own chevron (icon-arrow); U+E906 (icon-arrow3) is
       the right arrow that plain links keep. */
    .wrap-catalog-menu .catalog-menu__item.is-sub-catalog::after,
    .wrap-catalog-menu .menu__item.is-sub-catalog::after {
        content: "\e904";
        font-family: icons;
        font-size: 12px;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 24px;
        height: 24px;
        position: absolute;
        right: 0;
        top: 10px;
        z-index: 0;
        transition: transform .2s ease-in-out;
    }

    .wrap-catalog-menu .is-sub-catalog.is-open::after {
        transform: rotate(180deg);
    }

    /* The cloned main menu needs the same row layout as the catalog list. */
    .wrap-catalog-menu .menu__item {
        position: relative;
        border-bottom: 1px dashed #e4e4e4;
    }

    .wrap-catalog-menu .menu__link {
        display: flex;
        align-items: center;
        justify-content: space-between;
        min-height: 42px;
        padding: 9px 50px 9px 0;
        font-size: 15px;
        line-height: 110%;
        color: #111;
    }

    /* Revealed children sit directly under their parent, slightly indented. */
    .wrap-catalog-menu .is-sub-catalog .wrap-sub-menu {
        padding-left: 12px;
    }

    .wrap-catalog-menu .is-sub-catalog .sub-menu__link {
        display: block;
        min-height: 38px;
        padding: 9px 0;
        font-size: 14px;
        line-height: 120%;
        color: #535353;
    }

    .wrap-catalog-menu .is-sub-catalog .sub-menu__link:hover,
    .wrap-catalog-menu .is-sub-catalog .sub-menu__link:focus {
        color: #ff491a;
        text-decoration: none;
    }

    /* ---- page navigation inside the second tab ---- */
    .menu-page-nav {
        list-style: none;
        margin: 0;
        padding: 0;
    }

    .menu-page-nav__item {
        border-bottom: 1px dashed #e4e4e4;
    }

    .menu-page-nav__link {
        display: block;
        /* comfortable touch target */
        min-height: 42px;
        padding: 11px 0;
        font-size: 15px;
        line-height: 120%;
        color: #111;
    }

    .menu-page-nav__link:hover,
    .menu-page-nav__link:focus {
        color: #ff491a;
        text-decoration: none;
    }

    /* ---- smooth in-page scrolling that clears the fixed header ---- */
    html {
        scroll-behavior: smooth;
    }

    [id] {
        scroll-margin-top: calc(var(--mobile-header-h, 64px) + 12px);
    }
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* === homepage/css/catalog-menu.css === */
/*
 * Catalogue menu.
 *
 * Loaded after style.min.css, which stays untouched. Desktop keeps the design's
 * one-level dropdown; everything below the desktop block is mobile behaviour and
 * mobile looks only.
 */

/* ------------------------------------------------------------- desktop --- */
@media only screen and (min-width: 1101px) {
    /*
     * `Услуги ремонта` has 19 children; the design's 325px shows about seven
     * before scrolling. Taller, but still capped so the panel cannot run off a
     * short screen — the base rule already gives it `overflow: auto`.
     */
    .catalog-menu__item > .wrap-sub-menu > .sub-menu {
        max-height: min(60vh, 520px);
    }

    /* Some labels are long ("Сантехник, сантехнические работы"). */
    .catalog-menu__item > .wrap-sub-menu {
        min-width: 300px;
        max-width: 380px;
    }
}

/* -------------------------------------------------------------- mobile --- */
@media only screen and (max-width: 1100px) {
    /*
     * THE ACCORDION MUST NOT DEPEND ON :hover.
     *
     * In style.min.css the panel is `display: none`, and the ONLY rules that turn
     * it back on are hover ones — `.catalog-menu__item:hover .wrap-sub-menu` —
     * none of them inside a media query. Measured at 375px: with `is-open` the
     * panel gets `max-height: 4000px` but `display` stays `none`. So on a phone
     * the section is opened by hovering, not by the class the handler sets.
     *
     * That is the iOS two-tap bug. Safari gives the first tap as hover only when
     * hovering changes what is painted — and here it changes display:none to
     * block — then waits for a second tap to fire the click. Android Chrome
     * dispatches the click within the same gesture, so it looked fine there.
     *
     * It also explains the wrong-section behaviour: hover is sticky, so moving to
     * another section drops the previous panel straight back to display:none, the
     * list jumps, and the next tap lands on whatever slid under the finger.
     *
     * The accordion already animates through max-height; display simply must not
     * be part of it.
     */
    .wrap-sub-menu {
        display: block;
    }

    /*
     * TAP TARGET.
     *
     * The chevron a finger aims at is `.catalog-menu__item::after` — a
     * pseudo-element on the <li>, positioned over the right edge of the row. The
     * arrow that lives inside the <a> is `display: none` here. The accordion
     * handler in common.min.js listens on `.is-sub-catalog > a`, so a tap that
     * lands on the chevron hits the <li> and nothing happens, while a tap on the
     * label works — which is exactly why the section seemed to need several
     * presses.
     *
     * The link already spans the row (width: 100% with padding-right: 50px), so
     * letting taps fall through the chevron makes the whole row one target. The
     * glyph itself is left exactly as the design draws it.
     */
    .catalog-menu__item::after {
        pointer-events: none;
    }

    /*
     * LONG LISTS.
     *
     * Two caps clip an open section: `max-height: 500px` on the panel and
     * `max-height: 325px` + `overflow: auto` on the list inside it. With 19
     * children under `Услуги ремонта` the tail was simply cut off — not
     * scrollable, invisible. The whole menu already scrolls in `.menu-scroll`,
     * so the section opens to its full height and the page scrolls, rather than
     * a second scroller appearing inside the first.
     */
    .catalog-menu__item.is-open > .wrap-sub-menu {
        max-height: 4000px;
    }

    .catalog-menu__item > .wrap-sub-menu > .sub-menu {
        max-height: none;
        overflow: visible;
        gap: 0;
    }

    /*
     * GROUPING.
     *
     * Open children used to read as more top-level entries: same white
     * background, same dashed rule, same indent. The group now shares one tinted
     * field and one accent rail, so it is obvious where the section starts and
     * ends.
     */
    .catalog-menu__item.is-open {
        background: #fdf6f4;
        box-shadow: inset 3px 0 0 #ff491a;
    }

    .catalog-menu__item.is-open > .catalog-menu__link {
        font-weight: 700;
        color: #ff491a;
        padding-left: 12px;
    }

    .catalog-menu__item.is-open > .wrap-sub-menu {
        padding: 2px 15px 12px 12px;
    }

    /* Children: tighter, indented, and without the divider that made each of
       them look like a section of its own. */
    .catalog-menu__item.is-open .sub-menu__item {
        border-bottom: 0;
        font-size: 14px;
        line-height: 125%;
    }

    .catalog-menu__item.is-open .sub-menu__link {
        display: block;
        padding: 9px 0 9px 12px;
        color: #444;
    }

    .catalog-menu__item.is-open .sub-menu__link:active {
        color: #ff491a;
    }
}

/* ---------------------------------------------------- scrollbar, Firefox -- */
/*
 * `.custom-scroll` is the site's scrollable box — the page-navigation list
 * (`.navigation.custom-scroll`) uses it, as do the quiz and city pickers.
 * Chrome, Edge and Safari already get a 3px orange thumb from the
 * ::-webkit-scrollbar rules in style.min.css; Firefox ignores those entirely and
 * fell back to the default grey bar.
 *
 * `scrollbar-width` / `scrollbar-color` are the standard properties Firefox does
 * support. `thin` is the narrowest it offers — it cannot be pinned to 3px, so
 * this gets as close as the platform allows without a JS scrollbar. The bar
 * stays visible in both engines.
 */
.custom-scroll {
    scrollbar-width: thin;
    scrollbar-color: #ff491a #e4e4e4;
}

/* === homepage/css/hero-palette.css === */
/*
 * First screen — colour hierarchy.
 *
 * Loaded after style.min.css, which stays untouched. Nothing here moves, resizes
 * or renames anything: it only reassigns colour.
 *
 * The problem it fixes: the brand red was doing two jobs at once. As a large
 * field it filled the calculator block — `.w-calc-form` was
 * `rgba(255,73,26,.89)` across a 522px column, roughly twenty times the area of
 * the main CTA button — and as an accent it also painted the logo, the "до 30%"
 * figure, eight checkmarks and the buttons. A colour covering that much surface
 * stops reading as a signal, and the "Рассчитать" button inside the block was
 * red on red.
 *
 * So the red field becomes a neutral one and the red stays only on things that
 * are meant to be pressed or read first. Chosen grey is #f5f5f5 — already the
 * site's own tone, the one behind the partner strip in the header — so no new
 * colour enters the palette.
 *
 * Untouched on purpose: the logo, every `.btn`, the "до 30%" span, the layout
 * and all copy.
 */

/* ------------------------------------------------- calculator: the field -- */
.w-calc-form {
    /* Was rgba(255,73,26,.89). Same translucency so the photo behind still
       shows through and the block keeps its depth. */
    background: rgba(245, 245, 245, .95);
    color: #111;
}

/* The dashed rules inside were light red (#ff8965) — invisible now, and they
   were another red surface. */
.w-calc-form .form__item {
    border-bottom-color: #e2e2e2;
}

.calc-form-title {
    border-bottom: 1px solid #e2e2e2;
}

/* ------------------------------------------- calculator: the controls ----- */
/*
 * These stay red: they are the interactive parts, and on a neutral field they
 * are now the only saturated things in the block, which is the point.
 */

/* An empty radio was a white circle with no edge — fine on red, gone on grey. */
.w-calc-form .form-radios__txt::before {
    box-shadow: inset 0 0 0 1px #d7d7d7;
}

.w-calc-form .form-radios__input:checked ~ .form-radios__txt::before {
    box-shadow: inset 0 0 0 1px #ff491a;
}

/* Same reason: a white box on a white-ish field needs a visible edge. */
.w-calc-form .numbs {
    border-color: #e2e2e2;
}

/*
 * Slider. The filled part was pale red on white; inverted, it now carries the
 * accent and the empty part is grey. The thumb already is #ff491a in a white
 * ring, which reads correctly on the new field.
 */
.w-calc-form input[type="range"]::-webkit-slider-runnable-track {
    background: linear-gradient(
        to right,
        #ff491a var(--range-progress, 0),
        #dcdcdc var(--range-progress, 0)
    );
}

.w-calc-form input[type="range"]::-moz-range-track {
    background: #dcdcdc;
}

/* --------------------------------------------------------- checkmarks ----- */
/*
 * Eight full-strength red ticks scattered the eye across the whole left column
 * and competed with the button underneath them. Same brand colour, muted: still
 * recognisably ours, no longer shouting.
 */
.calc-list__item::before {
    color: rgba(255, 73, 26, .55);
}

/* === homepage/css/lead-consents.css === */
/*
 * The two legal consents under every lead form.
 *
 * Loaded after style.min.css, which stays untouched. The checkbox square and
 * tick are still drawn there off the .form-privacy classes; the component keeps
 * them. Everything below is what style.min.css does not have — the pair layout,
 * the error state, the focus ring — plus the colour and type corrections.
 *
 * WHY THE COLOURS CHANGE
 * The block used to read as one red mass: 15px text with three bright #ff491a
 * underlined links, and on error a fourth red line joined them. Directly under
 * the main CTA that put a second red cluster next to the button and blunted it.
 * So the resting state goes quiet — dark text, near-black links — and red is
 * kept for two jobs only: hover/focus, and the error.
 *
 * TWO BACKGROUNDS, NOT ONE
 * Five of the seven forms sit on white. The other two — .wrap-call and
 * .wrap-estimate — sit on dark background photos (call-bg.webp,
 * estimate-bg.webp) and inherit white text by design. Darkening the text
 * everywhere would make it invisible there, so those blocks keep light text and
 * get their own link and error colours. Everything else is shared.
 */

/* -------------------------------------------------------------- layout --- */
.lead-consents {
    display: flex;
    flex-direction: column;
    /*
     * The two consents are separate points and have to read as such. 12px is
     * wider than a line gap, so they never merge into a paragraph.
     */
    gap: 12px;
    /* Keeps the block off the CTA button above it. */
    margin-top: 4px;
}

.lead-consent {
    display: block;
    position: relative;
}

/* ---------------------------------------------------------- typography --- */
.lead-consent__txt {
    /*
     * 15px -> 13px. Legal small print does not need the size of body copy, and
     * the smaller face is what stops the block from competing with the button.
     * Still comfortably above the 12px floor on desktop.
     */
    font-size: 13px;
    /*
     * 1.4 instead of 1.3 — enough air between wrapped lines without inflating
     * the block: at 12px on mobile the pair stays within a couple of pixels of
     * the height it had before this pass.
     */
    line-height: 1.4;
    color: #333;
    overflow-wrap: break-word;
    cursor: pointer;
}

/*
 * The square is 20px, the first line of text is 18.2px on desktop and 16.8px on
 * mobile, so a square pinned to `top: 0` hangs below the line it belongs to.
 * One pixel up centres it against the first line at both sizes. The tick moves
 * with it, otherwise it would sit off-centre inside its own box.
 */
.lead-consent__txt::before,
.lead-consent__txt::after {
    top: -1px;
}

/* ----------------------------------------------------------- the links --- */
/*
 * Near-black rather than brand red. They still read as links — underlined, and
 * darker than the text around them — but they no longer shout. The underline is
 * softened so three of them in one sentence do not turn into a ladder.
 *
 * Written as a descendant pair on purpose: style.min.css colours these with
 * `.form-privacy__txt a`, and a single class cannot outweigh two. Same
 * specificity, loaded later — this wins.
 */
.lead-consent__txt .lead-consent__link {
    color: #1a1a1a;
    text-decoration: underline;
    text-decoration-color: rgba(26, 26, 26, .45);
    text-underline-offset: 2px;
    transition: color .15s ease-in-out, text-decoration-color .15s ease-in-out;
}

/*
 * Red belongs here: on the pointer and on the keyboard, where it marks the one
 * link being addressed instead of colouring all four at rest.
 */
.lead-consent__txt .lead-consent__link:hover,
.lead-consent__txt .lead-consent__link:focus-visible {
    color: #ff491a;
    text-decoration-color: currentColor;
}

.lead-consent__link:focus-visible {
    outline: 2px solid #ff491a;
    outline-offset: 2px;
    border-radius: 2px;
}

/* ------------------------------------------------------------ the box ---- */
/*
 * style.min.css draws the square as white with a 1px #e6e6e6 border — on a
 * white form that edge is all but invisible, and an unticked required control
 * that cannot be seen is the one thing this block must not have.
 *
 * The edge is #6b6b6b, not a mid grey: the hero form sits on the light panel
 * introduced with the palette pass (rgba(245,245,245,.95)), and against that a
 * #9a9a9a outline still disappeared. Dark enough to read as a real control on
 * both white and the panel, still neutral so it does not add to the red.
 *
 * Size stays 20px — the border is drawn inside it (box-sizing: border-box), so
 * nothing around the text moves.
 */
.lead-consent__txt::before {
    border: 2px solid #6b6b6b;
    background: #fff;
}

/* Pointing at the row is already an intent to tick it. */
.lead-consent:hover .lead-consent__txt::before {
    border-color: #ff491a;
}

/* Ticked: the brand colour, matching the tick that appears inside. */
.lead-consent__check:checked ~ .lead-consent__txt::before {
    border-color: #ff491a;
}

/* --------------------------------------------------------------- focus --- */
/*
 * The input is clipped to 1x1 in style.min.css, so the system ring is drawn
 * around nothing. Move it onto the square. :focus-visible, so a mouse click
 * stays quiet.
 */
.lead-consent__check:focus-visible ~ .lead-consent__txt::before {
    outline: 2px solid #ff491a;
    outline-offset: 2px;
}

/* --------------------------------------------------------------- error --- */
/*
 * A red badge plus one short red line, set apart from the consent text: the
 * message is the only red thing that appears, and the sentence above it stays
 * exactly as it was. Colouring the text and its links red instead — the obvious
 * shortcut — is what made the block unreadable in the first place.
 */
.lead-consent__error {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin: 6px 0 0;
    padding-left: 30px; /* the width reserved for the square, so it lines up */
    font-size: 12px;
    line-height: 1.35;
    color: #d92c00;
}

.lead-consent__error::before {
    content: "!";
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    margin-top: 1px;
    border-radius: 50%;
    background: #d92c00;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
}

/*
 * A hidden message must take no space — the display above would otherwise win
 * over the [hidden] default.
 */
.lead-consent__error[hidden] {
    display: none;
}

/* The square borrows the error colour; the text next to it is left alone. */
.lead-consent.is-invalid .lead-consent__txt::before {
    border-color: #d92c00;
}

/* ------------------------------------------------- blocks on dark photos -- */
/*
 * .wrap-call and .wrap-estimate: white text on a photo. style.min.css already
 * gives them `color: inherit`, so the text needs nothing — but the link and
 * error colours chosen for white would sink into the image.
 */
.wrap-call .lead-consent__txt,
.wrap-estimate .lead-consent__txt {
    color: inherit;
}

.wrap-call .lead-consent__link,
.wrap-estimate .lead-consent__link {
    color: #fff;
    text-decoration-color: rgba(255, 255, 255, .55);
}

.wrap-call .lead-consent__link:hover,
.wrap-call .lead-consent__link:focus-visible,
.wrap-estimate .lead-consent__link:hover,
.wrap-estimate .lead-consent__link:focus-visible {
    /* Lighter than #ff491a, which is too dark to read against the photo. */
    color: #ffb09c;
    text-decoration-color: currentColor;
}

/*
 * The white square already stands out against a photo; a #9a9a9a edge there
 * only makes it look grubby, so it gets a light one instead.
 */
.wrap-call .lead-consent__txt::before,
.wrap-estimate .lead-consent__txt::before {
    border-color: #d8d8d8;
}

.wrap-call .lead-consent__error,
.wrap-estimate .lead-consent__error {
    color: #ffa693;
}

.wrap-call .lead-consent__error::before,
.wrap-estimate .lead-consent__error::before {
    background: #ff4d26;
}

/* -------------------------------------------------------------- mobile --- */
@media only screen and (max-width: 767px) {
    .lead-consents {
        gap: 10px;
    }

    .lead-consent__txt {
        /* 13px -> 12px: one step down, still readable at arm's length. */
        font-size: 12px;
    }

    .lead-consent__error {
        font-size: 11px;
    }
}

/* === homepage/css/lead-submit.css === */
/*
 * What a registered lead form looks like while it is being sent, when it fails,
 * and when it succeeds.
 *
 * Loaded after style.min.css, which stays untouched. The design covered none of
 * these: the form was never locked, could not fail, and confirmed with a single
 * short line.
 */

/* ------------------------------------------------------------- sending --- */
/*
 * The button is disabled from JS while the lead is being sent, so a second
 * click cannot duplicate it. This is what that looks like — the caption is left
 * alone, only the button dims and the cursor says "busy".
 */
.lead-form-sending .form__btn,
form.is-sending [type="submit"] {
    opacity: .6;
    cursor: progress;
}

form.is-sending {
    /* Nothing inside is worth clicking until the answer arrives. */
    pointer-events: none;
}

/*
 * …except the fields themselves, so a visitor can keep reading what they typed
 * and the page does not feel frozen.
 */
form.is-sending .form__input,
form.is-sending .lead-consent__link {
    pointer-events: auto;
}

/* --------------------------------------------------------------- error --- */
/*
 * Shown only when the lead was NOT stored. It sits under the button, in the
 * same red as the consent errors so the form speaks with one voice.
 */
.lead-form__error {
    display: block;
    margin: 10px 0 0;
    padding: 9px 12px;
    border-radius: 6px;
    background: rgba(217, 44, 0, .08);
    color: #d92c00;
    font-size: 13px;
    line-height: 1.35;
}

.lead-form__error[hidden] {
    display: none;
}

/*
 * On the two blocks standing on dark photos the tinted plate disappears and the
 * red goes muddy — same treatment as the consent errors there.
 */
.wrap-call .lead-form__error,
.wrap-estimate .lead-form__error {
    background: rgba(0, 0, 0, .35);
    color: #ffa693;
}

/* ------------------------------------------------------------- success --- */
/*
 * The confirmation window. Its title was sized for «Заявка успешно
 * отправлена!» — 36px bold, one short line. The agreed wording is a sentence
 * and a promise, and at 36px it broke into seven lines of shouting inside a
 * 640px modal.
 *
 * The sentence is split at its own full stop in the markup: the confirmation
 * leads, the promise follows in a calmer size. Nothing is reworded.
 */
.modal-success {
    gap: 14px;
}

.modal-success__title {
    font-size: 24px;
    line-height: 1.25;
    /* Roughly forty characters a line — a comfortable measure, and it keeps the
       title to two lines at any modal width. */
    max-width: 18em;
}

.modal-success__note {
    font-size: 16px;
    line-height: 1.4;
    color: #535353;
    max-width: 24em;
}

/* The design's 56/69/80px was built around one line of text. */
.wrap-modal--success {
    padding: 48px 40px 52px;
}

@media only screen and (max-width: 767px) {
    .lead-form__error {
        font-size: 12px;
    }

    .modal-success__title {
        font-size: 20px;
    }

    .modal-success__note {
        font-size: 14px;
    }

    .wrap-modal--success {
        padding: 36px 24px 40px;
    }
}

/* === homepage/css/messenger-choice.css === */
/*
 * "Куда отправить смету" — the messenger picker.
 *
 * Loaded after style.min.css, which stays untouched. The control there is drawn
 * exactly like the consent checkbox was: a white square with a 1px #e6e6e6
 * edge. Inside a white modal that edge is barely there, so a required choice
 * looked like plain text with a gap in front of it.
 *
 * Same treatment as the consents, for the same reason and in the same colours —
 * the two sit a few centimetres apart in this modal and must not look like two
 * different kinds of control.
 *
 * Scoped to .form-check, which the design uses for this picker and nowhere else
 * on the page.
 */

.form-check__txt::before {
    border: 2px solid #6b6b6b;
    background: #fff;
}

/*
 * The tick lives on ::after, which carries its own border and swaps to the
 * brand colour when checked — it has to match the new weight, otherwise the
 * square appears to thin out at the moment it is ticked.
 */
.form-check__txt::after {
    border-width: 2px;
}

/* Pointing at the label is already an intent to choose it. */
.form-check__item:hover .form-check__txt::before {
    border-color: #ff491a;
}

/*
 * The row wraps to a second line now that there are three options; 30px between
 * columns is the design's gap, and this keeps the rows from touching when it
 * does wrap on a narrow screen.
 */
.form-check {
    row-gap: 14px;
}

/* === homepage/css/form-fields.css === */
/*
 * Text fields in the lead forms — a visible edge.
 *
 * Loaded after style.min.css, which stays untouched. There the field is drawn
 * with `border: 1px solid #f5f5f5` — the same colour as the panel the hero form
 * sits on, so the box has no edge at all and is held together only by its
 * shadow. Next to the consent boxes, which now have a real 2px outline, an
 * input with no border looked like a gap in the form.
 *
 * #949494 clears 3:1 against white, which is what a control's boundary is asked
 * for; the field keeps its 1px, its radius and its shadow, so nothing moves.
 *
 * Focus is the second half of this: the design gives a focused field nothing at
 * all, which leaves keyboard users without a caret position on a page whose
 * forms are the point. It takes the brand colour, the one place red belongs.
 */

.form__input,
.quiz-form__input {
    border-color: #949494;
}

.form__input:focus,
.quiz-form__input:focus {
    border-color: #ff491a;
    /* A second pixel drawn outside the border, so the field does not resize. */
    box-shadow: 0 0 0 1px #ff491a;
    outline: none;
}

/* --------------------------------------------------------- invalid ------- */
/*
 * A field the page has complained about. The class is set from lead-submit.js
 * rather than relying on :invalid, which is true from the first paint — an
 * empty required field would be red before anyone touched the form.
 */
.form__input.is-invalid,
.quiz-form__input.is-invalid {
    border-color: #d92c00;
}

.form__input.is-invalid:focus,
.quiz-form__input.is-invalid:focus {
    border-color: #d92c00;
    box-shadow: 0 0 0 1px #d92c00;
}

/*
 * The message itself, under the field it belongs to. Same red and same size as
 * the consent errors below it — one form, one voice.
 */
.lead-field__error {
    display: block;
    margin: 6px 0 0;
    font-size: 12px;
    line-height: 1.35;
    color: #d92c00;
}

.lead-field__error[hidden] {
    display: none;
}

/* On the two blocks standing on dark photos the darker red goes muddy. */
.wrap-call .lead-field__error,
.wrap-estimate .lead-field__error {
    color: #ffa693;
}

/* === homepage/css/sidebar-tall.css === */
/*
 * Left sidebar on desktop: pinned under the header, banner at the bottom.
 *
 * Loaded after style.min.css, which stays untouched.
 *
 * TWO PROBLEMS, ONE CONSTANT BEHIND BOTH
 * `.container .sidebar { position: sticky; top: 145px }` — 145 is the header's
 * height with the page unscrolled. The header is itself sticky at `top: -46px`,
 * so as soon as you scroll it comes to rest 46px higher while the sidebar keeps
 * hanging at 145: a band of empty white above «Навигация по странице» that
 * follows you down the page. The same number is also wrong for cities without
 * the partner strip, whose header is ~34px shorter.
 *
 * `.navigation { height: 430px; overflow: auto }` — a hard cap. The 18 items
 * need 788px and the panel ends up 786px tall no matter the monitor, so on a
 * 1080p screen ~150px sat unused, the banner floated in the middle of it, and
 * the menu scrolled at 430px with room to spare.
 *
 * WHAT THIS DOES
 *   1. The sticky offset comes from sidebar-sticky.js, which measures where the
 *      header's bottom edge actually rests (CLAUDE.md §8: measure, do not
 *      hardcode). 145px stays only as a fallback if the script never runs.
 *   2. On screens tall enough for it, the panel takes the height of its sticky
 *      slot, the banner drops to the bottom with `margin-top: auto`, and the
 *      list takes everything in between — so the scrollbar appears only when
 *      the list genuinely does not fit.
 */

/* ------------------------------------------------------------ 1. the top -- */
/*
 * Desktop only: below 1101px the sidebar is not a column at all but a
 * full-screen overlay panel (`position: fixed`, `top: 0`), which must not be
 * touched.
 */
@media only screen and (min-width: 1101px) {
    /*
     * The header collapses to its catalogue bar instead of stopping half-way
     * through the logo row. -46px left 51px of that row on screen — the top of
     * the letters and nothing usable. The measured value takes the whole row
     * away and keeps the bar, which is the part you navigate with.
     *
     * Desktop only: the mobile header is a different element with its own
     * measured height (mobile-menu.js), and the hamburger panel is positioned
     * from it.
     */
    .wrap-header {
        top: var(--header-collapse, -46px);
    }

    .container .sidebar {
        top: var(--sidebar-top, 145px);
    }
}

/* -------------------------------------------------- 2. the height and the -- */
/*
 * THE HOME PAGE ONLY — this half is about a menu with nineteen items.
 *
 * Part 1 above is about the header, and every page carries the same header, so
 * it applies everywhere. Stretching the panel to the viewport is a different
 * matter: it exists because the home page's list needs 788px and was capped at
 * 430. An inner column holds one or two widgets, and stretching it would push
 * the price banner down to the bottom edge of the screen with empty space above
 * it — there the column takes its height from its content (CLAUDE.md §7.8),
 * which is the same thing `sidebar-nav.css` already says about the menu inside.
 *
 * SCOPED BY THE MARKER THE INNER PAGE CARRIES, not by one the home page does:
 * `<body>` on the home page has only `webp` on it — there is no `.page` class
 * to select — so `.page:not(.page--inner)` matched nothing at all and quietly
 * switched the stretch off everywhere. `body:not(.page--inner)` asks the one
 * question that has an answer on both pages.
 *
 * Only when the screen is tall enough that stretching can only help.
 *
 * 940px is the floor. The panel needs its offset + 786 (today's content) + 24
 * (a gap under the banner) to match what it has now; with the partner strip the
 * offset is the largest, ~119px, and 119 + 786 + 24 = 929. Ask for less and the
 * menu would come out SHORTER than its current 430px, which is why laptops at
 * 1366x768 stay on the old layout untouched.
 */
@media only screen and (min-width: 1101px) and (min-height: 940px) {
    body:not(.page--inner) .sidebar {
        /* Breathing room under the banner so it does not sit on the very edge. */
        --sidebar-bottom-gap: 24px;

        /*
         * Sized from --sidebar-reserve (what the header occupies right now),
         * not from --sidebar-top (where it comes to rest). At the top of the
         * page the panel starts under the full 145px header while it is pinned
         * to 48px, and sizing it from 48 ran it ~97px below the fold with the
         * banner cut in half.
         */
        height: calc(100vh - var(--sidebar-reserve, 145px) - var(--sidebar-bottom-gap));

        display: flex;
        flex-direction: column;
    }

    /*
     * The menu widget takes the free space; the banner keeps its natural size.
     * min-height: 0 is what actually lets the list scroll inside a flex column —
     * without it a flex item refuses to shrink below its content and the
     * overflow silently moves to the panel instead.
     */
    body:not(.page--inner) .sidebar .widget.is-side-menu {
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-height: 0;
    }

    body:not(.page--inner) .sidebar .wrap-navigation {
        display: flex;
        flex: 1 1 auto;
        min-height: 0;
    }

    /* Was a hard 430px. Now: as tall as there is room for. */
    body:not(.page--inner) .sidebar .navigation {
        height: auto;
        flex: 1 1 auto;
        min-height: 0;
    }

    /*
     * The banner drops to the bottom edge of the panel and never shrinks — the
     * menu above it is what gives way when space runs short.
     */
    body:not(.page--inner) .sidebar .widget--fix {
        margin-top: auto;
        flex: 0 0 auto;
    }
}

/* === homepage/css/hero-calculator.css === */
/*
 * Итоговая сумма в калькуляторе первого экрана.
 *
 * Причина. В style.min.css `.form-price` — это flex-строка со space-between, в
 * которой правая колонка сжимается наравне с левой. Длинный заголовок
 * «Ориентировочная стоимость работ:» съедал место, и сумма ужималась до ширины
 * самого длинного слова: «от 158 000 ₽» разрывалось после «158» и наезжало на
 * подпись.
 *
 * Почему медиазапросы тут не помогают. Ширину задаёт не окно, а колонка
 * `.calc__right`, поэтому дефект виден и на 1920 — существующие брейкпоинты
 * 1400/1000 к нему отношения не имеют. Правило должно зависеть от места в
 * контейнере, а не от ширины экрана.
 *
 * Что делает правка:
 *   - заголовок сжимается только до min-content (две строки, 186px) и не
 *     дальше, поэтому сумме всегда остаётся её ширина;
 *   - сумма не сжимается вообще (flex: 0 0 auto);
 *   - если места не хватает даже на это — строка переносится целиком и сумма
 *     встаёт под заголовком, как в мобильной раскладке (там style.min.css с
 *     ≤1000px уже делает flex-direction: column).
 *
 * Сама сумма не рвётся благодаря неразрывным пробелам в значении
 * (PriceEstimate::label и hero-calculator.js), а не благодаря white-space:
 * тем же узлом печатается «Рассчитаем индивидуально» — это предложение, и
 * переносить его как раз нужно.
 */

.form-price {
    flex-wrap: wrap;
}

.form-price__left {
    flex: 1 1 0;
    min-width: min-content;
}

/*
 * Подпись — во всю ширину блока, а не в остатке колонки.
 *
 * Заголовок и подпись лежат в общей обёртке `.form-price__left`, поэтому
 * подпись получала ту же узкую колонку, что и заголовок (247px из 422px), и
 * разваливалась на три строки, хотя под ней пусто. `display: contents` убирает
 * коробку обёртки, и три её содержимых становятся элементами самой flex-строки;
 * наследование при этом сохраняется — подпись по-прежнему берёт из обёртки
 * размер, интерлиньяж и RobotoCondensed, поэтому значения из style.min.css
 * здесь не продублированы.
 *
 * Порядок задаётся `order`, а не разметкой: в DOM подпись стоит перед суммой,
 * а нужна под ними обеими. `flex: 1 0 100%` заставляет её занять отдельную
 * строку целиком.
 *
 * Только для строчной раскладки: на ≤1000px style.min.css уже ставит
 * flex-direction: column, где подпись и так во всю ширину, а порядок
 * «заголовок → подпись → сумма» менять незачем.
 */
@media only screen and (min-width: 1001px) {
    .form-price {
        column-gap: 20px;
        row-gap: 6px;
    }

    .form-price__left {
        display: contents;
    }

    .form-price__title {
        order: 1;
        flex: 1 1 0;
        min-width: min-content;
    }

    /* Без явного order сумма осталась бы нулевой и встала перед заголовком. */
    .form-price__right {
        order: 2;
    }

    .form-price__sub-title {
        order: 3;
        flex: 1 0 100%;
    }
}

/*
 * Сжиматься можно, но не ниже min-content. Для суммы min-content — это она
 * целиком: все пробелы в ней неразрывные, разбить её не на чем, поэтому она
 * никогда не ужмётся. А «Рассчитаем индивидуально» — обычная фраза, её
 * min-content равен самому длинному слову, и на узкой карточке она спокойно
 * переносится в две строки вместо того, чтобы вылезти за край.
 */
.form-price__right {
    flex: 0 1 auto;
    min-width: min-content;
}

/* === homepage/css/about-fears.css === */
/*
 * Список вопросов в блоке «О компании».
 *
 * Причина: раньше блок был четырьмя абзацами, и списка в вёрстке не
 * существовало. Теперь он открывается вопросами посетителя, а список читается
 * быстрее абзаца — глаз находит свой вопрос, не дочитывая предложение.
 *
 * Маркер рисуется на самом элементе через ::before, а не на строке таблицы или
 * группе — по той же причине, что описана в CLAUDE.md §8: абсолютные
 * псевдоэлементы, позиционированные от неподходящего предка, в WebKit всплывают
 * к начальному содержащему блоку.
 */

.about-fears {
    margin: 18px 0;
    padding: 0;
    list-style: none;
}

.about-fears li {
    position: relative;
    padding-left: 22px;
    margin-bottom: 10px;
}

.about-fears li:last-child {
    margin-bottom: 0;
}

.about-fears li::before {
    content: '';
    position: absolute;
    left: 0;
    /* Половина строки минус половина точки — маркер встаёт по первой строке,
       а не по центру пункта, который может занять две. */
    top: 0.62em;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #FF491A;
}

/* === homepage/css/promo-banner.css === */
/*
 * Промо-баннер «зафиксировать стоимость»: блок с суммой в окне и картинка на
 * мобильной плашке.
 *
 * Зачем отдельный файл: style.min.css править нельзя (CLAUDE.md §8), а этих двух
 * элементов в макете не было. Блок с суммой — новый акцент в окне, картинка на
 * плашке — задел под макеты из Figma: поле уже есть, и когда его заполнят,
 * плашка не должна поехать.
 */

/* ------------------------------------------- круглая стрелка на баннере --- */

/*
 * Кружок со стрелкой: круглый и не отнимающий ширину у текста.
 *
 * Обе беды — от того, что иконка стояла ОТДЕЛЬНОЙ КОЛОНКОЙ рядом с заголовком.
 *
 * 1. Как flex-элемент она сжимаема. Пока строка была короткой, места хватало
 *    всем; с длинной заголовок забрал ширину у соседа: при height 28px ширина
 *    упала до 18,3px, и border-radius: 50% нарисовал овал.
 *
 * 2. Запрет сжиматься круг вернул, но забрал у заголовка 9px — со 128 до 119, а
 *    «Перед осенним» требует 124. Строка перескочила с двух на три.
 *
 * Обтекание (float) тоже не годится: высота кружка 28px против интерлиньяжа
 * 19,5 — он вклинивается в ДВЕ строки, и на второй остаётся 105px при
 * нужных 119.
 *
 * Поэтому иконка уходит из потока в угол карточки — ровно туда, где она в
 * макете. Заголовку достаётся вся ширина, 159px вместо 119; «Внимание!»
 * заканчивается на 105-м пикселе, а кружок начинается на 145-м, так что
 * столкнуться им негде.
 *
 * Ширину тут задаёт СОСЕД ПО КОНТЕЙНЕРУ, а не окно, поэтому медиазапросы такое
 * не лечат (CLAUDE.md §8).
 */
/*
 * Подложка под верхним текстом карточки.
 *
 * Без неё текст сливался с фотографией, и это не вкусовщина, а замер: красный
 * «Внимание!» (#ff491a) не дотягивал до контраста 3:1 на ОДИННАДЦАТИ снимках из
 * восемнадцати, белая строка до 4.5:1 — на шести. У осенней листвы красный давал
 * 1.18 при норме 3.
 *
 * Сила подобрана перебором по всем восемнадцати фотографиям, а не на глаз: 0.75
 * — наименьшее затемнение, при котором проходят все. Меньше 0.75 не проходят
 * подсолнух, голубь, листья, флаг и снег.
 *
 * Флагу этого было мало и при 0.82: в исходном кадре белые лучи делали его
 * втрое светлее остальных. Он перекадрирован по самому флагу — снимок починен
 * там, где он сломан, вместо того чтобы топить ради него все остальные.
 *
 * Градиент, а не сплошная плашка: текст занимает верхние 28% карточки, ниже
 * фотография открывается, а с 63% начинается собственная тёмная панель нижней
 * строки, которая была в вёрстке изначально.
 */
.fix-price::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    height: 62%;
    background: linear-gradient(
        to bottom,
        rgba(16, 24, 40, 0.75) 0%,
        rgba(16, 24, 40, 0.75) 45%,
        rgba(16, 24, 40, 0) 100%
    );
    pointer-events: none;
    z-index: 1;
}

/* Текст — над подложкой. */
.fix-price__content {
    z-index: 2;
}

.fix-price__top-bar {
    position: relative;
}

.fix-price__title {
    width: 100%;
}

/*
 * Отступ от угла — такой же, с каким от края стоит текст (14px слева).
 * Прижатый вплотную кружок читается как обрезанный краем карточки, тем более
 * что у неё скруглённый угол в 10px.
 */
.fix-price__icon {
    position: absolute;
    top: 12px;
    right: 12px;
    flex-shrink: 0;
}

/* ------------------------------------------ фотография на мобильной плашке --- */

/*
 * Фотография — ФОН полосы, а не миниатюра рядом с текстом.
 *
 * Миниатюрой она добавляла плашке 15 пикселей высоты (49 -> 64), а вместе с
 * шапкой это уже половина экрана телефона, закрытая до того, как посетитель
 * увидел хоть строку страницы. Фоном она не занимает в потоке ничего, и высота
 * остаётся прежней.
 *
 * Затемнение обязательно и здесь: подобранная по замеру подложка 0.75 — то же
 * число, что и на карточке, и по той же причине.
 */
.promo-bar {
    position: relative;
    overflow: hidden;
}

.promo-bar__img {
    position: absolute;
    inset: 0;
    display: block;
    margin: 0;
}

.promo-bar__img img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.promo-bar__img::after {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(16, 24, 40, 0.75);
}

/* Текст и кнопка — поверх фотографии и её подложки. */
.promo-bar__content,
.promo-bar__btn {
    position: relative;
    z-index: 1;
}

/*
 * Плашка говорит одну строку — «Успейте до 31 августа» — и она белая.
 *
 * Красного «Внимание!» и пояснения здесь больше нет: строка и кнопка стоят
 * рядом на полосе высотой 49px, и три текста в ней спорили за место, а вместе
 * с шапкой полоса и так забирает верх экрана телефона.
 */
.promo-bar__text {
    color: #fff;
    font-weight: 700;
}

/* === homepage/css/cookie-notice.css === */
/*
 * Уведомление о cookie.
 *
 * Отдельный файл, потому что style.min.css править нельзя (CLAUDE.md §8), а
 * этого элемента в макете не было вовсе.
 *
 * Панель прижата к низу окна и НЕ перекрывает содержимое страницы: position
 * fixed, поэтому она ничего не сдвигает и не меняет высоту документа. Кнопки
 * «наверх» и мобильного меню живут выше по z-index — уведомление не должно
 * закрывать собой то, чем пользуются.
 */

.cookie-notice {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 11; /* ниже шапки (12), выше остального содержимого */
    padding: 0 16px 16px;
    pointer-events: none; /* сама полоса кликов не ловит — только панель */
}

.cookie-notice[hidden] {
    display: none;
}

.cookie-notice__panel {
    display: flex;
    align-items: center;
    gap: 20px;
    max-width: 1180px;
    margin: 0 auto;
    padding: 18px 22px;
    border-radius: 16px;
    background: #fff;
    box-shadow: 0 8px 32px rgba(16, 24, 40, 0.18);
    pointer-events: auto;
}

.cookie-notice__text {
    margin: 0;
    font-size: 14px;
    line-height: 1.45;
    color: #101828;
}

.cookie-notice__link {
    color: inherit;
    text-decoration: underline;
}

.cookie-notice__link:hover {
    text-decoration: none;
}

/*
 * flex-shrink: 0 — кнопка не отдаёт ширину длинному тексту: ширину здесь задаёт
 * сосед по контейнеру, а не окно (§8), и сжатая кнопка «Ок» рассыпается по
 * буквам.
 */
.cookie-notice__btn {
    flex-shrink: 0;
    min-width: 96px;
    padding: 12px 26px;
    border: 0;
    border-radius: 8px;
    background: #ff491a;
    color: #fff;
    font-size: 15px;
    font-weight: 700;
    line-height: 1.2;
    cursor: pointer;
}

.cookie-notice__btn:hover {
    background: #e63f13;
}

.cookie-notice__btn:focus-visible {
    outline: 2px solid #101828;
    outline-offset: 2px;
}

/*
 * Кнопка «наверх» уходит над панелью, пока та показана.
 *
 * Панель накрывала её целиком: кнопка оставалась видимой, но нажать её было
 * нельзя. Сдвиг задан переменной, которую публикует cookie-notice.js по
 * измеренной высоте — зашитая константа сломалась бы на телефоне и ещё раз
 * после «Подробнее», где панель втрое выше (§8).
 *
 * transform, а не bottom: своё bottom у кнопки задано в style.min.css, который
 * править нельзя, и переопределять его отсюда значило бы спорить с ним.
 */
.has-cookie-notice .btn-up {
    transform: translateY(calc(-1 * var(--cookie-notice-h, 0px)));
}

/*
 * «Подробнее» — кнопка, нарисованная как ссылка: она управляет текстом на этой
 * же странице и никуда не ведёт, а <a href="#"> обещал бы переход.
 * На десктопе прятать нечего, поэтому её там нет.
 */
.cookie-notice__more {
    display: none;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    /* После font: inherit — иначе сокращение сбросило бы насыщенность обратно. */
    font-weight: 700;
    text-decoration: underline;
    cursor: pointer;
}

@media (max-width: 760px) {
    .cookie-notice {
        padding: 0 10px 10px;
    }

    /*
     * Панель в четыре строки закрывала четверть экрана телефона. Видна первая
     * фраза; остальное открывается по «Подробнее» — и исчезает вместе с ним,
     * потому что сворачивать обратно незачем: следующий шаг здесь «Ок».
     */
    .cookie-notice__rest {
        display: none;
    }

    .cookie-notice__more {
        display: inline;
        margin-left: 4px;
    }

    .is-expanded .cookie-notice__rest {
        display: inline;
    }

    .is-expanded .cookie-notice__more {
        display: none;
    }

    .cookie-notice__panel {
        flex-direction: column;
        align-items: stretch;
        gap: 12px;
        padding: 14px 16px;
        border-radius: 12px;
    }

    .cookie-notice__text {
        font-size: 13px;
    }

    .cookie-notice__btn {
        width: 100%;
    }
}

/* === homepage/css/portfolio-thumbs.css === */
/*
 * Стрелки у колонки миниатюр портфолио.
 *
 * ЗАЧЕМ. Слайдер миниатюр инициализируется в common.min.js с
 * `slidesPerView: 4` и без навигации: при семи фотографиях три остаются
 * недостижимыми. Перетаскивание работает, но о нём нечем догадаться — колонка
 * выглядит просто списком из четырёх снимков, и половина галереи для человека
 * не существует.
 *
 * Здесь только внешний вид кнопок. Показывает и прячет их portfolio-thumbs.js:
 * прятать нечего, когда фотографий четыре или меньше, а кнопка, которая
 * никуда не ведёт, — обещание, которого страница не выполняет.
 *
 * Правится отдельным файлом, а не style.min.css (CLAUDE.md §8).
 */

.photos-block-slider__wrap-thumbs {
    position: relative;
}

.photos-thumbs-nav {
    display: none;
    width: 100%;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: #f4f4f4;
    color: #ff491a;
    cursor: pointer;
    align-items: center;
    justify-content: center;
    transition: background-color 0.15s ease, opacity 0.15s ease;
}

.photos-thumbs-nav:hover {
    background: #ececec;
}

/* Показывает скрипт — только когда листать действительно есть что. */
.photos-thumbs-nav.is-shown {
    display: flex;
}

/*
 * Край списка — не повод убирать кнопку: исчезающий элемент сдвигает колонку
 * и уводит миниатюры из-под курсора. Гасим и отключаем на месте.
 */
.photos-thumbs-nav[disabled] {
    opacity: 0.35;
    cursor: default;
}

.photos-thumbs-nav__icon {
    display: block;
    width: 12px;
    height: 8px;
    fill: currentColor;
}

.photos-thumbs-nav--prev {
    margin-bottom: 8px;
}

.photos-thumbs-nav--next {
    margin-top: 8px;
}

/*
 * До 1100px слайдер миниатюр горизонтальный (breakpoint из common.min.js), и
 * вертикальные стрелки над и под колонкой там ни на что не указывают: листать
 * нужно вбок. Их место — по бокам, поэтому на мобильной они скрыты, а листание
 * остаётся жестом, который на касании как раз очевиден.
 */
@media (max-width: 1099px) {
    .photos-thumbs-nav.is-shown {
        display: none;
    }
}

/* === homepage/css/portfolio-card.css === */
/*
 * Карточка проекта: колонки заканчиваются на одном уровне.
 *
 * ЧТО БЫЛО. Стрелки ленты миниатюр стояли в потоке и добавляли колонке свои
 * 64 px: верхняя вылезала над снимком, нижняя — под него. Правая колонка при
 * этом обрывалась там, где кончился текст, и низких краёв у карточки было три.
 *
 * ЧТО СТАЛО. Стрелки лежат поверх краёв ленты и высоту колонки не меняют,
 * а кнопка правой колонки прижата к её низу. Ничего, кроме этого, файл не
 * трогает: размеры слайдеров считает Swiper от ширины контейнера, и
 * вмешательство в них (пробовал `aspect-ratio` на самом слайдере) замыкает
 * вычисление на себя — ширина выходит нулевой, а карточка пустой.
 *
 * Правится отдельным файлом, а не style.min.css (CLAUDE.md §8).
 */

/*
 * Одна кнопка под лентой — по центру, а не растянутая на всю ширину.
 *
 * В макете их две, и `.photos-actions__item { flex: 1 }` делит строку пополам.
 * Пока «видеообзоры» ждут своего раздела, кнопка осталась одна, и то же
 * правило растягивало её от края до края — она читалась как полоса, а не как
 * кнопка.
 *
 * `:only-child` вместо класса-модификатора: когда вторую кнопку включат,
 * правило перестанет действовать само, и вспоминать о нём не придётся.
 */
.photos-actions__item:only-child {
    flex: 0 1 auto;
    margin-inline: auto;
    min-width: 260px;
}

@media (min-width: 1100px) {
    /* Обе колонки одной высоты — иначе низ правой не к чему прижимать. */
    .photos-block {
        align-items: stretch;
    }

    .photos-block__right {
        display: flex;
        flex-direction: column;
    }

    .photos-block__right .photos-block-actions {
        margin-top: auto;
    }

    /*
     * Снимок заполняет свою рамку.
     *
     * Высоту рамке задаёт лента миниатюр (в макете у неё зашито 433 px), а
     * снимок при ширине 448 px выходил 336 — и висел по центру рамки с
     * пустыми полосами сверху и снизу. Именно эти полосы и читались как
     * «уровни не сходятся»: рамка-то стояла ровно.
     *
     * `cover`, а не `fill`: рамка чуть выше пропорции файла, и `fill`
     * растянул бы комнату по вертикали. Срезать край честнее, чем
     * деформировать снимок.
     */
    /*
     * Высоту карточки задаёт снимок, а не лента миниатюр.
     *
     * Раньше её задавала лента: в макете у неё зашито `max-height: 433px`, а
     * снимок при ширине колонки 448 px выходит 336 — и висел по центру рамки
     * с пустыми полосами сверху и снизу. Ленту вынимаем из потока, и высоту
     * начинает диктовать единственный элемент со своей формой — фотография.
     *
     * Слайдер снимка при этом НЕ трогаем: Swiper считает ширину слайдов от
     * ширины контейнера, и любая правка его размеров замыкает вычисление на
     * себя — ширина выходит нулевой, а карточка пустой. Проверено дважды.
     *
     * Ширину колонке приходится назвать: единственный её носитель — сама
     * лента, а у элемента вне потока ширины для родителя нет. 126 px — то же
     * значение, что стоит в макете у `.photos-block-slider-thumbs`.
     */
    .photos-block-slider__wrap-thumbs {
        position: relative;
        flex: 0 0 126px;
        width: 126px;
    }

    .photos-block-slider-thumbs {
        position: absolute;
        inset: 0;
        max-height: none;
    }

    /*
     * Поверх, а не в потоке. Кнопка закрывает край крайней миниатюры — той
     * самой, к которой она и ведёт, — и ничего не сдвигает.
     */
    .photos-thumbs-nav {
        position: absolute;
        left: 0;
        right: 0;
        z-index: 2;
        margin: 0;
        background: rgba(244, 244, 244, 0.92);
    }

    .photos-thumbs-nav--prev {
        top: 0;
    }

    .photos-thumbs-nav--next {
        bottom: 0;
    }
}

/* === homepage/css/works-comparison.css === */
/*
 * Таблица «что входит в ремонт» прокручивается внутри себя.
 *
 * ЗАЧЕМ. В макете колонок было четыре, и таблица помещалась. Колонки теперь
 * строятся из прайса — их пять, и на 1440 таблица шире своего контейнера
 * (1278 против 876). Без этой рамки лишнее выталкивало всю страницу: документ
 * получал горизонтальную прокрутку, а вместе с ней уезжали и слайды портфолио,
 * которые до того аккуратно прятались за краем своего слайдера.
 *
 * Прокрутка живёт у таблицы, а не у страницы (CLAUDE.md §8): широкий блок
 * скроллится сам, страница остаётся на месте. Шестой вид ремонта в прайсе
 * ничего здесь не сломает — он просто окажется за краем прокрутки.
 *
 * На мобильной таблица превращается в карточки (`table-cards.css`), ширины там
 * нет, и правило безвредно: прокручивать нечего.
 *
 * Правится отдельным файлом, а не style.min.css (CLAUDE.md §8).
 */

.wrap-kits-table {
    overflow-x: auto;
    /* Инерционная прокрутка на iOS: без неё жест обрывается на границе. */
    -webkit-overflow-scrolling: touch;
}

/*
 * Значок статуса — символ из редактора, а не иконка шрифта.
 *
 * Размер и центрирование задаются здесь, чтобы «✓», «+» и «—» стояли ровно в
 * той же клетке, что раньше занимала <i class="icon-check2">, и замена символа
 * не двигала строку.
 */
.kits-status__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    font-size: 18px;
    line-height: 1;
    font-weight: 700;
    color: var(--status-color, #a9a9a9);
}

/*
 * Заливка ячейки — из цвета статуса, а не из классов `is-include`.
 *
 * В макете состояний было ровно два и у каждого свой класс с зашитым фоном.
 * Статусов теперь столько, сколько заведёт редактор, и сопоставлять их с
 * зашитыми классами значило бы держать второй список — тот самый, из-за
 * которого легенда и таблица расходились. Фон считается из того же цвета,
 * который редактор выбрал для значка; браузер без `color-mix` просто оставит
 * белый фон макета.
 */
.kits-status--mark {
    background: color-mix(in srgb, var(--status-color, #a9a9a9) 8%, #fff);
    border-color: color-mix(in srgb, var(--status-color, #a9a9a9) 18%, #fff);
}

/*
 * Подпись статуса — для тех, кто значка не видит.
 *
 * ИМЕННО ТАК, а не `display: none`: скрытый так текст скринридер не читает, и
 * ячейка превращается в «плюс», о котором нечего сказать. Класса-утилиты в
 * проекте не было — первая версия просто печатала подпись в ячейку, и таблица
 * разъезжалась на четыре строки в клетке.
 */
.kits-status__label,
.kits-marker__item .kits-status__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/*
 * ЛЕГЕНДА — ОДНА ПЛАШКА, А НЕ ТРИ КАРТОЧКИ.
 *
 * ПОЧЕМУ ОНА ТЕРЯЛАСЬ. В макете у `.kits-marker` есть только раскладка колонкой,
 * а у `.kits-marker-title` и `.kits-marker__list` нет НИ ОДНОГО правила: заголовок
 * «Обозначения» выходил обычным текстом, а три пункта — тремя белыми карточками
 * с тем же скруглением, что у пунктов меню категорий прямо над ними. Читалось
 * это как продолжение списка категорий: четвёртый, пятый и шестой пункт того же
 * меню, только почему-то со значками.
 *
 * ЧТО СТАЛО. Рамка, светлый фон и отступы — у самой легенды; заголовок внутри
 * неё. Пункты фона и рамки лишаются вовсе: карточка здесь одна, и три вложенных
 * карточки внутри неё вернули бы ту же кашу. Отличает пункты только цветной
 * значок — тот же символ и тот же `--status-color`, что в ячейках таблицы, из
 * тех же данных `statuses`.
 *
 * Ширина — 100% боковой колонки (255 px, 200 на ≤1620): геометрия блока задана
 * макетом, и плашка её не двигает.
 */
.kits-marker {
    /* Перебивает `flex-direction: column` макета и `row` на ≤1100: внутри
       плашки раскладкой занимается список, а не она сама. */
    display: block;
    box-sizing: border-box;
    width: 100%;
    padding: 16px 18px;
    background: #f7f8fa;
    border: 1px solid #ebedf0;
    border-radius: 12px;
}

.kits-marker-title {
    margin: 0 0 12px 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 120%;
    color: #111;
}

.kits-marker__list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* Пункт легенды: тот же значок и тот же цвет, что в ячейках таблицы. */
.kits-marker__item {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Макет рисует пункт белой карточкой (`background:#fff;border-radius:6px`)
       и держит min-height 48 — внутри общей плашки это снова три карточки. */
    padding: 0;
    min-height: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
    font-size: 14px;
    line-height: 130%;
    color: #333;
}

/*
 * Значок в легенде — такая же плашка, как в ячейке.
 *
 * Заливка считается из того же `--status-color`, поэтому «✓» в легенде и «✓» в
 * таблице совпадают сами: второго списка цветов нет ни здесь, ни в шаблоне.
 */
.kits-marker__item .kits-status__mark {
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    border-radius: 6px;
    background: color-mix(in srgb, var(--status-color, #a9a9a9) 10%, #fff);
}

/*
 * На узких экранах плашка остаётся плашкой, а пункты — столбиком.
 *
 * Макет на ≤1100 разворачивает `.kits-marker` в строку, на ≤360 добавляет
 * перенос, а на ≤520 запрещает пунктам переносить текст. Вместе это давало три
 * обозначения, разложенные «как получится»: два в строку, третье под ними, с
 * разными интервалами. Здесь список остаётся вертикальным на любой ширине —
 * значки выстраиваются в одну колонку, подписи начинаются по одной линии.
 */
@media only screen and (max-width: 1100px) {
    .kits-marker__list {
        flex-direction: column;
        flex-wrap: nowrap;
        gap: 10px;
    }

    .kits-marker__item {
        min-width: 0;
        /* Макет на ≤520 ставит nowrap: длинная подпись выходила за плашку. */
        white-space: normal;
    }
}

/*
 * ============================================================================
 * ТАБЛИЦА НА УЗКИХ ЭКРАНАХ: НАЗВАНИЕ РАБОТЫ — СТРОКОЙ, СТАТУСЫ — ПОД НИМ
 * ============================================================================
 *
 * ЧЕРЕЗ ЧТО ПРОШЛИ. Макет раздавал тарифам по `width:25%` — верстка ровно под
 * ЧЕТЫРЕ. Их пять, и пятая колонка уезжала за край без возможности до неё
 * добраться. Следующая попытка — липкая колонка с названием работы и свайп по
 * таблице — четыре тарифа показывала, но пятый обрезался посередине пустой
 * ячейки (значок статуса стоит по центру своей клетки и в видимую полоску не
 * попадает), и экран читался как уехавшая вёрстка.
 *
 * ЧТО ЗДЕСЬ. Названию работы отдана вся ширина строки, статусы стоят под ним —
 * и тогда ПЯТЬ тарифов помещаются в 375 px без всякого свайпа. Ни липкой
 * колонки, ни горизонтальной прокрутки, ни обрезанных колонок: показывать
 * нечего, потому что всё видно.
 *
 * ЧИСЛО КОЛОНОК — ИЗ ДАННЫХ. `--tariff-count` печатает шаблон, поэтому шестой
 * вид ремонта в прайсе просто делает колонки уже, а не ломает раскладку.
 *
 * ТАБЛИЦА ОСТАЁТСЯ ТАБЛИЦЕЙ. Меняется только `display`: разметка, заголовки и
 * порядок ячеек прежние, и скринридер читает ту же таблицу, что на desktop.
 */
@media only screen and (max-width: 1100px) {
    /*
     * ОДИН КЕГЕЛЬ НА ОБА ЗАГОЛОВКА, И ОН ЗАВИСИТ ОТ ШИРИНЫ ЭКРАНА.
     *
     * Название работы и название вида ремонта — заголовки одного уровня: первый
     * называет карточку, второй — колонку внутри неё. Разный кегль читался бы
     * как разная важность, которой между ними нет, поэтому размер один и лежит
     * в переменной.
     *
     * Почему не 13 px всегда: колонка тарифа это (ширина экрана − отступы) / 5,
     * и на 320 px в ней остаётся 48 px. «тической» при 13 px требует 59 —
     * измерено, слово вылезало за карточку на 11 px. `clamp` держит 13 px на
     * широких телефонах и сжимает кегль ровно там, где иначе не помещается.
     */
    .kits-table {
        --kits-heading-size: clamp(10px, 3.2vw, 13px);
    }

    .kits-table,
    .kits-table thead,
    .kits-table tbody {
        display: block;
        width: 100%;
        max-width: none;
        padding: 0;
        border-spacing: 0;
    }

    /*
     * Строка — сетка из тарифов. Название работы занимает её целиком (`1 / -1`)
     * и встаёт первой строкой сетки, статусы — второй.
     */
    .kits-table thead tr,
    .kits-table tbody tr {
        display: grid;
        grid-template-columns: repeat(var(--tariff-count, 5), 1fr);
        gap: 6px;
        align-items: stretch;
    }

    /*
     * Ячейка с названием работы возвращается: макет прячет первую колонку
     * целиком (`td:first-child{display:none}`), потому что печатал название
     * псевдоэлементом соседней ячейки.
     */
    .kits-table tbody td:first-child {
        display: block;
        grid-column: 1 / -1;
    }

    /*
     * Ширину ячеек задаёт сетка, а не они сами.
     *
     * Макет держит `width:25%` — доля от таблицы под четыре колонки. В сетке
     * этот процент считается уже от КОЛОНКИ, и ячейка съёживалась до 15 px:
     * ровно ширина галочки. Измерено — колонки при этом были правильные, 58,6,
     * а плашки статусов болтались в них узкими полосками.
     */
    .kits-table td,
    .kits-table th {
        width: auto;
        min-width: 0;
    }

    /*
     * Угловая ячейка шапки не участвует в сетке: она пустая (название категории
     * стоит в выпадающем списке над таблицей), а место заняла бы как полноценная
     * колонка — и пять тарифов перестали бы помещаться.
     */
    .kits-table thead th:first-child {
        display: none;
    }

    /*
     * Название работы из макета печаталось псевдоэлементом второй ячейки —
     * теперь оно снова обычный текст первой. Псевдоэлементы гасятся, иначе
     * название стоит дважды.
     */
    .kits-table td:nth-child(2)::before,
    .kits-table th:nth-child(2)::before,
    .kits-table td:nth-child(2)::after,
    .kits-table th:nth-child(2)::after {
        content: none;
        display: none;
    }

    /* Позиционирование держало тот псевдоэлемент — вместе с ним и уходит. */
    .kits-table td:nth-child(2),
    .kits-table th:nth-child(2) {
        position: static;
        z-index: auto;
    }

    /*
     * Каждая работа — своя карточка.
     *
     * Общая рамка вокруг всего `tbody` (её рисует table-cards.css) собирала
     * сорок девять строк в одно полотно, где не видно, где кончается одна
     * работа и начинается следующая. Здесь рамка у строки, а полотно снято.
     */
    .kits-table tbody {
        background: transparent;
        border: 0;
    }

    .kits-table tbody tr {
        margin: 0 0 8px 0;
        padding: 12px;
        background: #fff;
        border: 1px solid #ececf0;
        border-radius: 10px;
    }

    /*
     * Отступы шапки — те же, что у карточки работы.
     *
     * Колонки считаются от содержимого своего контейнера: 8 px у шапки против
     * 12 px у строки давали разную ширину колонки, и «Капитальный» переставал
     * стоять точно над своим столбцом галочек — а вся таблица держится на том,
     * что стоит.
     */
    .kits-table thead {
        margin: 0 0 10px 0;
        padding: 12px;
        border-radius: 10px;
    }

    .kits-table thead {
        position: static;
    }

    /*
     * НАЗВАНИЕ РАБОТЫ — ТЕМ ЖЕ ШРИФТОМ, ЧТО НАЗВАНИЕ ВИДА РЕМОНТА.
     *
     * Это заголовок своей карточки, а не подпись к ячейке: строкой ниже под ним
     * стоят пять статусов, и он их называет — ровно так же, как название тарифа
     * называет свою колонку. Разный кегль у двух заголовков одного уровня
     * читается как разная важность, которой между ними нет.
     */
    .kits-table-title {
        min-width: 0;
        min-height: 0;
        padding: 0 0 10px 0;
        background: transparent;
        border-radius: 0;
        font-size: var(--kits-heading-size);
        line-height: 1.25;
        font-weight: 700;
        color: #111;
    }

    /* Карточка тарифа: название, цена, ссылка — тремя строками. */
    .kits-table-type {
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        gap: 2px;
        height: 100%;
        min-height: 0;
        /* Боковых отступов нет: расстояние между тарифами держит `gap` сетки, а
           каждый пиксель здесь отнимается у названия — измерено, что с 2 px
           «тический» вылезал за карточку на 3 px. */
        padding: 0;
        background: transparent;
    }

    .kits-table-type__title {
        white-space: normal;
        /* Тот же кегль, что у названия работы — см. переменную выше. */
        font-size: var(--kits-heading-size);
        line-height: 1.25;
        font-weight: 700;
        color: #111;
        /*
         * Мягкий перенос стоит в самом значении (`short_label` в
         * config/price-settings.php): «Косме-» / «тический» — по слогу и с
         * дефисом. `overflow-wrap` здесь не появляется намеренно — именно он и
         * рвал слова посреди буквы.
         */
        hyphens: manual;
        /*
         * Место под две строки — столько занимают четыре названия из пяти.
         * «Черновой» помещается в одну, и его цена вставала выше, чем у
         * соседей: пять цен на разных уровнях читаются как сбитая вёрстка. Тот
         * же приём, что на desktop.
         */
        min-height: calc(2 * 1.25em);
    }

    .kits-table-type__area {
        font-size: 11px;
        line-height: 1.2;
    }

    /*
     * Ссылка прижата к низу карточки, а не идёт следом за текстом.
     *
     * Названия занимают то одну строку, то две («Черновой» против
     * «Косме-тический»), и иконка, поставленная сразу под ценой, вставала у
     * каждой карточки на своей высоте — пять значков на разных уровнях читаются
     * как сбитая вёрстка. `margin-top: auto` в колоночном flex отдаёт ссылке
     * весь остаток высоты: карточки растянуты по сетке до одной высоты, поэтому
     * низ у них общий, и значки встают на одну линию сами.
     */
    .kits-table-type__link {
        margin: auto 0 0;
        padding: 8px 0 0;
        font-size: 14px;
    }

    /* Статус занимает свою клетку целиком — пять одинаковых плашек в ряд. */
    .kits-status {
        padding: 8px 2px;
        min-height: 44px;
        height: 100%;
    }

    .kits-status__mark {
        min-width: 0;
        font-size: 17px;
    }

    /*
     * `.is-hidden-pc` в макете снимается через `display:initial`, а для <div>
     * это `inline`: короткое название встало бы в строку с ценой.
     */
    .kits-table-type__title.is-hidden-pc {
        display: block;
    }
}

/*
 * Колонки видов ремонта — одной ширины.
 *
 * Без этого ширину задаёт длина заголовка: «Косметический ремонт» получал 136
 * px, «Евроремонт» — 96, и таблица выглядела собранной наспех. Доли, а не
 * пиксели: видов ремонта столько, сколько в прайсе, и зашитая ширина
 * рассыпалась бы на шестом.
 */
@media (min-width: 1100px) {
    /*
     * `fixed` обязателен: при автоматической раскладке проценты — лишь
     * пожелание, и ширину всё равно назначает самое длинное слово в заголовке.
     * Проверено: без этой строки колонки остались 136 / 122 / 129 / 96 / 112.
     */
    .kits-table {
        table-layout: fixed;
    }

    .kits-table thead th:not(:first-child) {
        width: calc(76% / 5);
    }

    /* Первой колонке остаётся её минимум из макета — там названия работ. */
    .kits-table thead th:first-child {
        width: 24%;
    }

    /*
     * Колонке разрешено быть уже своего содержимого — иначе фиксированная
     * раскладка не может сузить ячейку до заданной доли.
     *
     * БЕЗ `overflow-wrap: anywhere`. С ним перенос шёл по буквам:
     * «Косметичес‑кий ремонт», «Капитальны‑й ремонт». Слова переносятся между
     * словами, как и положено; название длиннее колонки просто выйдет за неё,
     * и это лучше, чем разорванное посередине слово.
     */
    .kits-table-title,
    .kits-table-type__title {
        min-width: 0;
    }

    /*
     * Самое длинное слово должно помещаться целиком.
     *
     * «Косметический» — 110 px при кегле 15, а колонка давала 87: слово либо
     * вылезало за белую плашку, либо (с `overflow-wrap: anywhere`) рвалось
     * посередине — «Косметичес‑кий». Перенос по слогам был бы правильным
     * ответом, и в макете он стоял руками через `&shy;`, но названия приходят
     * из прайса, а `hyphens: auto` без словаря переносов молча не работает —
     * проверено здесь же.
     *
     * Поэтому решает не перенос, а арифметика: кегль 13 (макет берёт такой же
     * на узких экранах) и доли 24/76 вместо 30/70. Проверено на месте: строка
     * заголовка занимает ровно ширину колонки, 97 из 97, без выхода за плашку.
     */
    .kits-table-type__title {
        font-size: 13px;
        line-height: 1.25;
        hyphens: auto;
        /*
         * Место под две строки — столько занимают четыре названия из пяти.
         *
         * «Евроремонт» помещается в одну, и вся его карточка съезжала вверх: цена
         * и оранжевый значок вставали на 8 px выше, чем у соседей. Пять значков
         * на разных уровнях читаются как сбитая вёрстка. Прижать значок к низу
         * (`margin-top:auto`, как на телефоне) здесь нельзя — автоматический
         * отступ съел бы свободное место, и `justify-content: center` перестал
         * бы центрировать карточку. Резервируем высоту у заголовка: карточка
         * остаётся центрированной, строки встают на общие линии.
         */
        min-height: calc(2 * 1.25em);
    }

    /*
     * Шапка колонки занимает всю высоту ячейки.
     *
     * «Евроремонт» — в одну строку, остальные в две, и его белая плашка
     * выходила 87 px против 106 у соседей: она висела по центру ячейки, а не
     * заполняла её. Процентная высота внутри таблицы считается только от
     * ячейки с заданной высотой — отсюда `height: 1px` у th: она не
     * ограничивает (ячейка всё равно растёт по содержимому), но делает
     * проценты разрешимыми.
     */
    .kits-table thead th {
        height: 1px;
    }

    .kits-table-type {
        display: flex;
        flex-direction: column;
        justify-content: center;
        height: 100%;
    }
}

/* === homepage/css/repair-services.css === */
/*
 * Витрина услуг «Ремонт помещений»: сетка карточек одной геометрии.
 *
 * ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ. style.min.css не редактируется (CLAUDE.md §8), а
 * макетные правила блока рассчитаны на другое устройство: список был Swiper'ом,
 * карточка — свободной высоты, кнопки шли сразу за текстом. При разной длине
 * названий и описаний это давало ряд, в котором ни одна горизонталь не
 * совпадала: заголовки на разной высоте, кнопки лесенкой, низ карточек рваный.
 *
 * ЧТО ЗДЕСЬ. Карточка — колонка с растущей контентной областью и прижатым к низу
 * блоком кнопок; заголовок и описание резервируют своё число строк, поэтому
 * совпадают не только верх и низ карточки, но и каждая строка внутри. Ничего не
 * обрезается «на середине»: тексты пишутся под этот объём, а `line-clamp`
 * страхует от случайно длинной правки.
 *
 * Правится отдельным файлом, а не style.min.css (CLAUDE.md §8).
 */

/* --------------------------------------------------------------- сетка -- */

/*
 * Четыре в ряд на desktop — и это ровно та сетка, что была в макете
 * (`flex: calc(25% - 20.3px)`), только без Swiper: список больше не слайдер.
 *
 * `grid` вместо `flex` сознательно: у грида ряд имеет высоту, и все карточки
 * ряда растягиваются до неё сами. На флексе то же самое пришлось бы держать
 * `align-items: stretch` плюс договорённость о ширине в трёх медиазапросах.
 */
.repair-list {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 27px;
    /* Макет на узких экранах ставит `flex-wrap: nowrap; gap: 0` — это было для
       карусели, и для сетки означает «все карточки в одну строку». */
    flex-wrap: wrap;
}

@media only screen and (max-width: 1400px) {
    .repair-list {
        gap: 20px;
    }
}

@media only screen and (max-width: 1100px) {
    .repair-list {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media only screen and (max-width: 900px) {
    .repair-list {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 16px;
    }
}

/* ------------------------------------------------------------ карточка -- */

/*
 * Карточка — колонка из трёх частей: картинка, контент, кнопки.
 *
 * `height: 100%` обязателен: элемент грида растянут по ряду, но его внутренняя
 * колонка об этом не знает, и кнопки прижимались бы к тексту, а не к низу.
 */
.repair-list__item {
    display: flex;
    flex-direction: column;
    height: 100%;
    /* Ширину задаёт сетка. Макетное `flex: calc(25% - 20.3px) 0 0` внутри грида
       считается от ячейки и делает карточку уже колонки. */
    flex: initial;
    min-width: 0;
}

/*
 * Картинка одной высоты у всех карточек — верх и низ на общей линии.
 *
 * Высота фиксированная, а не по пропорции файла: фотографии разных объектов
 * приходят с разным соотношением сторон, и «естественная» высота развалила бы
 * ряд. `object-fit: cover` кадрирует, а не растягивает.
 */
.repair-list__wrap-img {
    flex: 0 0 auto;
    height: 207px;
}

.repair-list__img,
.repair-list__img img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

@media only screen and (max-width: 900px) {
    .repair-list__wrap-img {
        height: 180px;
    }
}

/* Контентная часть растёт, кнопки уезжают вниз. */
.repair-list__content {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/*
 * Заголовок занимает две строки ВСЕГДА.
 *
 * «Ремонт зала» — одна строка, «Ремонт квартиры «евродвушки»» — две, и без
 * резерва описание под ними начиналось бы на разной высоте. Отсюда `min-height`
 * ровно в две строки: одна строка добирает пустую, три — обрезаются, но текст
 * названий приходит из каталога и в две строки укладывается.
 *
 * `line-height` здесь свой: макетные 85% рассчитаны на одну строку, на двух они
 * склеивают буквы.
 */
.repair-list__title {
    margin: 0 0 12px 0;
    font-size: 20px;
    line-height: 1.2;
    min-height: calc(2 * 1.2em);
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
}

/*
 * Описание — шесть строк, с резервом высоты.
 *
 * Число строк не выбрано, а посчитано на странице. Колонка отдаёт тексту 212 px,
 * в строку входит 27 знаков — но это потолок: переносы по словам съедают конец
 * почти каждой строки. Замер по всем двадцати восьми описаниям: тринадцати
 * хватает пяти строк, трём нужно шесть, самому длинному (122 знака) — ровно
 * шесть. Пяти строк не хватало четырём карточкам, и они обрывались на середине
 * фразы.
 *
 * `min-height` держит начало кнопок на общей линии, даже если описание короче;
 * `line-clamp` — страховка от правки длиннее нынешних, а не способ показывать
 * оборванное предложение.
 */
.repair-list__txt {
    flex: 1 1 auto;
    min-height: calc(6 * 1.4em);
}

.repair-list__txt p {
    margin: 0;
    line-height: 1.4;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 6;
    line-clamp: 6;
    overflow: hidden;
}

/*
 * Кнопки прижаты к низу карточки.
 *
 * `margin-top: auto` съедает весь остаток высоты, поэтому обе кнопки и нижняя
 * граница карточки стоят на общей линии у всего ряда — независимо от того,
 * сколько строк занял заголовок.
 */
.repair-list-actions {
    margin-top: auto;
    padding-top: 20px;
}

/* --------------------------------------------------- «Показать ещё» ----- */

/*
 * Скрытая карточка убирается из потока целиком.
 *
 * `visibility` или прозрачность оставили бы её место в сетке пустым, а строку —
 * доступной скринридеру и поиску как «видимая, но невидимая».
 */
.repair-list__item[hidden] {
    display: none;
}

.repair-more {
    display: flex;
    justify-content: center;
    margin: 28px 0 0;
}

/*
 * Оформление — как у остальных «показать ещё» на странице (`show-all-kits`,
 * `table-actions__btn`): пунктирная подчёркнутая ссылка фирменного цвета.
 * Своей кнопки блок не изобретает.
 */
.repair-more__btn {
    padding: 0;
    background: none;
    border: 0;
    border-bottom: 1px dashed #ff491a;
    font-weight: 700;
    font-size: 14px;
    line-height: 136%;
    color: #ff491a;
    cursor: pointer;
}

.repair-more__btn:hover {
    border-bottom-color: transparent;
}


/*
 * НА ТЕЛЕФОНЕ — ЛИСТАЛКА, А НЕ СТОЛБИК.
 *
 * Одна колонка означала девять карточек подряд: экран за экраном одинаковых
 * плашек, между которыми ничего не происходит. Пролистать их вбок — то же
 * движение, что в портфолио, и весь раздел занимает один экран.
 *
 * БЕЗ SWIPER. Прокрутка нативная: `scroll-snap` останавливает трек ровно на
 * карточке, инерция достаётся от системы, а ширины никто не пересчитывает —
 * ровно та беда, из-за которой слайдер уже дважды схлопывался в ноль
 * (CLAUDE.md §8). Здесь ломаться нечему: это обычный блок с прокруткой.
 */
@media only screen and (max-width: 560px) {
    .repair-list {
        display: flex;
        /*
         * `nowrap` обязателен и перебивает базовое правило сетки: с переносом
         * девять карточек по 295 px просто сложились обратно в столбик, а
         * прокручивать стало нечего — измерено, scrollWidth совпал с шириной
         * экрана.
         */
        flex-wrap: nowrap;
        grid-template-columns: none;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        gap: 12px;

        /*
         * Трек идёт до краёв экрана и возвращает отступ внутрь: карточка,
         * упирающаяся в край, читается как обрезанная, а выехавшая за него —
         * как продолжение.
         */
        margin: 0 -16px;
        padding: 0 16px;
        scroll-padding: 0 16px;
    }

    /*
     * 86%: следующая карточка выглядывает из-за края. Это и есть подсказка, что
     * список листается, — без неё нужны точки или стрелки, то есть ещё один
     * механизм на то же самое.
     */
    .repair-list__item {
        flex: 0 0 86%;
        scroll-snap-align: start;
    }

    /*
     * «Показать ещё» на телефоне не нужна: в треке лежат все карточки, и
     * доступны они свайпом. Кнопка, ничего не открывающая, — это обещание,
     * которого блок не выполняет.
     */
    .repair-more {
        display: none;
    }
}

/* === homepage/css/block-cta.css === */
/*
 * Кнопка действия под блоком — одна на все блоки, где она есть.
 *
 * ОТКУДА ПЕРЕЕХАЛО. Правила лежали в `matrix-table.css`, потому что первой их
 * получила матрица «Итоговая стоимость». Но `.matrix-actions` печатают три
 * блока — итоговая стоимость, сроки и прайс-лист, — и когда прайс понадобился
 * на отдельной странице «Цены», за двадцатью строками кнопки пришлось бы тянуть
 * 27 КБ геометрии матриц, которых на той странице нет вовсе. Компонент, которым
 * пользуются трое, живёт отдельно от блока, которому он когда-то принадлежал.
 *
 * ИМЯ КЛАССА ОСТАВЛЕНО КАК БЫЛО. Переименовать `.matrix-actions` в что-нибудь
 * нейтральное — значит поправить три шаблона ради красоты имени и получить
 * расхождение с любым местом, о котором забыли.
 */

.matrix-actions {
    display: flex;
    justify-content: center;
    margin: 28px 0 0;
}

.matrix-actions__btn {
    min-width: 260px;
    height: 56px;
}

@media only screen and (max-width: 560px) {
    .matrix-actions__btn {
        min-width: 0;
        width: 100%;
    }
}

/* === homepage/css/matrix-table.css === */
/*
 * ОБЩАЯ МАТРИЦА: «тип квартиры × вид ремонта».
 *
 * Один стандарт на все таблицы этого вида — «Итоговая стоимость» и «Типовые
 * сроки», а дальше и любые следующие. Второй файл с той же геометрией разошёлся
 * бы с первым на первой же правке, поэтому здесь лежит ВСЁ поведение, а в файле
 * блока — только ширины его колонок и то, что действительно принадлежит ему
 * одному.
 *
 * ТАБЛИЦА НЕ НОСИТ КЛАСС `table` ИЗ ВЁРСТКИ — И ЭТО РЕШЕНИЕ, А НЕ ЗАБЫВЧИВОСТЬ.
 * На том классе висит всё, что здесь мешало, и каждый пункт стоил замера:
 *   • скрипт вёрстки сворачивает длинные таблицы (`.table:not(.table--price)`)
 *     и вставляет «Показать все» ВНУТРЬ горизонтального скроллера — кнопка
 *     уезжала вместе с таблицей (замер: 16 → −384 при прокрутке на 400);
 *   • `.table{padding:0 0 0 8px}` на узких экранах — отступ слева «из ниоткуда»;
 *   • `border-spacing: 20px` одинаков между всеми колонками, поэтому соседние
 *     подколонки стояли так же далеко, как разные виды ремонта;
 *   • `.table tbody td:nth-child(2)::before` — пустой блок карточного режима, из
 *     за которого первая колонка значений вставала на 14px ниже соседней;
 *   • `.table td:nth-child(3)`, `:nth-child(5)` — разделители и отступы,
 *     рассчитанные на таблицу из ТРЁХ видов ремонта;
 *   • `.table th:first-child{display:none}` прятал колонку названий на мобиле;
 *   • `position: relative` у `thead`/`tbody`/`tr` отменял липкость колонки.
 * Классы `table-content`, `table-title`, `table-type` оставлены — они
 * самостоятельные, не привязаны к `.table`, и их базовый вид подходит.
 *
 * ПРАВИЛА, КОТОРЫЕ ЗДЕСЬ ДЕЙСТВУЮТ:
 *   • ширины колонок объявлены в `<colgroup>` — содержимое на них не влияет;
 *   • таблица занимает контейнер целиком, остаток забирает колонка названий;
 *   • промежуток внутри группы меньше, чем между группами, и линии разной силы;
 *   • переключатель типа строения меняет ТОЛЬКО значение: ни одной ячейки не
 *     добавляется и не убирается, поэтому сдвиг геометрии невозможен;
 *   • широкая матрица прокручивается ВНУТРИ себя; страница вбок не едет (§8), а
 *     название квартиры остаётся на месте.
 */

/* ------------------------------------------- переключатель типа строения -- */

/*
 * ПЕРЕКЛЮЧЕНИЕ БЕЗ СКРИПТА.
 *
 * В ячейке лежат значения по всем типам строения, видно отмеченное. Правила
 * привязаны к ПОРЯДКУ: N-й переключатель показывает N-е значение. Ключи типов
 * задаёт редактор, они произвольны, а порядок — нет: и переключатели, и значения
 * печатаются одним и тем же циклом.
 *
 * Почему не JS: подмена текста скриптом означала бы пересчёт ширин ячеек в
 * момент переключения, то есть ровно тот сдвиг, от которого мы уходим.
 *
 * ПОДДЕРЖИВАЕТСЯ ЧЕТЫРЕ ТИПА СТРОЕНИЯ — запас есть, но не бесконечный: пятый
 * потребует ещё одного блока правил. Это стерегут тесты блоков, они читают этот
 * файл и считают максимальный `nth-of-type`.
 *
 * Селектор несёт класс таблицы намеренно: на узких экранах вёрстка объявляет
 * `.table-content>span{display:flex}` (0-1-1), и одиночный класс ему проигрывал —
 * все значения раскладывались столбиком, строка вырастала с 44 до 74px.
 */
.matrix .matrix__value {
    display: none;
}

.matrix__switch:nth-of-type(1):checked ~ .matrix-wrap .matrix__value:nth-child(1),
.matrix__switch:nth-of-type(2):checked ~ .matrix-wrap .matrix__value:nth-child(2),
.matrix__switch:nth-of-type(3):checked ~ .matrix-wrap .matrix__value:nth-child(3),
.matrix__switch:nth-of-type(4):checked ~ .matrix-wrap .matrix__value:nth-child(4) {
    display: block;
}

/*
 * Отмеченная подпись. Вёрстка умеет это сама (`.price-table-filters__radio:checked
 * ~ .price-table-filters__txt`), но только когда input лежит рядом с подписью. У
 * нас он вынесен перед таблицей — иначе до ячеек не дотянуться, — поэтому три
 * состояния повторены здесь: жирная подпись, оранжевый круг, залитая точка.
 */
.matrix__switch:nth-of-type(1):checked ~ .price-table-filter .price-table-filters__item:nth-child(1) .price-table-filters__txt,
.matrix__switch:nth-of-type(2):checked ~ .price-table-filter .price-table-filters__item:nth-child(2) .price-table-filters__txt,
.matrix__switch:nth-of-type(3):checked ~ .price-table-filter .price-table-filters__item:nth-child(3) .price-table-filters__txt,
.matrix__switch:nth-of-type(4):checked ~ .price-table-filter .price-table-filters__item:nth-child(4) .price-table-filters__txt {
    font-weight: 700;
}

.matrix__switch:nth-of-type(1):checked ~ .price-table-filter .price-table-filters__item:nth-child(1) .price-table-filters__txt::before,
.matrix__switch:nth-of-type(2):checked ~ .price-table-filter .price-table-filters__item:nth-child(2) .price-table-filters__txt::before,
.matrix__switch:nth-of-type(3):checked ~ .price-table-filter .price-table-filters__item:nth-child(3) .price-table-filters__txt::before,
.matrix__switch:nth-of-type(4):checked ~ .price-table-filter .price-table-filters__item:nth-child(4) .price-table-filters__txt::before {
    border-color: #ff491a;
}

.matrix__switch:nth-of-type(1):checked ~ .price-table-filter .price-table-filters__item:nth-child(1) .price-table-filters__txt::after,
.matrix__switch:nth-of-type(2):checked ~ .price-table-filter .price-table-filters__item:nth-child(2) .price-table-filters__txt::after,
.matrix__switch:nth-of-type(3):checked ~ .price-table-filter .price-table-filters__item:nth-child(3) .price-table-filters__txt::after,
.matrix__switch:nth-of-type(4):checked ~ .price-table-filter .price-table-filters__item:nth-child(4) .price-table-filters__txt::after {
    opacity: 1;
    visibility: visible;
}

/* Подпись — метка, а не ссылка: курсор и запрет выделения при клике. */
.price-table-filters__txt[for] {
    cursor: pointer;
    user-select: none;
}

/* ------------------------------------------------------------- контейнер -- */

.matrix-wrap {
    /*
     * Тень у правого края — признак того, что справа есть продолжение.
     * «Крышка» цвета фона привязана к содержимому, тень — к контейнеру, поэтому
     * она гаснет на последней колонке. Без JS.
     */
    background-color: #fff;
    background-image:
        linear-gradient(to left, #fff, #fff),
        linear-gradient(to left, rgba(17, 17, 17, 0.12), rgba(17, 17, 17, 0));
    background-position: right center, right center;
    background-repeat: no-repeat;
    background-size: 14px 100%, 18px 100%;
    background-attachment: local, scroll;

    /*
     * Одна карточка, а не набор плиток.
     *
     * Рамка нарисована тенью, а не `border`: контейнер прокручивается, и рамка
     * съедала бы два пикселя его внутренней ширины. Замер поймал именно это —
     * таблица 1106px в контейнере 1104px давала прокрутку там, где прокручивать
     * нечего.
     */
    box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.7);
    border-radius: 6px;
}

/* --------------------------------------------------------------- колонки -- */

.matrix {
    /*
     * Ширины колонок — единственный источник геометрии. `fixed` обязателен:
     * только при нём `<colgroup>` командует, а содержимое ячеек — нет.
     *
     * Ширину колонок значений и `min-width` таблицы задаёт файл блока: у «цены»
     * две подколонки на вид ремонта, у «сроков» одна.
     */
    --matrix-name: 136px;
    /* Промежуток внутри группы и между группами — разные намеренно. */
    --matrix-inner: 4px;
    --matrix-outer: 14px;
    /* Отступ текста первой колонки от кромки карточки. */
    --matrix-name-pad: 16px;

    table-layout: fixed;
    border-collapse: separate;
    /* Промежутки делает padding ячеек: border-spacing одинаков для всех колонок
       и поэтому не умеет «внутри группы теснее». */
    border-spacing: 0;
    /*
     * ТАБЛИЦА ЗАНИМАЕТ КОНТЕЙНЕР ЦЕЛИКОМ, А СВОБОДНОЕ МЕСТО ЗАБИРАЕТ ПЕРВАЯ
     * КОЛОНКА.
     *
     * Ни пустого поля слева, ни хвоста справа. При этом лишние пиксели НЕ
     * расходятся по колонкам значений: у них фиксированная ширина из
     * `<colgroup>`, а «резиновая» только колонка типов квартир (`width: auto` —
     * при `fixed` она и получает весь остаток).
     */
    width: 100%;
    max-width: none;
    margin: 0;
    padding: 0;
    background: #fff;
    color: #333;
    font-size: 14px;
    text-align: center;
}

/*
 * Ширина не задана намеренно: колонка забирает остаток контейнера. Минимум ей
 * обеспечивает `min-width` таблицы из файла блока.
 */
.matrix__col--name {
    width: auto;
}

/* --------------------------------------------------------- шапка таблицы -- */

.matrix thead th {
    padding: 0;
    border: 0;
    vertical-align: bottom;
    text-align: center;
}

/*
 * УГЛОВАЯ ЯЧЕЙКА НАЗВАНА ПО ОБЕИМ ОСЯМ И РАЗДЕЛЕНА ПО ДИАГОНАЛИ.
 *
 * Сверху справа — то, что идёт по столбцам (вид ремонта), снизу слева — то, что
 * идёт по строкам (тип квартиры). Линия нарисована градиентом НА САМОЙ ЯЧЕЙКЕ: у
 * вложенного блока в `th` с `rowspan` проценты высоты не разрешаются — замер дал
 * линию в 40px при ячейке 113px. У `to top right` граница 50% проходит ровно
 * через углы при любых пропорциях, подгонять под ширину колонки не нужно.
 *
 * `position: relative` нужно подписям — они расставлены по углам абсолютом.
 */
.matrix thead th.matrix__corner {
    position: relative;
    padding: 0;
    border-right: 1px solid #e3e3e3;
    background-color: #fff;
    background-image: linear-gradient(
        to top right,
        transparent calc(50% - 0.5px),
        #d5d5d5 calc(50% - 0.5px),
        #d5d5d5 calc(50% + 0.5px),
        transparent calc(50% + 0.5px)
    );
}

.matrix__axes {
    display: block;
    height: 100%;
}

.matrix__axis {
    position: absolute;
    font-weight: 700;
    font-size: 12px;
    line-height: 1.2;
    color: #666;
    white-space: nowrap;
}

/* По столбцам — сверху справа, по строкам — снизу слева. */
.matrix__axis--columns {
    top: 10px;
    right: var(--matrix-name-pad);
}

.matrix__axis--rows {
    bottom: 10px;
    left: var(--matrix-name-pad);
}

/*
 * Заголовок группы стоит по центру ВСЕХ своих подколонок.
 *
 * Отступы слева и справа равны — иначе центр плашки не совпадает с центром
 * группы. Равенство проверяется замером: |центр плашки − центр группы| < 1px.
 *
 * Высота задана явно, чтобы `height: 100%` у плашки было от чего считать. Все
 * ячейки строки всё равно одной высоты, поэтому и плашки выходят одинаковыми
 * независимо от того, в одну строку название или в две.
 */
.matrix thead th.matrix__group {
    height: 96px;
    padding: 0 var(--matrix-outer) 8px;
    vertical-align: top;
}

/*
 * ПЛАШКА: НАЗВАНИЕ СВЕРХУ, РАСПОР, ИКОНКА У ПОЛА.
 *
 * Было `justify-content: center` — центрировался весь контент разом, и от числа
 * строк в названии зависело всё: замер дал плашки 62px и 81px в ячейке 87px,
 * названия на разных линиях. Теперь название прижато к верху, иконку отжимает
 * вниз `margin-top: auto`, а плашка тянется на всю высоту ячейки — все плашки
 * одинаковы по построению, а не по совпадению длин слов.
 */
.matrix .table-type {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    height: 100%;
    min-height: 0;
    padding: 12px 6px 8px;
    text-align: center;
}

.matrix .table-type__title {
    text-align: center;
}

.matrix .table-type__link {
    /* Распор: иконка уезжает к нижнему краю плашки, а не липнет к тексту. */
    margin: auto 0 0;
}

/*
 * ПОЛОСА ПОДЗАГОЛОВКОВ — РОВНАЯ СТРОКА ШАПКИ.
 *
 * Когда её клетки носили чередование групп, полоса выглядела набором пятен и
 * казалось, что подписи поменялись местами (в DOM они не менялись — замер
 * показывал совпадение x и width каждой подписи со своей колонкой до пикселя).
 * Общий нейтральный фон, нижняя граница, один размер и цвет; границы групп
 * рисуют разделители.
 */
.matrix thead th.matrix__sub {
    height: 34px;
    padding-top: 6px;
    padding-bottom: 6px;
    background: #f6f6f6;
    border-bottom: 1px solid #e6e6e6;
    font-weight: 500;
    font-size: 12px;
    line-height: 1.2;
    color: #666;
    vertical-align: middle;
}

/* ------------------------------------------------------------- промежутки -- */

/*
 * ЛИНИИ РАЗНОЙ СИЛЫ: внутри группы тоньше, между группами заметнее.
 *
 * Разделена каждая колонка — иначе соседние значения сливаются в одно поле. Но
 * линия внутри группы светлее: если сделать обе одинаковыми, таблица
 * рассыпается на равноправные столбцы, а группа вида ремонта перестаёт читаться
 * как целое. Отсюда же «не хаотичные карточки»: внутри группы 8px, между
 * группами 28px и по центру этого промежутка — разделитель.
 *
 * Группа может состоять из одной колонки: тогда её единственная ячейка — сразу и
 * первая, и последняя, и получает внешние отступы с обеих сторон.
 */
.matrix th.matrix__sub,
.matrix td.matrix__cell {
    padding-left: var(--matrix-inner);
    padding-right: var(--matrix-inner);
    border-right: 1px solid #f2f2f2;
}

.matrix th.matrix__sub--start,
.matrix td.matrix__cell--start {
    padding-left: var(--matrix-outer);
}

.matrix th.matrix__sub--end,
.matrix td.matrix__cell--end {
    padding-right: var(--matrix-outer);
    border-right: 1px solid #e3e3e3;
}

/* Последняя группа граничит с рамкой контейнера — второй линии не нужно. */
.matrix th.matrix__sub:last-child,
.matrix td.matrix__cell:last-child {
    border-right: 0;
}

/* Чередование групп — чтобы подколонки читались как одна колонка. */
.matrix__group--alt {
    background: #fbfbfb;
}

/* --------------------------------------------------------------- строки -- */

.matrix tbody td,
.matrix tbody th {
    height: 54px;
    padding-top: 0;
    padding-bottom: 0;
    border-top: 1px solid #f1f1f1;
    vertical-align: middle;
}

/*
 * Название строки — ЗАГОЛОВОК СТРОКИ, а не обычная ячейка.
 *
 * ЗАЧЕМ ПРАВКА. В матрице цен одиннадцать столбцов, в матрице сроков шесть.
 * Пока первая ячейка была `<td>`, у строки не было заголовка вовсе: скринридер,
 * оказавшись на сумме в середине таблицы, называл столбец («Итого») и молчал о
 * том, к какой квартире эта сумма относится. Разбирающая страницу машина по той
 * же причине не могла отличить подпись строки от данных. `<th scope="row">`
 * говорит и тому и другому одно и то же одним словом.
 *
 * ВИД НЕ МЕНЯЕТСЯ, НО БРАУЗЕР ЭТОГО НЕ ЗНАЕТ: у `<th>` собственные умолчания —
 * жирное начертание и выравнивание по центру. Оба гасятся здесь, а замер до и
 * после правки показывает те же координаты, ширины и высоты.
 */
.matrix th.matrix__name-cell {
    padding: 0;
    border-right: 1px solid #e3e3e3;
    text-align: left;
    font-weight: 400;
}

.matrix .table-content {
    /* Белая плашка с радиусом внутри каждой ячейки и создавала «карточки». */
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    min-height: 0;
    padding: 6px 0;
    background: none;
    border-radius: 0;
    font-size: 14px;
    line-height: 1.25;
    text-align: center;
}

/*
 * Значение не переносится: «2 000 ₽ /» и «м²» на двух строках — это две разные
 * величины на вид. Ширина колонки под него и рассчитана.
 */
.matrix .table-content span {
    white-space: nowrap;
}

/*
 * ПСЕВДОПОДПИСЬ ЯЧЕЙКИ ГАСИТСЯ — ОНА НЕВИДИМА, НО ЗАНИМАЕТ МЕСТО.
 *
 * `.table-content::before` — подпись столбца карточного режима. Текста в ней у
 * нас нет (`content: ""`), но она остаётся flex-элементом с `padding: 7px 8px`,
 * то есть 18px ширины: замер на 1024 показал значение, сдвинутое на 18px вправо
 * и вылезшее за правый край ячейки на 4px во всех тридцати клетках. Пустой
 * элемент, который толкает содержимое, — худший вид дефекта: на глаз его нет.
 */
.matrix .table-content::before,
.matrix .table-content::after {
    content: none;
    display: none;
}

/*
 * Значение — просто текст, а не плашка. На узких экранах вёрстка одевает каждый
 * `.table-content > span` в белую рамку с радиусом и `min-height: 29px`.
 * `display` здесь НЕ трогается: им управляет переключатель типа строения.
 */
.matrix .table-content > span {
    width: auto;
    min-height: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
}

.matrix .table-title {
    /*
     * ПЛОЩАДЬ ПОД НАЗВАНИЕМ, А НЕ РЯДОМ С НИМ.
     *
     * `.table-title` в вёрстке — flex-строка с `nowrap`, поэтому `display:block`
     * у площади не значил ничего: flex делает элементы соседями по горизонтали.
     * Замер поймал название и площадь на одной линии, причём площадь заходила в
     * правый отступ ячейки.
     */
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    min-height: 0;
    /*
     * Слева — небольшой отступ от кромки карточки: ноль читался как «в упор», а
     * отступ вёрстки (19px) отодвигал колонку слишком далеко. Край САМОЙ колонки
     * при этом стоит на линии контента блока.
     */
    padding: 6px var(--matrix-inner) 6px var(--matrix-name-pad);
    background: none;
    border-radius: 0;
    white-space: normal;
    font-size: 14px;
    line-height: 1.25;
    text-align: left;
}

/*
 * НАЗВАНИЕ КВАРТИРЫ НЕ РАЗРЫВАЕТСЯ.
 *
 * «1-комнатная» содержит дефис, и браузер считает его законным местом переноса:
 * замер показал «1-» на одной строке и «комнатная» на второй. Строки таблицы при
 * этом становились разной высоты — у «Студии» перенос не случался.
 */
.matrix__name {
    white-space: nowrap;
    font-weight: 700;
}

/*
 * Диапазон площади — пояснение к названию, а не второй заголовок. Своя строка и
 * тоже неразрывен: «(30–45 м²)» разлеталось на «(30–», «45», «м²)». Это единица
 * измерения, читать её по частям нельзя (CLAUDE.md §7.3).
 */
.matrix__area {
    display: block;
    white-space: nowrap;
    font-weight: 400;
    font-size: 12px;
    color: #999;
}

/*
 * Фраза вместо значения («Рассчитаем индивидуально», «Уточним при расчёте»):
 * длинная, и в отличие от числа её переносить нужно — это предложение (§7.3).
 */
.matrix__no-value {
    white-space: normal !important;
    font-size: 12px;
    line-height: 1.2;
    color: #999;
}

/* ------------------------------------------------------------------ CTA -- */
/*
 * Кнопка под блоком переехала в `block-cta.css`: её печатают ещё сроки и
 * прайс-лист, а прайс с недавних пор живёт и на отдельной странице «Цены», где
 * матриц нет вовсе. Тянуть туда весь этот файл ради двадцати строк кнопки
 * значило бы платить 27 КБ за то, чего на странице не существует.
 */

/* ------------------------------------------------------ узкие экраны ----- */

@media only screen and (max-width: 1100px) {
    /* Колонки теснее — числа считает файл блока по той же формуле. */
    .matrix {
        --matrix-name: 128px;
        --matrix-outer: 8px;
        --matrix-inner: 3px;
        --matrix-name-pad: 10px;
    }

    /*
     * Название квартиры остаётся на месте при прокрутке вбок.
     *
     * Целятся КЛАССЫ, а не `:first-child`: в шапке две строки, и во второй
     * первая ячейка — подпись первой группы. Позиционный селектор сделал бы
     * липкой её. `thead` в селекторе не для красоты: базовое правило угловой
     * ячейки несёт его же, и без него `position: relative` победило бы липкость.
     */
    .matrix thead th.matrix__corner,
    .matrix tbody th.matrix__name-cell {
        position: sticky;
        left: 0;
        z-index: 2;
        /*
         * ТОЛЬКО `background-color`. Сокращённое `background: #fff` сбрасывает
         * `background-image` — а на угловой ячейке им нарисована диагональ.
         */
        background-color: #fff;
        box-shadow: 6px 0 6px -6px rgba(17, 17, 17, 0.16);
    }

    .matrix .table-title {
        font-size: 13px;
    }

    .matrix__area {
        font-size: 11px;
    }

    .matrix .table-content {
        font-size: 13px;
    }

    .matrix tbody td {
        height: 48px;
    }

    .matrix thead th.matrix__sub {
        height: 30px;
        font-size: 11px;
    }

    /* Плашка ниже: на узком экране название печатается сокращением. */
    .matrix thead th.matrix__group {
        height: 74px;
        padding-bottom: 6px;
    }

    .matrix .table-type {
        padding: 8px 4px 6px;
    }

    .matrix__axes {
        padding: 8px var(--matrix-name-pad);
    }

    .matrix__axis {
        font-size: 10px;
    }
}

/* === homepage/css/final-price.css === */
/*
 * «Итоговая стоимость ремонта»: ширины колонок и то, что принадлежит только
 * этому блоку.
 *
 * ВСЁ ПОВЕДЕНИЕ МАТРИЦЫ — В `matrix-table.css`: переключатель типа строения,
 * диагональная угловая ячейка, геометрия плашек, полоса подзаголовков, линии,
 * липкая колонка, узкие экраны. Здесь остаются только числа, которые у каждого
 * блока свои: у этого на вид ремонта ДВЕ подколонки — ставка и итог.
 *
 * style.min.css не редактируется (CLAUDE.md §8), поэтому правки живут отдельным
 * файлом.
 */

.table--final-price {
    --fp-rate: 84px;
    --fp-total: 110px;

    /*
     * Минимальная ширина считается из числа видов ремонта, которое печатает
     * шаблон: шестой вид сделает таблицу шире контейнера, и она начнёт
     * прокручиваться внутри себя, а не сожмёт колонки до нечитаемого.
     */
    min-width: calc(var(--matrix-name) + var(--price-types, 5) * (var(--fp-rate) + var(--fp-total)));
}

.final-price__col--rate {
    width: var(--fp-rate);
}

.final-price__col--total {
    width: var(--fp-total);
}

@media only screen and (max-width: 1100px) {
    .table--final-price {
        --fp-rate: 78px;
        --fp-total: 102px;
    }
}

@media only screen and (max-width: 360px) {
    /* На 320px рядом с названием должна помещаться целая группа. */
    .table--final-price {
        --matrix-name: 112px;
        --matrix-outer: 6px;
        --fp-rate: 72px;
        --fp-total: 96px;
    }
}

/* === homepage/css/times.css === */
/*
 * «Типовые сроки ремонта»: ширины колонок и то, что принадлежит только этому
 * блоку.
 *
 * ВСЁ ПОВЕДЕНИЕ МАТРИЦЫ — В `matrix-table.css`, том же, что у «Итоговой
 * стоимости»: переключатель типа строения, диагональная угловая ячейка,
 * геометрия плашек, полоса подзаголовков, линии, липкая колонка, узкие экраны.
 * Здесь только числа: у этого блока на вид ремонта ОДНА колонка — срок.
 */

.table--times {
    --times-term: 124px;

    /*
     * Минимальная ширина считается из числа колонок, которое печатает шаблон.
     * Колонок здесь ровно столько, у скольких видов ремонта назван хоть один
     * срок, поэтому появление евроремонта в данных пересчитает ширину само.
     */
    min-width: calc(var(--matrix-name) + var(--times-columns, 4) * var(--times-term));
}

/*
 * ШИРИНА РАЗДАЁТСЯ ДОЛЯМИ, А НЕ ОСТАТКОМ.
 *
 * У этого блока колонок вдвое меньше, чем у матрицы цен: при фиксированных
 * колонках срока весь остаток контейнера доставался колонке названий — замер на
 * 1440 дал ей 506px при тексте в полтораста. Доли делят место поровну между
 * колонками сроков и оставляют названию долю чуть больше — оно и длиннее.
 *
 * Числа считаются из количества колонок: `--times-columns` печатает шаблон, и
 * пятая колонка (когда появятся сроки евроремонта) сама сузит остальные, ничего
 * здесь не меняя.
 */
.table--times .matrix__col--name {
    width: calc(100% / (var(--times-columns, 4) + 1.4) * 1.4);
}

.times__col--term {
    width: calc(100% / (var(--times-columns, 4) + 1.4));
}

@media only screen and (max-width: 1100px) {
    .table--times {
        --times-term: 118px;
    }
}

@media only screen and (max-width: 360px) {
    /* На 320px рядом с названием должна помещаться целая колонка срока. */
    .table--times {
        --matrix-name: 112px;
        --matrix-outer: 6px;
        --times-term: 110px;
    }
}

/* === homepage/css/price-list.css === */
/*
 * «Прайс-лист на ремонтные работы»: три колонки — работа, единица, цена.
 *
 * ПОЧЕМУ НЕ `matrix-table.css`. Тот файл описывает матрицу «строка × колонка» с
 * группами подколонок и липкой первой колонкой. Здесь обычный список из трёх
 * колонок, и половина того поведения ему не нужна. Общее у них — язык:
 * карточка с рамкой, полоса шапки, тонкие линии между строками, одна высота у
 * всех строк. style.min.css не редактируется (CLAUDE.md §8).
 *
 * ПЕРЕКЛЮЧЕНИЕ ВКЛАДОК БЕЗ СКРИПТА: N-я радиокнопка показывает N-ю панель. Тот
 * же приём, что у переключателя типа строения в матрицах, и по той же причине —
 * скрипт пересобирал бы разметку в момент клика.
 *
 * ПОДДЕРЖИВАЕТСЯ ДВЕНАДЦАТЬ ВКЛАДОК. Сегодня их восемь; запас есть, но не
 * бесконечный, и его стережёт тест: тринадцатая вкладка молча осталась бы
 * пустой.
 */

.price-list__switch {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
}

.price-list__pane {
    display: none;
}

.price-list__switch:nth-of-type(1):checked ~ .price-list__panes .price-list__pane:nth-child(1),
.price-list__switch:nth-of-type(2):checked ~ .price-list__panes .price-list__pane:nth-child(2),
.price-list__switch:nth-of-type(3):checked ~ .price-list__panes .price-list__pane:nth-child(3),
.price-list__switch:nth-of-type(4):checked ~ .price-list__panes .price-list__pane:nth-child(4),
.price-list__switch:nth-of-type(5):checked ~ .price-list__panes .price-list__pane:nth-child(5),
.price-list__switch:nth-of-type(6):checked ~ .price-list__panes .price-list__pane:nth-child(6),
.price-list__switch:nth-of-type(7):checked ~ .price-list__panes .price-list__pane:nth-child(7),
.price-list__switch:nth-of-type(8):checked ~ .price-list__panes .price-list__pane:nth-child(8),
.price-list__switch:nth-of-type(9):checked ~ .price-list__panes .price-list__pane:nth-child(9),
.price-list__switch:nth-of-type(10):checked ~ .price-list__panes .price-list__pane:nth-child(10),
.price-list__switch:nth-of-type(11):checked ~ .price-list__panes .price-list__pane:nth-child(11),
.price-list__switch:nth-of-type(12):checked ~ .price-list__panes .price-list__pane:nth-child(12) {
    display: block;
}

/* Отмеченная вкладка. */
.price-list__switch:nth-of-type(1):checked ~ .price-list__nav .price-list__tab:nth-of-type(1),
.price-list__switch:nth-of-type(2):checked ~ .price-list__nav .price-list__tab:nth-of-type(2),
.price-list__switch:nth-of-type(3):checked ~ .price-list__nav .price-list__tab:nth-of-type(3),
.price-list__switch:nth-of-type(4):checked ~ .price-list__nav .price-list__tab:nth-of-type(4),
.price-list__switch:nth-of-type(5):checked ~ .price-list__nav .price-list__tab:nth-of-type(5),
.price-list__switch:nth-of-type(6):checked ~ .price-list__nav .price-list__tab:nth-of-type(6),
.price-list__switch:nth-of-type(7):checked ~ .price-list__nav .price-list__tab:nth-of-type(7),
.price-list__switch:nth-of-type(8):checked ~ .price-list__nav .price-list__tab:nth-of-type(8),
.price-list__switch:nth-of-type(9):checked ~ .price-list__nav .price-list__tab:nth-of-type(9),
.price-list__switch:nth-of-type(10):checked ~ .price-list__nav .price-list__tab:nth-of-type(10),
.price-list__switch:nth-of-type(11):checked ~ .price-list__nav .price-list__tab:nth-of-type(11),
.price-list__switch:nth-of-type(12):checked ~ .price-list__nav .price-list__tab:nth-of-type(12) {
    background: #ff491a;
    border-color: #ff491a;
    color: #fff;
}

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

.price-list__updated {
    margin: 8px 0 0;
    font-size: 14px;
    color: #999;
}

/* ---------------------------------------------------------------- вкладки -- */

.price-list__nav-title {
    margin: 0 0 10px;
    font-weight: 700;
    font-size: 15px;
    color: #333;
}

.price-list__nav-track {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 20px;
}

.price-list__tab {
    display: inline-flex;
    align-items: center;
    padding: 9px 16px;
    background: #fff;
    border: 1px solid #e6e6e6;
    border-radius: 999px;
    font-size: 14px;
    line-height: 1.2;
    color: #333;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.price-list__tab:hover {
    border-color: #ff491a;
}

/* ----------------------------------------------------------------- таблица -- */

.price-list__card {
    background: #fff;
    border-radius: 6px;
    /* Рамка тенью: карточка не должна съедать два пикселя внутренней ширины. */
    box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.7);
    overflow: hidden;
}

.price-list__table {
    table-layout: fixed;
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
    margin: 0;
    color: #333;
    font-size: 15px;
}

/*
 * Ширины объявлены в `<colgroup>`: содержимое ячеек на них не влияет, поэтому
 * длинное название работы не может сузить колонку цены.
 */
.price-list__col--work {
    width: auto;
}

.price-list__col--unit {
    width: 120px;
}

.price-list__col--price {
    width: 160px;
}

.price-list__head {
    height: 44px;
    padding: 0 16px;
    background: #f6f6f6;
    border-bottom: 1px solid #e6e6e6;
    font-weight: 500;
    font-size: 13px;
    color: #666;
    text-align: left;
    vertical-align: middle;
}

.price-list__head--unit,
.price-list__head--price {
    text-align: center;
}

.price-list__row--extra {
    display: none;
}

.price-list__row--shown {
    display: table-row;
}

.price-list__cell {
    height: 52px;
    padding: 8px 16px;
    border-top: 1px solid #f1f1f1;
    vertical-align: middle;
}

.price-list__row:first-child .price-list__cell {
    border-top: 0;
}

.price-list__cell--unit,
.price-list__cell--price {
    text-align: center;
}

.price-list__cell--price {
    font-weight: 700;
    /* Цена не переносится: «600» и «₽» на разных строках — это другая цена. */
    white-space: nowrap;
}

/*
   Название работы печатается прямо в ячейку. Раньше оно было обёрнуто в
   <span class="price-list__work"> ради `display: block` — но ячейка таблицы и
   так блочная, и обёртка не делала ничего, кроме 795 лишних узлов DOM.
   Межстрочное расстояние переехало на саму ячейку.
*/
.price-list__cell--work {
    line-height: 1.3;

    /*
     * ДЛИННОЕ СЛОВО ПЕРЕНОСИТСЯ, А НЕ ВЫЛЕЗАЕТ ИЗ КОЛОНКИ. Ширины колонок
     * заданы, и слово шире колонки некуда девать: оно выходит за ячейку и
     * налезает на соседнюю. В данных такое есть — «Демонтаж розеток,
     * выключателей,светильников» приехал из legacy без пробела после запятой,
     * и на 390px эти 25 символов дают 206px в ячейке шириной 194.
     *
     * Чинится здесь, а не правкой названия: следующая длинная позиция появится
     * при первом же импорте, и переписывать данные под вёрстку — не тот
     * порядок. `break-word` рвёт слово ТОЛЬКО когда оно не помещается целиком,
     * поэтому обычные названия переносятся по-прежнему по пробелам.
     */
    overflow-wrap: break-word;
}

/* Цены нет — фраза, а не ноль (§7.3): ноль обещал бы бесплатную работу. */
.price-list__no-price {
    font-weight: 400;
    font-size: 13px;
    color: #999;
    white-space: normal;
}

/* ------------------------------------------------------------ смотреть ещё -- */

.price-list__more {
    display: flex;
    justify-content: center;
    padding: 14px 16px 18px;
    border-top: 1px solid #f1f1f1;
}

.price-list__more-btn {
    padding: 0;
    background: none;
    border: 0;
    border-bottom: 1px dashed #ff491a;
    font-size: 15px;
    line-height: 1.4;
    color: #ff491a;
    cursor: pointer;
}

.price-list__more-btn:hover {
    border-bottom-color: transparent;
}

/* ------------------------------------------------------ узкие экраны ----- */

/* ------------------------------------------------------- прокрутка таблицы -- */

/*
 * ШИРИНЫ КОЛОНОК ОДНИ НА ВСЕХ ЭКРАНАХ, А НЕ ПРЯЧУТСЯ.
 *
 * Раньше на мобильном единица и цена скрывались `display: none`, а их `<col>`
 * оставались с нулевой шириной. При `table-layout: fixed` строка теряет ячейку,
 * и WebKit раздаёт ширину по содержимому оставшейся колонки, а не по ширине
 * таблицы: на iPhone таблица сжималась в узкую полосу слева, справа внутри
 * карточки зияла пустота. Blink в той же разметке держал 100%, поэтому дефекта
 * не было видно ни в одном локальном замере — только на реальном телефоне.
 *
 * Теперь колонки объявлены в пикселях и не скрываются никогда. Не помещаются —
 * таблица прокручивается ВНУТРИ своего контейнера; страница вбок не едет (§8).
 */
.price-list__scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.price-list__table {
    --pl-unit: 92px;
    --pl-price: 116px;
    /* Меньше этого названию работы уже не хватает на осмысленную строку. */
    --pl-name: 200px;

    min-width: calc(var(--pl-name) + var(--pl-unit) + var(--pl-price));
}

.price-list__col--unit {
    width: var(--pl-unit);
}

.price-list__col--price {
    width: var(--pl-price);
}

@media only screen and (max-width: 767px) {
    .price-list__table {
        --pl-unit: 72px;
        --pl-price: 92px;
        /* 160 + 164 = 324: на 375px влезает в карточку без прокрутки, на 320 —
           прокручивается внутри таблицы, как и задумано. */
        --pl-name: 160px;

        font-size: 14px;
    }

    .price-list__cell {
        height: auto;
        padding: 10px 12px;
    }

    .price-list__head {
        padding: 0 12px;
    }

    .price-list__subsection {
        padding: 10px 12px 6px;
    }

    /*
     * Лента вкладок прокручивается внутри блока, но НЕ вплотную к краю экрана:
     * отрицательные поля выносили её за отступы страницы, и первая вкладка
     * начиналась от самой кромки. Отступ теперь тот же, что у остальных
     * мобильных блоков.
     */
    .price-list__nav-track {
        flex-wrap: nowrap;
        overflow-x: auto;
        scroll-snap-type: x proximity;
        margin: 0 0 18px;
        padding: 0 0 4px;
        -webkit-overflow-scrolling: touch;
    }

    .price-list__tab {
        scroll-snap-align: start;
    }
}

/* ------------------------------------------------- аккордеоны разделов -- */

/*
 * СТРУКТУРА ПОВТОРЯЕТ СТАРЫЙ САЙТ: вкладка → аккордеоны → позиции.
 *
 * Аккордеон — `<details>`, а не скрипт: раскрытие работает без JS, состояние
 * держит браузер, и разметка при этом не пересобирается.
 */
.price-list__section {
    margin: 0 0 10px;
    background: #fff;
    border-radius: 6px;
    box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.7);
    overflow: hidden;
}

.price-list__section-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

/* Стрелка рисуется сама — родная у Safari и Chrome выглядит по-разному. */
.price-list__section-head::-webkit-details-marker {
    display: none;
}

.price-list__section-head::before {
    content: "";
    flex: 0 0 auto;
    width: 3px;
    height: 18px;
    background: #ff491a;
    border-radius: 2px;
}

.price-list__section-name {
    flex: 1 1 auto;
    font-weight: 500;
    font-size: 15px;
    line-height: 1.3;
    color: #333;
}

.price-list__section-count {
    flex: 0 0 auto;
    min-width: 26px;
    padding: 2px 8px;
    background: #f6f6f6;
    border-radius: 999px;
    font-size: 12px;
    color: #999;
    text-align: center;
}

.price-list__section-head::after {
    content: "+";
    flex: 0 0 auto;
    width: 20px;
    font-size: 20px;
    line-height: 1;
    color: #ff491a;
    text-align: center;
}

.price-list__section[open] .price-list__section-head::after {
    content: "−";
}

/* Внутри аккордеона карточка уже не нужна: рамку держит сам аккордеон. */
.price-list__section .price-list__card {
    border-radius: 0;
    box-shadow: none;
    border-top: 1px solid #f1f1f1;
}

/*
 * Подзаголовок внутри раздела: подкатегория legacy («Демонтаж стен») или
 * разделитель из файла («СТЕНЫ»). Одинаковые названия под разными
 * разделителями — разные позиции, и подпись объясняет, почему цена другая.
 */
.price-list__subsection {
    padding: 10px 16px 6px;
    background: #fbfbfb;
    border-top: 1px solid #f1f1f1;
    font-weight: 700;
    font-size: 12px;
    line-height: 1.2;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

/* === homepage/css/consult.css === */
/* ==========================================================================
   Блок консультации «Остались вопросы по ремонту?»

   ПОЧЕМУ ФОН НЕ СВОЙСТВО БЛОКА. В макете `.wrap-call` несёт
   `background: url(../img/bgs/desktop/call-bg.jpg)` — то есть кадр зашит в
   стиль, один на 354 города, и заменить его из админки нельзя. Здесь картинка
   приходит из слайда и лежит отдельным слоем `<img>`: так работают srcset,
   webp-двойник и lazy-загрузка, и на телефон уезжает копия на 400/800 px, а не
   полноразмерный кадр. Макетный фон поэтому гасится — иначе браузер скачал бы
   обе картинки, а нижняя всё равно не видна.

   ЗАТЕМНЕНИЕ — ОТДЕЛЬНЫЙ СЛОЙ. Редактор поставит светлый кадр, и белый
   заголовок на нём исчезнет; подбирать «файл потемнее» — это чинить вёрстку
   картинкой. Степень задаётся атрибутом `data-overlay` на блоке.

   ВЫСОТУ ЗАДАЁТ СОДЕРЖИМОЕ. Никаких min-height под «красивую» полосу: пустое
   место снизу на мобильной — это экран, который посетитель листает зря.
   ========================================================================== */

.consult {
    position: relative;
    overflow: hidden;
    /* Заливка под фотографией: она же и есть фон, когда картинки нет. */
    background-color: #1d2939;
    background-image: none;
    color: #fff;
    isolation: isolate;
}

/* Слой с фотографией. `inset: 0` вместо width/height: у картинки свои размеры,
   и растягивать её процентами пришлось бы дважды. */
.consult__bg {
    position: absolute;
    inset: 0;
    z-index: -2;
}

.consult__bg picture,
.consult__bg img {
    display: block;
    width: 100%;
    height: 100%;
}

/* Кадр горизонтальный и широкий (1321×324), блок при узком экране заметно
   выше — поэтому обрезаем по центру, а не растягиваем пропорции. */
.consult__bg img {
    object-fit: cover;
    object-position: center;
}

/* Затемнение живёт на самом блоке, а не на группе строк или обёртке картинки:
   позиционированный предок у псевдоэлемента должен быть очевидным (CLAUDE.md §8). */
.consult--photo::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(16, 24, 40, var(--consult-overlay, .55));
    pointer-events: none;
}

.consult[data-overlay='none']::before { --consult-overlay: 0; }
.consult[data-overlay='light']::before { --consult-overlay: .35; }
.consult[data-overlay='medium']::before { --consult-overlay: .55; }
.consult[data-overlay='strong']::before { --consult-overlay: .72; }

/* Ширину содержимого задаёт контейнер страницы — тот же, что у соседних блоков
   (замер на 1920: блок и прайс-лист оба 1321px). Своего `max-width` здесь нет
   намеренно: кап в 1100px оставлял справа 161px пустоты, и строка формы
   заканчивалась заметно раньше края блока. Длину строки текста ограничивает сам
   абзац — это типографика, а не ширина блока. */
.consult__inner {
    position: relative;
}

.consult .title h2 span {
    color: #ff491a;
}

.consult .title p {
    max-width: 780px;
    color: rgba(255, 255, 255, .88);
}

/* --- Строка формы -------------------------------------------------------- */

/*
   Desktop: Имя | Телефон | кнопка — одной линией.

   `align-items: stretch` вместо макетного `center`: центрирование оставляет
   каждому элементу его собственную высоту, и кнопка (56px) с полем (56px)
   совпадают только пока обе константы не разошлись. Растяжение делает равную
   высоту свойством строки, а не совпадением двух чисел.
*/
.consult__row {
    display: flex;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 12px;
    margin: 0;
}

.consult__row > .form__item {
    flex: 1 1 220px;
    /* min-width: 0 — иначе flex-элемент не сожмётся уже своего содержимого и
       строка вылезет за контейнер (та же болезнь, что у калькулятора). */
    min-width: 0;
    display: block;
}

.consult__row .form__wrap-input,
.consult__row .form__input {
    width: 100%;
}

/*
   РАВНАЯ ВЫСОТА — ТОЛЬКО ГДЕ ЕСТЬ СТРОКА.

   `height: 100%` считается от высоты родителя, а у колонки она задана
   содержимым: процент разрешать не от чего, и кнопка падает до высоты своей
   надписи. Замер на 768px показал 27px против 56px у полей. Поэтому растяжение
   живёт в том же медиазапросе, что и сама строка, а в столбик высоту задаёт
   макет — те же 56/50px, что у соседних форм сайта.
*/
@media only screen and (min-width: 1101px) {
    .consult__row .form__wrap-input,
    .consult__row .form__input {
        height: 100%;
    }
}

/* Кнопка не уже и не ниже соседей: своя колонка и высота строки.

   СЕЛЕКТОР ДЛИННЫЙ НАМЕРЕННО. Макет держит `.call .form__items .form__item
   :last-child { max-width: 210px }` — это 0-4-0, и короткий `.consult__submit`
   (0-1-0) ему проигрывает: замер показал кнопку 210px там, где надпись просит
   больше. Перебиваем классом ячейки вместе с классами блока и строки — та же
   специфичность, а наш файл подключён позже (CLAUDE.md §8). `!important` для
   этого не нужен. */
.consult .consult__row .form__item.consult__submit {
    flex: 0 0 auto;
    max-width: none;
}

.consult .consult__row .form__item.consult__submit .form__btn {
    width: 100%;
    min-width: 260px;
    margin: 0;
}

@media only screen and (min-width: 1101px) {
    .consult .consult__row .form__item.consult__submit .form__btn {
        height: 100%;
    }
}

/* --- Согласия ------------------------------------------------------------ */

.consult__consents {
    margin: 16px 0 0;
    max-width: 780px;
}

.consult__consents .lead-consent__text,
.consult__consents .form-privacy__txt {
    color: rgba(255, 255, 255, .82);
}

.consult__consents a {
    color: #fff;
}

/* --- Мобильная ----------------------------------------------------------- */

@media only screen and (max-width: 1100px) {
    /* Всё в столбик и во всю ширину: заголовок, текст, имя, телефон, кнопка,
       согласия. Кнопка не может выйти за экран, потому что её ширину задаёт
       колонка, а не min-width. */
    .consult__row {
        flex-direction: column;
        gap: 10px;
    }

    .consult__row > .form__item {
        flex: 1 1 auto;
        width: 100%;
    }

    .consult .consult__row .form__item.consult__submit .form__btn {
        min-width: 0;
        width: 100%;
    }

    .consult__consents {
        margin-top: 14px;
        max-width: none;
    }
}

@media only screen and (max-width: 480px) {
    /* Длинные юридические ссылки на 320px рвутся по слогам, а не вылезают за
       край: перенос разрешён внутри слова, но только когда иначе не помещается. */
    .consult__consents a {
        overflow-wrap: anywhere;
    }
}

/* === homepage/css/reviews.css === */
/* ==========================================================================
   Блок «Реальные отзывы клиентов»

   КАРУСЕЛЬ — МАКЕТНАЯ (Swiper из сборки сайта, `.reviews-list.swiper`: 4
   карточки от 1320px, 3 от 1100px, «auto» ниже). Здесь только то, чего в
   макете не было, потому что там лежали восемь одинаковых заглушек: равная
   высота карточек, раскрытие длинного отзыва и подпись площадки без логотипа.

   ВЫСОТА КАРТОЧЕК РАВНАЯ ПО ПОСТРОЕНИЮ. `swiper-slide` по умолчанию берёт
   высоту содержимого, и карточка с коротким отзывом оказывалась ниже соседей.
   `height: auto` у слайда + растяжение ряда делают высоту свойством ряда, а не
   совпадением длин текстов.

   РАСКРЫТИЕ — БЕЗ СКРИПТА. Текст в разметке целиком, видимую высоту задаёт CSS,
   раскрывает скрытый checkbox. Подмена текста из JS означала бы пересчёт высот
   в момент клика, а обрезка на сервере — потерю конца отзыва.
   ========================================================================== */

/* --- Ряд и карточка ------------------------------------------------------ */

.reviews .reviews-list__wrap {
    align-items: stretch;
}

.reviews .reviews-list__item {
    height: auto;
    display: flex;
    flex-direction: column;
}

/* Блок ссылок прижат к низу карточки: иначе у короткого отзыва «Читать
   полностью» висит посередине белого поля. */
.reviews .reviews-list__wrap-content {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

.reviews .reviews-list__links {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 14px 0 0;
    flex-wrap: wrap;
}

/* --- Цифры над карточками ------------------------------------------------ */

/*
   ПАНЕЛЬ ИЗ ДВУХ ЦИФР — ЭТО КАРТОЧКА ТОГО ЖЕ РОДА, ЧТО И ОТЗЫВЫ ПОД НЕЙ.
   Сначала цифры стояли голым текстом на фоне секции: два числа и две подписи
   висели между вводным абзацем и рядом карточек, не принадлежа ни тому ни
   другому, — читалось это как забытая строка отладки. Панель берёт те же
   белый фон, рамку #f5f5f5 и радиус 16px, что и карточка отзыва, и потому
   встаёт в блок как его часть.

   ШИРИНА — ПО СОДЕРЖИМОМУ. Растянутая во всю колонку панель с двумя числами
   слева и пустотой справа выглядит недовёрстанной; `fit-content` делает её
   ровно такой, сколько в ней есть.
*/
.reviews .reviews-stats {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    width: fit-content;
    max-width: 100%;
    margin: 0 0 28px;
    padding: 18px 4px;
    background: #fff;
    border: 1px solid #f5f5f5;
    border-radius: 16px;
}

.reviews .reviews-stats__item {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 2px 28px;
}

/*
   Разделитель — рамка НА САМОМ ЭЛЕМЕНТЕ, а не абсолютный псевдоэлемент: у
   псевдоэлемента без позиционированного предка растяжка уходит к внешнему
   блоку, и это уже приводило к белому экрану на iPhone (§8).
*/
.reviews .reviews-stats__item + .reviews-stats__item {
    border-left: 1px solid #f0f0f0;
}

.reviews .reviews-stats__value {
    font-weight: 700;
    font-size: 34px;
    line-height: 100%;
    color: #ff491a;
    /* «5,0» не разрывается по запятой: половина оценки на другой строке
       читается как другая оценка. */
    white-space: nowrap;
}

.reviews .reviews-stats__body {
    display: flex;
    flex-direction: column;
    gap: 6px;
    /*
       Иначе шкала звёзд растягивается на всю ширину колонки и раздаёт лишнее
       место между звёздами: замер дал 160px против 96px у той же шкалы в
       карточке отзыва — то есть звёзды визуально «разъезжались» ровно там, где
       их сравнивают с карточками ниже.
    */
    align-items: flex-start;
}

.reviews .reviews-stats__label {
    font-size: 15px;
    line-height: 130%;
    color: #667085;
}

@media only screen and (max-width: 620px) {
    /*
       На узком экране панель занимает всю ширину и раскладывается в столбик:
       две ячейки в ряд при 320px дают по 130px на цифру с подписью, и подпись
       рвётся на три строки.
    */
    .reviews .reviews-stats {
        width: 100%;
        padding: 8px 4px;
    }

    .reviews .reviews-stats__item {
        width: 100%;
        padding: 12px 18px;
    }

    .reviews .reviews-stats__item + .reviews-stats__item {
        border-left: 0;
        border-top: 1px solid #f0f0f0;
    }

    .reviews .reviews-stats__value {
        font-size: 28px;
    }
}

/* --- Логотип площадки ---------------------------------------------------- */

/* Высота как у логотипа-картинки (21px + отступ макета): незнакомая площадка не
   должна менять геометрию карточки. */
.reviews .reviews-list__logo--text {
    display: inline-flex;
    align-items: center;
    min-height: 21px;
    padding: 2px 10px;
    border: 1px solid #e6e6e6;
    border-radius: 999px;
    font-size: 12px;
    line-height: 120%;
    color: #667085;
    /* Переносится, а не выпирает: замер на 320px дал подпись 226px при 228px
       свободных внутри карточки — запаса нет, и любое более длинное название
       площадки вылезло бы за край. */
    max-width: 100%;
}

/* --- Текст отзыва: одна линия обрезки на весь ряд ------------------------ */

/*
   ТЕКСТ У ВСЕХ КАРТОЧЕК ЗАКАНЧИВАЕТСЯ НА ОДНОЙ ЛИНИИ. В макете у блока текста
   стоял `max-height`, то есть высоту задавала длина отзыва: короткий кончался
   на второй строке, длинный — на седьмой. Карточки при этом равные (ряд
   растянут), но ссылки «Читать полностью» и «Читать на площадке» вставали на
   разной высоте, а под короткой карточкой оставалось полполя пустоты — ряд
   выглядел рваным. Высота здесь фиксированная: она и есть та линия, ниже
   которой текста не бывает, и всё, что под ней, выравнивается само.

   ВЫСОТА СЧИТАЕТСЯ ОТ КЕГЛЯ, А НЕ В ПИКСЕЛЯХ. `1.4em` — это ровно одна строка
   (макетный `line-height: 140%`), поэтому на брейкпоинте с другим шрифтом блок
   остаётся теми же тремя строками, без второго набора констант. Менять число
   строк — одной цифрой в `--reviews-lines`.
*/
.reviews .reviews-list__content {
    --reviews-lines: 3;
    height: calc(var(--reviews-lines) * 1.4em);
    max-height: none;
    /*
       РАСТУШЁВКА — ТОЛЬКО У ОБРЕЗАННОГО ТЕКСТА. Макет красит текст градиентом
       (`background-clip: text`) всегда, и у короткого отзыва выцветала его
       последняя строка: законченная мысль читалась как оборванная на полуслове.
    */
    background: none;
    -webkit-text-fill-color: currentColor;
    color: #535353;
}

.reviews .reviews-list__item--long .reviews-list__content {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0) 45.69%, #fff 110.34%), #535353;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* --- Длинный отзыв ------------------------------------------------------- */

.reviews .reviews-list__toggle {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 0;
    height: 0;
}

/*
   Раскрытая карточка: снимаем и ограничение высоты, и градиентную «растушёвку»
   макета — она нарисована фоном по тексту (`background-clip: text`), и без её
   снятия конец раскрытого отзыва оставался бы выцветшим.
*/
.reviews .reviews-list__toggle:checked ~ .reviews-list__content {
    height: auto;
    max-height: none;
    background: none;
    -webkit-text-fill-color: currentColor;
    color: #535353;
}

.reviews .reviews-list__more-close,
.reviews .reviews-list__toggle:checked ~ .reviews-list__links .reviews-list__more-open {
    display: none;
}

.reviews .reviews-list__toggle:checked ~ .reviews-list__links .reviews-list__more-close {
    display: inline;
}

.reviews .reviews-list__more-link {
    cursor: pointer;
}

/* Клавиатура: у label нет своего фокуса, поэтому подсвечиваем его от чекбокса —
   иначе управлять раскрытием можно только мышью. */
.reviews .reviews-list__toggle:focus-visible ~ .reviews-list__links .reviews-list__more-link {
    outline: 2px solid #ff491a;
    outline-offset: 3px;
}

/* --- Ссылка на оригинал -------------------------------------------------- */

.reviews .reviews-list__source-link {
    font-size: 15px;
    line-height: 140%;
    color: #667085;
    border-bottom: 1px solid rgba(102, 112, 133, .4);
}

.reviews .reviews-list__source-link:hover {
    color: #ff491a;
    border-bottom-color: #ff491a;
}

/* --- Кнопки под каруселью ------------------------------------------------ */

@media only screen and (max-width: 767px) {
    /* Кнопки в столбик и во всю ширину: «Записаться на бесплатную консультацию»
       не помещается в строку с соседкой уже на 430px. */
    .reviews .reviews-actions {
        flex-direction: column;
        align-items: stretch;
        gap: 12px;
    }

    .reviews .reviews-actions__item {
        width: 100%;
    }

    .reviews .reviews-actions__btn {
        width: 100%;
    }
}

/* === homepage/css/why.css === */
/* ==========================================================================
   Блок «Почему выбирают „ПОД КЛЮЧ“»

   СЕТКА ВМЕСТО ЛЕНТЫ. Макет держал `.why-list` флексом с `flex: calc(33.3333% -
   17.4px) 0 0` и одновременно отдавал его Swiper'у (`.why-list.swiper`). Здесь
   карусели нет: 3 карточки в ряд на desktop, 2 на планшете, 1 на телефоне —
   обычный grid, без подгонки отдельных карточек отступами.

   ВЫРАВНИВАНИЕ ВНУТРИ РЯДА ДЕРЖИТ SUBGRID. Требование «более длинный заголовок
   не должен толкать текст ниже относительно соседей» нельзя выполнить, пока
   каждая карточка считает высоту своей шапки сама: заголовок в две строки
   опускает свой текст на строку ниже, чем у соседей. Карточка занимает две
   строки внешней сетки и наследует их (`grid-template-rows: subgrid`), поэтому
   шапки всех карточек ряда имеют одну высоту, а тексты начинаются с одной линии
   по построению — а не потому, что заголовки случайно одной длины.

   ЗАПАСНОЙ ВАРИАНТ ЧЕСТНЫЙ. Там, где subgrid не поддерживается, шапке задаётся
   минимальная высота в две строки: выравнивание держится, пока заголовок не
   длиннее двух строк. Это хуже subgrid и лучше разъезжающихся текстов.
   ========================================================================== */

.why .why-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px 26px;
    /* Флексовые правила макета не должны участвовать. */
    flex-wrap: initial;
    align-items: stretch;
}

/*
   Карточка занимает две строки ряда: первая — шапка (иконка + заголовок),
   вторая — текст. Высота карточек одинакова, потому что это ячейки одной сетки,
   а не элементы, подогнанные друг под друга.
*/
.why .why-list__item {
    grid-row: span 2;
    display: grid;
    grid-template-rows: subgrid;
    gap: 16px;
    /* Ширину задаёт колонка: макетное `flex: calc(33.3333% …)` здесь мешает. */
    flex: initial;
    max-width: none;
    height: auto;
    margin: 0;
}

.why .why-list-header {
    /* Иконка и заголовок — по верхнему краю: при заголовке в две строки
       центрирование опускало бы иконку относительно соседних карточек. */
    align-items: flex-start;
    margin: 0;
}

.why .why-list__content {
    margin: 0;
}

/* --- Иконка -------------------------------------------------------------- */

/*
   Размер и цвет глифа задаёт макет (`.why-list-header__icon`: 52px, ниже 1100px
   — 46px). Здесь только защита от сжатия: во flex-строке иконка не должна
   ужиматься под длинный заголовок — иначе в карточке с двумя строками названия
   она окажется уже, чем у соседей.
*/
.why .why-icon {
    flex: 0 0 auto;
    line-height: 1;
}

/* --- Запасной вариант без subgrid ---------------------------------------- */

@supports not (grid-template-rows: subgrid) {
    .why .why-list__item {
        grid-row: auto;
        display: flex;
        flex-direction: column;
    }

    /* Две строки заголовка + высота иконки: тексты остаются на одной линии,
       пока заголовок не длиннее двух строк. */
    .why .why-list-header {
        min-height: 60px;
        margin: 0 0 16px;
    }
}

/* --- Планшет ------------------------------------------------------------- */

@media only screen and (max-width: 1100px) {
    .why .why-list {
        grid-template-columns: repeat(2, 1fr);
        gap: 16px;
    }
}

/* --- Телефон ------------------------------------------------------------- */

@media only screen and (max-width: 767px) {
    .why .why-list {
        grid-template-columns: 1fr;
    }

    /* Одна колонка: subgrid внутри ряда из одной карточки ничего не выравнивает,
       и лишняя строка сетки только добавляла бы пустоту под коротким текстом. */
    .why .why-list__item {
        grid-row: auto;
        display: flex;
        flex-direction: column;
    }

    .why .why-list-header {
        min-height: 0;
        margin: 0 0 12px;
    }
}

/* === homepage/css/quality.css === */
/* ==========================================================================
   Блок «Контроль качества на объектах»

   СЕТКА ВМЕСТО ЛЕНТЫ. Макет держал `.control-list` флексом с
   `flex: calc(25% - 20.3px) 0 0` и одновременно отдавал `.wrap-control-list`
   Swiper'у. Здесь карусели нет: 4 карточки в ряд на desktop, 2×2 на планшете,
   1 на телефоне — grid, без подгонки карточек отступами.

   ВЫРАВНИВАНИЕ ДЕРЖИТ SUBGRID. Требования «фотографии одинаковой высоты»,
   «заголовки начинаются на одной линии», «контентные области одинаковой высоты»
   нельзя выполнить, пока каждая карточка считает высоту своих частей сама:
   должность в две строки опускает подзаголовок и список ниже, чем у соседей.
   Карточка занимает четыре строки внешней сетки (фото / шапка / подзаголовок /
   список) и наследует их, поэтому части выровнены по построению.

   ЗАПАСНОЙ ВАРИАНТ. Без subgrid карточка становится обычной колонкой, а шапке и
   подзаголовку задаётся минимальная высота: выравнивание держится, пока
   должность не длиннее двух строк.
   ========================================================================== */

.quality .quality__list {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 27px;
    /* Флексовые правила макета не должны участвовать. */
    flex-wrap: initial;
    align-items: stretch;
    /* Номер приходит из данных, счётчик вёрстки не нужен. */
    counter-reset: none;
}

.quality .quality__item {
    /* Четыре строки: фото, шапка, подзаголовок, список. */
    grid-row: span 4;
    display: grid;
    grid-template-rows: subgrid;
    gap: 0;
    flex: initial;
    max-width: none;
    height: auto;
    background: #fff;
    border-radius: 16px;
    overflow: hidden;
}

/* Блок без фотографий: строк три — шапка, подзаголовок, список. */
.quality:not(.quality--with-photos) .quality__item {
    grid-row: span 3;
}

/* Номер печатает шаблон: у вёрстки он был CSS-счётчиком и зависел от позиции,
   поэтому скрытие второй карточки давало уровни 1, 2, 3 вместо 1, 3, 4. */
.quality .control-list__wrap-img::after {
    content: none;
}

/* --- Фотография ---------------------------------------------------------- */

.quality .quality__media {
    position: relative;
    height: 214px;
}

.quality .quality__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Место под фотографию, которой ещё нет: нейтральная плашка в тон блока. */
.quality .quality__media-empty {
    width: 100%;
    height: 100%;
    background: linear-gradient(180deg, #fff5f5 0%, #ffecea 100%);
}

/* --- Номер уровня -------------------------------------------------------- */

.quality .quality__number {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    min-width: 35px;
    height: 33px;
    padding: 0 8px;
    background: #ff491a;
    border-radius: 6px;
    font-weight: 700;
    font-size: 21px;
    line-height: 100%;
    color: #fff;
}

.quality .quality__number--on-photo {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 1;
}

/* --- Содержимое ---------------------------------------------------------- */

.quality .quality__content {
    display: grid;
    grid-template-rows: subgrid;
    /* Три строки содержимого; четвёртая (фото) остаётся снаружи. */
    grid-row: span 3;
    gap: 12px;
    padding: 20px;
}

/*
   ПО БАЗОВОЙ ЛИНИИ, А НЕ ПО ЦЕНТРУ. Строка шапки у всех карточек одной высоты
   (subgrid), но при `align-items: center` короткая должность центрируется в ней
   и начинается ниже длинной: замер дал 42px против 20px у должности в три
   строки. Baseline сажает первую строку названия на одну линию во всех
   карточках и заодно выравнивает её с цифрой в плашке.
*/
.quality .quality__head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin: 0;
}

.quality .quality__title {
    margin: 0;
    font-size: 18px;
    line-height: 120%;
    color: #111;
}

.quality .quality__subtitle {
    margin: 0;
    font-size: 15px;
    line-height: 140%;
    color: #667085;
}

.quality .quality__points {
    margin: 0;
}

.quality .quality__points ul {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 10px;
}

.quality .quality__points li {
    position: relative;
    padding-left: 18px;
    font-size: 15px;
    line-height: 140%;
    color: #999;
}

/* Маркер рисуется на самом пункте: у него есть своя строка, и растягивать
   псевдоэлемент от списка не нужно (CLAUDE.md §8). */
.quality .quality__points li::before {
    content: '';
    position: absolute;
    left: 0;
    top: 8px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #ff491a;
}

/* --- Запасной вариант без subgrid ---------------------------------------- */

@supports not (grid-template-rows: subgrid) {
    .quality .quality__item,
    .quality:not(.quality--with-photos) .quality__item {
        grid-row: auto;
        display: flex;
        flex-direction: column;
    }

    .quality .quality__content {
        display: flex;
        flex-direction: column;
        grid-row: auto;
        flex: 1 1 auto;
    }

    /* Две строки должности и две строки подзаголовка: списки остаются на одной
       линии, пока тексты не длиннее этого. */
    .quality .quality__head {
        min-height: 44px;
    }

    .quality .quality__subtitle {
        min-height: 42px;
    }
}

/* --- Планшет ------------------------------------------------------------- */

@media only screen and (max-width: 1100px) {
    .quality .quality__list {
        grid-template-columns: repeat(2, 1fr);
        gap: 16px;
    }
}

/* --- Телефон ------------------------------------------------------------- */

@media only screen and (max-width: 767px) {
    .quality .quality__list {
        grid-template-columns: 1fr;
    }

    /*
       Одна колонка: subgrid внутри ряда из одной карточки ничего не выравнивает
       и только добавляет пустоту под коротким текстом. Размер шрифта при этом не
       уменьшается — читаемость важнее сохранения колонок.
    */
    .quality .quality__item,
    .quality:not(.quality--with-photos) .quality__item {
        grid-row: auto;
        display: flex;
        flex-direction: column;
    }

    .quality .quality__content {
        display: flex;
        flex-direction: column;
        grid-row: auto;
    }

    .quality .quality__head,
    .quality .quality__subtitle {
        min-height: 0;
    }
}

/* === homepage/css/estimate.css === */
/* ==========================================================================
   Блок «Получите смету в день обращения»

   ФОН — СЛОЙ, А НЕ СВОЙСТВО БЛОКА. В макете `.wrap-estimate` несёт
   `background: url(../img/bgs/desktop/estimate-bg.jpg)`, то есть кадр зашит в
   стиль, один на 354 города. Здесь картинка приходит из слайда отдельным
   `<img>`: работают srcset, webp-двойник и ленивая загрузка, на телефон уезжает
   копия на 400/800 px. Макетный фон гасится — иначе браузер скачал бы обе
   картинки, а нижняя всё равно не видна.

   ЗАТЕМНЕНИЕ ОТДЕЛЬНЫМ СЛОЕМ, как в блоке консультации: редактор поставит
   светлый кадр, и белый текст на нём исчезнет; подбирать «файл потемнее» —
   значит чинить вёрстку картинкой.
   ========================================================================== */

.estimate-block {
    position: relative;
    overflow: hidden;
    background-color: #1d2939;
    background-image: none;
    isolation: isolate;
}

.estimate-block__bg {
    position: absolute;
    inset: 0;
    z-index: -2;
}

.estimate-block__bg picture,
.estimate-block__bg img {
    display: block;
    width: 100%;
    height: 100%;
}

.estimate-block__bg img {
    object-fit: cover;
    object-position: center;
}

/* Затемнение живёт на самом блоке — позиционированный предок у псевдоэлемента
   должен быть очевидным (CLAUDE.md §8). */
.estimate-block--photo::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(16, 24, 40, var(--estimate-overlay, .55));
    pointer-events: none;
}

.estimate-block[data-overlay='none']::before { --estimate-overlay: 0; }
.estimate-block[data-overlay='light']::before { --estimate-overlay: .35; }
.estimate-block[data-overlay='medium']::before { --estimate-overlay: .55; }
.estimate-block[data-overlay='strong']::before { --estimate-overlay: .72; }

.estimate-block__inner {
    position: relative;
}

/* --- Строка формы -------------------------------------------------------- */

/*
   Имя | Телефон | кнопка — одной линией, как в блоке консультации.
   `align-items: stretch` делает равную высоту свойством строки, а не
   совпадением двух констант.
*/
.estimate-block__row {
    display: flex;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 12px;
    margin: 0;
}

/*
   БАЗИС МАЛЕНЬКИЙ НАМЕРЕННО. Левая колонка блока — не вся ширина страницы, а
   643px (справа стоит панель процесса), и при базисе 200px три элемента в неё не
   помещались: строка переносилась, кнопка уезжала под поля и схлопывалась до
   27px — `height: 100%` в отдельной строке не от чего разрешать. Базис 120px
   оставляет раскладку одной строкой, а фактическую ширину полей считает flex.
*/
.estimate-block__row > .form__item {
    flex: 1 1 120px;
    /* Иначе flex-элемент не сожмётся уже содержимого и строка вылезет вбок. */
    min-width: 0;
    display: block;
}

.estimate-block__row .form__wrap-input,
.estimate-block__row .form__input {
    width: 100%;
}

.estimate-block .estimate-block__row .form__item.estimate-block__submit {
    flex: 0 0 auto;
    max-width: none;
}

.estimate-block .estimate-block__row .form__item.estimate-block__submit .form__btn {
    width: 100%;
    min-width: 200px;
    margin: 0;
}

@media only screen and (min-width: 1101px) {
    /* Растяжение — только там, где есть строка: у колонки высота задана
       содержимым, процент разрешать не от чего, и кнопка падает до высоты своей
       надписи (CLAUDE.md §8). */
    .estimate-block__row .form__wrap-input,
    .estimate-block__row .form__input,
    .estimate-block .estimate-block__row .form__item.estimate-block__submit .form__btn {
        height: 100%;
    }
}

/* --- Согласия и примечание ----------------------------------------------- */

.estimate-block__consents {
    margin: 16px 0 0;
}

.estimate-block__consents .lead-consent__text,
.estimate-block__consents .form-privacy__txt {
    color: rgba(255, 255, 255, .82);
}

.estimate-block__consents a {
    color: #fff;
}

.estimate-block .estimate-note {
    margin: 18px 0 0;
}

/* --- Мобильная ----------------------------------------------------------- */

@media only screen and (max-width: 1100px) {
    /* Всё в столбик и во всю ширину: поля, кнопка, согласия. */
    .estimate-block__row {
        flex-direction: column;
        gap: 10px;
    }

    .estimate-block__row > .form__item {
        flex: 1 1 auto;
        width: 100%;
    }

    .estimate-block .estimate-block__row .form__item.estimate-block__submit .form__btn {
        min-width: 0;
        width: 100%;
    }

    .estimate-block .estimate-note__btn {
        width: 100%;
    }
}

@media only screen and (max-width: 480px) {
    /* Длинные юридические ссылки рвутся по слогам, а не вылезают за край. */
    .estimate-block__consents a {
        overflow-wrap: anywhere;
    }
}

/* === homepage/css/agreement.css === */
/* ==========================================================================
   Блок «Договор и гарантии»

   ПУНКТЫ — СЕТКОЙ, А НЕ ФЛЕКСОМ. Макет раскладывает их `flex: 1` с переносом, и
   при трёх пунктах на планшете получалось два ряда неравной ширины: замер на
   768px дал 160px и 333px рядом. Ширину должна задавать колонка, а не длина
   текста, поэтому здесь grid: три колонки на desktop, одна ниже 768.

   ВЫРАВНИВАНИЕ ЗАГОЛОВКОВ. Пункты «Сроки», «Стоимость» и «Гарантия» — короткие,
   но описание у них разной длины: `align-items: stretch` у ячеек сетки делает
   высоту свойством ряда, а не совпадением длин.
   ========================================================================== */

/*
   `minmax(0, 1fr)`, А НЕ `1fr`. У `1fr` минимум равен min-content, поэтому
   колонка с длинным словом забирает больше своей доли: замер дал 129 / 144 /
   142 вместо равных. Нулевой минимум делает колонки действительно равными, а
   текст переносится внутри.
*/
.agreement-block .agreement-block__list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 14px;
    width: 100%;
    /* Флексовые правила макета не должны участвовать. */
    flex-wrap: initial;
    align-items: stretch;
}

.agreement-block .agreement-block__list .agreement-list__item {
    /* Ширину задаёт колонка сетки: макетное `flex: 1` мешает. */
    flex: initial;
}

/*
   ПОРОГ ПО ШИРИНЕ КОЛОНКИ, А НЕ ПО ШИРИНЕ ОКНА. Снимок договора стоит рядом и
   забирает 358px, поэтому на 768px левой колонке остаётся 333px: три колонки
   превращаются в полосы по 102px при высоте 227px. Ниже 1000px раскладываем в
   одну колонку — на 1024 левой части хватает (621px, колонки по ~197px).
*/
@media only screen and (max-width: 1000px) {
    .agreement-block .agreement-block__list {
        grid-template-columns: 1fr;
        gap: 16px;
    }
}

/* === homepage/css/pay.css === */
/* ==========================================================================
   Блок «Способы оплаты»

   СЕТКА ВМЕСТО ФЛЕКСА. Макет раскладывает карточки `flex: 1` с переносом, то
   есть ширину диктует длина названия: «Кредит» получал полосу заметно уже, чем
   «Оплата банковской картой». Здесь ширину задаёт колонка.

   `minmax(0, 1fr)`, А НЕ `1fr` — у `1fr` минимум равен min-content, и колонка с
   длинным словом забирает больше своей доли (проверено на блоке договора).
   ========================================================================== */

/*
   ШИРИНА ТЕКСТА = ШИРИНЕ РЯДА КАРТОЧЕК. Ограничение меры (920px при ряде в
   1106px) выглядело поломкой: правый край абзаца обрывался заметно раньше
   карточек под ним, и блок читался как незаконченный. Длину строки держит сам
   текст — три предложения, а не шесть строк.
*/

.pay-block .pay-block__list {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 26px;
    width: 100%;
    /* Флексовые правила макета не должны участвовать. */
    flex-wrap: initial;
    align-items: stretch;
}

.pay-block .pay-block__list .pay-list__item {
    /* Ширину задаёт колонка сетки: макетное `flex: 1` мешает. */
    flex: initial;
}

/* Уточнение под названием: факт, а не реклама, поэтому и выглядит спокойно. */
.pay-block .pay-block__note {
    margin: 4px 0 0;
    font-size: 14px;
    line-height: 130%;
    color: #667085;
}

/* --- Планшет ------------------------------------------------------------- */

@media only screen and (max-width: 1100px) {
    .pay-block .pay-block__list {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
    }
}

/* --- Телефон ------------------------------------------------------------- */

@media only screen and (max-width: 620px) {
    /*
       Одна колонка. Порог 620, а не 767: на двух колонках карточка ужимается до
       ~140px, и название «Оплата банковской картой» ложится в четыре строки
       рядом с иконкой 46px — читается как ошибка вёрстки.
    */
    .pay-block .pay-block__list {
        grid-template-columns: 1fr;
    }
}

/* === homepage/css/faq.css === */
/*
 * FAQ: вопрос — заголовок и кнопка.
 *
 * ЗАЧЕМ ПРАВКА. Замер главной: двадцать вопросов, четырнадцать скрыты целиком,
 * все двадцать ответов `display: none`, а заголовок — `<div>` с `cursor:
 * pointer`. Фокусируемых элементов среди заголовков — ноль: с клавиатуры не
 * открывался ни один ответ. Разметка теперь `h3 > button`, и здесь снимается
 * то, что браузер рисует у кнопки и у заголовка по умолчанию.
 *
 * ВНЕШНИЙ ВИД НЕ МЕНЯЕТСЯ. Все размеры, цвет и стрелка остаются на `.faq__title`
 * из вёрстки; здесь только сброс собственных стилей `button` и `h3`, ширина на
 * всю строку (чтобы область нажатия совпадала с прежней) и видимый фокус.
 */

.faq__heading {
    /*
     * Заголовок не должен принести свой кегль и отступы: визуально это та же
     * строка, что была, — меняется её роль в структуре документа, а не вид.
     */
    margin: 0;
    padding: 0;
    font: inherit;
    color: inherit;
}

.faq__item .faq__title {
    /*
     * Кнопка приходит со своим шрифтом, фоном, рамкой и выравниванием по
     * центру. Всё это снимается здесь, а не в вёрстке: `.faq__title` из мокапа
     * задаёт вид, и переопределять его значило бы держать два описания одного
     * элемента.
     */
    display: block;
    width: 100%;
    margin: 0;
    background: none;
    border: 0;
    font: inherit;
    font-weight: 700;
    font-size: 16px;
    line-height: 120%;
    color: #111;
    text-align: left;
    -webkit-appearance: none;
    appearance: none;
}

/*
 * ВИДИМЫЙ ФОКУС — обязательная часть правки, а не украшение. Смысл всей замены
 * в том, что до вопроса теперь можно дойти клавишей Tab; без заметной рамки
 * человек не увидит, где он находится, и доступность останется формальной.
 *
 * `:focus-visible`, а не `:focus`: браузер сам решает, показывать ли рамку, и
 * при нажатии мышью её не будет — как у остальных кнопок страницы.
 */
.faq__item .faq__title:focus-visible {
    outline: 2px solid #ff5c00;
    outline-offset: 4px;
    border-radius: 4px;
}

/* === homepage/css/article.css === */
/* ==========================================================================
   Статья: «Важно знать» на главной и внутренние текстовые страницы

   ОДИН НАБОР ПРАВИЛ НА ВСЕ СТАТЬИ. Разметку печатает общий компонент
   <x-article>, он же используется страницей «О компании». Второй файл со
   своими отступами и своей шириной колонки разошёлся бы с первым при первой
   правке, и две статьи на одном сайте выглядели бы по-разному.

   ВЁРСТКА БЛОКА — МАКЕТНАЯ. `.text h2/h3/p/ul/ol/figure/figcaption/blockquote`
   уже описаны в style.min.css, и переписывать их здесь нечего. В этом файле
   только то, чего в макете не было, потому что там лежало четыре абзаца рыбы
   одинаковой длины.
   ========================================================================== */

/* --- Ширина строки ------------------------------------------------------- */

/*
   ТЕКСТ ЗАНИМАЕТ БЛОК ЦЕЛИКОМ. Здесь стояло ограничение в 78 символов ради
   комфортной длины строки — и оно же сделало блок наполовину пустым: колонка
   текста кончалась на 658px при 1106px ширины карточки, и справа оставалось
   белое поле в 40% блока. Читается это не как «удобная колонка», а как
   недовёрстанный блок: все соседние блоки страницы занимают её ширину
   полностью, и один узкий столбец посреди них выглядит ошибкой.

   ЭТО УЖЕ БЫЛО В «СПОСОБАХ ОПЛАТЫ» — там абзац шириной 920px стоял над рядом
   карточек шириной 1106px, и разъезжающийся правый край владелец заметил
   первым же взглядом. Лечится тем же: ширину задаёт блок, а не типографская
   норма. Комфорт чтения на широком экране добираем короткими абзацами и
   подзаголовками, а не пустотой справа.
*/

/* --- Фотография ---------------------------------------------------------- */

/*
   КАРТИНКА НЕ РАСТЯГИВАЕТСЯ. `width`/`height` из файла нужны против сдвигов
   вёрстки (§7), но без этой пары они же заставили бы браузер нарисовать кадр
   в его собственных пикселях — шире колонки на мобильном и с искажением
   пропорций там, где ширина ограничена.
*/
.text--article figure img {
    display: block;
    width: 100%;
    height: auto;
}

/* Скругление у макета висит на `picture`, а `picture` — строчный элемент:
   между ним и подписью оставался зазор от базовой линии строки. */
.text--article figure picture {
    display: block;
}

/* --- Список -------------------------------------------------------------- */

/*
   ПУНКТ НУМЕРОВАННОГО СПИСКА — ЭТО ТЕКСТ, А НЕ РЯД КОЛОНОК.

   В макете `ol li` объявлен `display: flex` с `align-items: center`: там пункт
   всегда был одной строкой простого текста, и флекс ничего не ломал. Но у
   пункта с подписью внутри два узла — `<strong>` и текст за ним, — и флекс
   делает из них ДВА СТОЛБЦА: подпись «Перепланировку помещений.» встала
   колонкой в 180px и переносилась по словам, а объяснение начиналось справа
   от неё, отдельным столбцом, с оторванным двоеточием в начале строки.

   Номер при этом никуда не денется: он рисуется `::before` в абсолютной
   позиции, и `display: block` его не трогает.

   Замер: `li` 1106px, внутри `strong` 180×39 отдельным блоком — именно это и
   видно на странице как «подпись слева, текст справа».
*/
.text--article ol li {
    display: block;
}

/* Номер макет центрирует по высоте пункта (`top: 50%`) — у пункта в две
   строки это середина абзаца. Возвращаем его к первой строке. */
.text--article ol li::before {
    top: 0;
    transform: none;
}

/* Межстрочное расстояние списка — как у абзаца: 129% макета рассчитаны на
   строку-другую, а у пункта из трёх строк слова слипаются. */
.text--article ol li,
.text--article ul li {
    line-height: 140%;
}

/* --- Плашка «Важно знать» ------------------------------------------------ */

/*
   Правый отступ 80px в макете оставлен под декор, которого в блоке нет: на
   узком экране он съедал четверть строки, а на широком делал плашку заметно
   уже соседнего текста.
*/
.text--article blockquote {
    padding-right: 22px;
}

.text--article blockquote h4 span {
    color: #ff491a;
}

@media only screen and (max-width: 767px) {
    /* Отступ фигуры на мобильном — как у абзаца: 30px сверху и снизу разрывают
       статью пополам на экране высотой в шесть строк. */
    .text--article figure {
        margin: 20px 0;
    }
}

/* ------------------------------------- статья как отдельная страница -- */

/*
 * ТЕКСТ ЗАНИМАЕТ ОБЛАСТЬ СТРАНИЦЫ ЦЕЛИКОМ — И НА ВНУТРЕННЕЙ ТОЖЕ.
 *
 * ЗДЕСЬ СТОЯЛО `max-width: 900px` ради длины строки, и владелец увидел ровно
 * то, о чём предупреждает комментарий в начале файла: замер на 1440 дал
 * область 1146px при тексте 900px — 226px пустоты справа, текст прижат к
 * левому краю. На главной это правило уже снимали по той же причине, и держать
 * его на внутренней странице значило иметь две разные статьи на одном сайте.
 *
 * ДЛИНА СТРОКИ ЛЕЧИТСЯ НЕ ПУСТОТОЙ. Длинный перечень уходит в две колонки
 * (ниже), абзацы держатся короткими, у разделов есть заголовки. Это работа
 * содержимого, а не поля справа.
 */

/* --- Читаемость ---------------------------------------------------------- */

/*
 * МЕЖСТРОЧНОЕ РАССТОЯНИЕ — ПОД ДЛИНУ СТРОКИ. Макетные 130% рассчитаны на узкую
 * колонку в карточке; на полной ширине области страницы строка длинная, и на
 * возврате к началу следующей глаз промахивается мимо строки. Это та же правка,
 * что уже сделана спискам ниже в этом файле.
 */
.text--article p {
    line-height: 160%;
}

/*
 * ВЫДЕЛЕНИЕ ВНУТРИ ТЕКСТА. В legacy жирным открыта мысль абзаца — «<strong>
 * Косметический ремонт %CITY</strong> – это декоративная отделка…». Своего
 * правила у `strong` в макете нет вовсе, и браузерного `bold` (700) при Roboto
 * рядом с обычным начертанием мало: выделение видно, только если знать, что оно
 * там есть. Цвет основного текста вместо серого добавляет ему тот же вес, что
 * даёт заголовку его насыщенность.
 */
.text--article strong {
    font-weight: 700;
    color: #303030;
}

/*
 * ПУНКТ СПИСКА — ТОТ ЖЕ ТЕКСТ, ЧТО АБЗАЦ. В макете он светло-серый (#999) на
 * белом: контраст 2.8:1 — ниже порога WCAG AA (4.5:1) для основного текста, и
 * на телефоне при свете такой перечень читается с трудом. У «Косметического
 * ремонта» в этом сером лежит всё содержание страницы: 29 пунктов против двух
 * абзацев. Цвет берётся у абзаца, а не назначается свой.
 */
.text--article ul li,
.text--article ol li {
    color: #535353;
}

/* --- Перечень в две колонки ---------------------------------------------- */

/*
 * КОЛОНКИ ВКЛЮЧАЕТ РАЗМЕТКА, А НЕ ЭТОТ ФАЙЛ. Класс `article-list--split`
 * ставит компонент и только там, где список это действительно перечень:
 * пунктов от восьми, ни у одного нет подписи, каждый в строку-другую. У
 * «Косметического ремонта» их 29 по одной строке — столбиком это два экрана
 * вертикали, и справа от каждого пункта 900px пустоты.
 *
 * ОДНА КОЛОНКА НА УЗКОМ. Порог 900px взят по замеру: ниже него на колонку
 * остаётся меньше 430px, и пункт вроде «Декоративная отделка: укладка кафельной
 * плитки или использование декоративных штукатурок» ломается на три строки —
 * две колонки перестают экономить высоту и начинают мешать чтению.
 */
@media only screen and (min-width: 900px) {
    .text--article .article-list--split {
        column-count: 2;
        column-gap: 48px;
    }

    /*
     * ПУНКТ НЕ ДЕЛИТСЯ МЕЖДУ КОЛОНКАМИ. Без этого пункт из двух строк
     * оставляет первую строку внизу левой колонки, а вторую — вверху правой:
     * читается как два разных пункта, один из которых начинается с середины
     * фразы.
     */
    .text--article .article-list--split li {
        break-inside: avoid;
        page-break-inside: avoid;
    }
}

/*
 * РАЗДЕЛИТЕЛЬ МЕЖДУ ПУНКТАМИ. В перечне из тридцати строк глазу не за что
 * зацепиться: одинаковые короткие строки сливаются в столбик, и найти взглядом
 * нужную работу трудно. Линия идёт под пунктом, а не рамкой вокруг: это всё
 * ещё список, а не таблица.
 */
.text--article .article-list--split li {
    padding-bottom: 8px;
    border-bottom: 1px solid #ededed;
}

/* Последний пункт колонки линию не рисует — под ним уже граница блока. */
.text--article .article-list--split li:last-child {
    border-bottom: 0;
}

/*
 * Отступ между первым экраном и текстом страницы — тот же, что между блоками
 * главной. Своей константы здесь нет: `.wrap-text` уже несёт внешние отступы
 * блока, а крошки — свой сверху.
 */
.inner-page__content {
    min-width: 0;
}

/*
 * КРОШКИ ВЫРАВНЕНЫ ПО ЛЕВОМУ КРАЮ ОБЛАСТИ СТРАНИЦЫ. Внутренний отступ у
 * `.wrap-text` свой на каждой ширине (замер: 20px на 1440, 0 на 390), и
 * повторять его у крошек значило бы держать вторую копию чужого числа — она
 * разъехалась бы при первой правке макета.
 */

/* --- Статья как отдельная страница: белый фон ---------------------------- */

/*
 * По просьбе владельца статья внутренней страницы стоит на белом.
 *
 * В вёрстке `.wrap-text` залит `#fbfbfb` с градиентом — на главной это один
 * блок среди других серых, и он читается как карточка. На внутренней странице
 * это ВСЯ страница, и серая плашка во весь экран выглядит не карточкой, а
 * выключенным блоком.
 *
 * Селектор с `.page--inner`: правило вёрстки записано одним классом, и
 * одиночный `.wrap-text` проиграл бы ему по порядку, а не по смыслу. Блок
 * «Важно знать» на главной остаётся серым — его этот селектор не задевает.
 */
.page--inner .wrap-text {
    background: #fff;
}

/* --- Заголовок страницы -------------------------------------------------- */

/*
 * H1 отделён от текста так же, как всё остальное в статье.
 *
 * ЧТО БЫЛО. В вёрстке `.text` никогда не содержал H1: на главной статья
 * начинается с H2, и правило для первого уровня туда просто не написали. Замер:
 * у абзаца, списка и H2 внутри статьи `margin-bottom: 20px`, у H1 — `0`, и
 * первая строка текста прилипала к заголовку вплотную (зазор 0px).
 *
 * ЧИСЛО НЕ ПРИДУМАНО: это тот же шаг 20px, которым разделено всё остальное в
 * блоке. Своя константа здесь завела бы второй ритм в одном тексте.
 */
.text--article h1 {
    margin-bottom: 20px;
}

/*
 * ТАБЛИЦА ВНУТРИ СТАТЬИ.
 *
 * ОТКУДА ОНА. У 33 разделов внутри «Важно знать» из legacy лежит таблица
 * «Вид работ / Описание / Цена (руб) / Единицы измерения». Вёрстки для неё в
 * макете нет: там таблицы либо матрицы главной, либо прайс-лист, и оба
 * набора правил написаны под своё содержимое (§8) — брать их сюда значит
 * получить чужую геометрию.
 *
 * ПРОКРУТКА ВНУТРИ СВОЕЙ ОБЁРТКИ, А НЕ У ДОКУМЕНТА. Колонок четыре, и на
 * 390 px они не помещаются никогда; без этого вбок уезжает вся страница.
 *
 * ШИРИНА КОЛОНОК НЕ ЗАДАНА ЧИСЛАМИ: таблицы приехали из legacy, число и смысл
 * колонок у них разные, и любое `nth-child` здесь было бы правилом под одну
 * таблицу из тридцати трёх (§8).
 */
.text--article .article-table {
    margin-bottom: 24px;
    overflow-x: auto;
    /* Полоса прокрутки не должна закрывать последнюю строку. */
    padding-bottom: 4px;
}

.text--article .article-table table {
    width: 100%;
    /* Ячейкам нужна собственная ширина по содержимому, иначе на узком экране
       «Единицы измерения» сжимается в столбик по одной букве. */
    min-width: 560px;
    border-collapse: collapse;
    font-size: 15px;
    line-height: 140%;
}

.text--article .article-table th,
.text--article .article-table td {
    padding: 10px 14px;
    border: 1px solid #e6e6e6;
    text-align: left;
    vertical-align: top;
    /* Длинное слово из legacy («выключателей,светильников» без пробела) не
       имеет права вылезти за колонку — §7.9.1, тот же случай, что в прайсе. */
    overflow-wrap: break-word;
}

.text--article .article-table th {
    background: #f6f6f6;
    font-weight: 700;
    color: #303030;
}

.text--article .article-table td {
    color: #535353;
}

/* Полосатость облегчает чтение длинных перечней работ. */
.text--article .article-table tbody tr:nth-child(even) td {
    background: #fbfbfb;
}

/* === homepage/css/geography.css === */
/* ==========================================================================
   Блок «Наши представители есть в городах»

   Вёрстка новая: у макета здесь лежали семнадцать городов без ссылок в трёх
   колонках фиксированной ширины, и переключение вкладок делал скрипт. Здесь —
   сетка на `auto-fill`, ссылки и переключение чистым CSS.

   ПЕРЕКЛЮЧЕНИЕ БЕЗ СКРИПТА — ТОТ ЖЕ ПРИЁМ, ЧТО В ПРАЙС-ЛИСТЕ: скрытые радио
   перед лентой вкладок, `:checked ~` показывает свою панель и красит свою
   вкладку. Все города при этом остаются в документе, то есть видны поиску.
   ========================================================================== */

/* --- Переключатели -------------------------------------------------------- */

.geo .geo__switch {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 0;
    height: 0;
}

/*
   Панель и вкладка по номеру отмеченного радио. Девять пар — по числу округов
   в словаре (App\Support\FederalDistrict); лишние правила ничего не стоят и
   ждут округ, который появится.
*/
.geo .geo__switch:nth-of-type(1):checked ~ .geo__panes .geo__pane:nth-child(1),
.geo .geo__switch:nth-of-type(2):checked ~ .geo__panes .geo__pane:nth-child(2),
.geo .geo__switch:nth-of-type(3):checked ~ .geo__panes .geo__pane:nth-child(3),
.geo .geo__switch:nth-of-type(4):checked ~ .geo__panes .geo__pane:nth-child(4),
.geo .geo__switch:nth-of-type(5):checked ~ .geo__panes .geo__pane:nth-child(5),
.geo .geo__switch:nth-of-type(6):checked ~ .geo__panes .geo__pane:nth-child(6),
.geo .geo__switch:nth-of-type(7):checked ~ .geo__panes .geo__pane:nth-child(7),
.geo .geo__switch:nth-of-type(8):checked ~ .geo__panes .geo__pane:nth-child(8),
.geo .geo__switch:nth-of-type(9):checked ~ .geo__panes .geo__pane:nth-child(9) {
    display: block;
}

.geo .geo__switch:nth-of-type(1):checked ~ .geo__nav .geo__tab:nth-of-type(1),
.geo .geo__switch:nth-of-type(2):checked ~ .geo__nav .geo__tab:nth-of-type(2),
.geo .geo__switch:nth-of-type(3):checked ~ .geo__nav .geo__tab:nth-of-type(3),
.geo .geo__switch:nth-of-type(4):checked ~ .geo__nav .geo__tab:nth-of-type(4),
.geo .geo__switch:nth-of-type(5):checked ~ .geo__nav .geo__tab:nth-of-type(5),
.geo .geo__switch:nth-of-type(6):checked ~ .geo__nav .geo__tab:nth-of-type(6),
.geo .geo__switch:nth-of-type(7):checked ~ .geo__nav .geo__tab:nth-of-type(7),
.geo .geo__switch:nth-of-type(8):checked ~ .geo__nav .geo__tab:nth-of-type(8),
.geo .geo__switch:nth-of-type(9):checked ~ .geo__nav .geo__tab:nth-of-type(9) {
    background: #ff491a;
    border-color: #ff491a;
    color: #fff;
}

/* Клавиатура: у label своего фокуса нет, подсвечиваем его от радио — иначе
   переключать вкладки можно только мышью. */
.geo .geo__switch:focus-visible ~ .geo__nav .geo__tab {
    outline: none;
}

.geo .geo__switch:nth-of-type(1):focus-visible ~ .geo__nav .geo__tab:nth-of-type(1),
.geo .geo__switch:nth-of-type(2):focus-visible ~ .geo__nav .geo__tab:nth-of-type(2),
.geo .geo__switch:nth-of-type(3):focus-visible ~ .geo__nav .geo__tab:nth-of-type(3),
.geo .geo__switch:nth-of-type(4):focus-visible ~ .geo__nav .geo__tab:nth-of-type(4),
.geo .geo__switch:nth-of-type(5):focus-visible ~ .geo__nav .geo__tab:nth-of-type(5),
.geo .geo__switch:nth-of-type(6):focus-visible ~ .geo__nav .geo__tab:nth-of-type(6),
.geo .geo__switch:nth-of-type(7):focus-visible ~ .geo__nav .geo__tab:nth-of-type(7),
.geo .geo__switch:nth-of-type(8):focus-visible ~ .geo__nav .geo__tab:nth-of-type(8),
.geo .geo__switch:nth-of-type(9):focus-visible ~ .geo__nav .geo__tab:nth-of-type(9) {
    outline: 2px solid #ff491a;
    outline-offset: 3px;
}

/* --- Города своего субъекта ---------------------------------------------- */

/*
   Перечень над вкладками: та же сетка ссылок, но без переключателей — субъект
   у страницы один. Отделён рамкой снизу, чтобы не слипаться с лентой округов.
*/
.geo .geo-region {
    margin: 0 0 30px;
    padding: 0 0 26px;
    border-bottom: 1px solid #ececec;
}

/*
   Заголовок берёт разметку соседнего блока (`.title h2`), поэтому кегль и
   начертание у них общие по построению — переписывать их здесь нечего.
   Отдельная мелкая надпись, которая тут стояла, читалась как подпись к чужому
   заголовку, хотя это самостоятельный перечень.
*/
.geo .geo-region .title {
    margin: 0 0 18px;
}

.geo .geo-region__list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 10px 24px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.geo .geo-region__item {
    margin: 0;
    padding: 0;
    list-style: none;
}

@media only screen and (max-width: 767px) {
    .geo .geo-region__list {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 8px 16px;
    }

    .geo .geo-region {
        margin: 0 0 22px;
        padding: 0 0 20px;
    }
}

@media only screen and (max-width: 359px) {
    .geo .geo-region__list {
        grid-template-columns: 1fr;
    }
}

/* --- Заголовок и разделитель --------------------------------------------- */

.geo .geo__inner {
    padding: 20px 0 0;
    border-top: 1px solid #ececec;
}

/* --- Лента вкладок -------------------------------------------------------- */

.geo .geo__nav-track {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 22px;
}

.geo .geo__tab {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    padding: 9px 16px;
    background: #fff;
    border: 1px solid #e6e6e6;
    border-radius: 999px;
    font-size: 14px;
    line-height: 1.2;
    color: #333;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: border-color .2s ease, background .2s ease, color .2s ease;
}

.geo .geo__tab:hover {
    border-color: #ff491a;
}

/* Число городов — подсказка о размере вкладки, а не самостоятельная цифра. */
.geo .geo__tab-count {
    font-size: 12px;
    color: #999;
}

.geo .geo__switch:checked ~ .geo__nav .geo__tab .geo__tab-count {
    color: inherit;
    opacity: .75;
}

/* --- Список городов ------------------------------------------------------- */

.geo .geo__pane {
    display: none;
}

/*
   КОЛОНКИ СЧИТАЕТ САМА СЕТКА. `auto-fill` с `minmax` даёт 5–6 колонок на
   широком экране и столько же, сколько поместится, на любом другом: фиксированное
   число колонок оставляло бы пустое поле справа ровно на тех ширинах, под
   которые его не подбирали.
*/
.geo .geo__list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 10px 24px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.geo .geo__item {
    margin: 0;
    padding: 0;
    /* Правило макета `.wrap-geo li::before` рисует маркер — здесь список
       ссылок, а не перечисление. */
    list-style: none;
}

.geo .geo__link {
    display: inline-block;
    padding: 2px 0;
    font-size: 15px;
    line-height: 140%;
    color: #333;
    border-bottom: 1px solid transparent;
    transition: color .15s ease, border-color .15s ease;
}

.geo .geo__link:hover,
.geo .geo__link:focus-visible {
    color: #ff491a;
    border-bottom-color: #ff491a;
    text-decoration: none;
}

/* Текущий город: он остаётся ссылкой, но видно, что вы уже здесь. */
.geo .geo__link.is-current {
    font-weight: 700;
    color: #ff491a;
}

@media only screen and (max-width: 767px) {
    /*
       Лента вкладок прокручивается вбок внутри блока — тем же способом, что в
       прайс-листе. Девять вкладок в одну строку на 320px пришлось бы ужимать
       до нечитаемого кегля, а перенос по строкам съедает пол-экрана до того,
       как покажется первый город.
    */
    .geo .geo__nav-track {
        flex-wrap: nowrap;
        overflow-x: auto;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
        /* Полоса прокрутки не должна прижимать вкладки к тексту. */
        padding-bottom: 6px;
        /* Края ленты уходят под отступ блока, чтобы прокрутка читалась как
           продолжение, а не как обрезанная вкладка. */
        margin-right: -16px;
        padding-right: 16px;
    }

    .geo .geo__tab {
        scroll-snap-align: start;
        flex: none;
    }

    .geo .geo__list {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 8px 16px;
    }
}

@media only screen and (max-width: 359px) {
    /* На самых узких экранах две колонки по 140px не помещаются. */
    .geo .geo__list {
        grid-template-columns: 1fr;
    }
}

/* === homepage/css/footer.css === */
/* ==========================================================================
   Подвал и окно выбора города

   Вёрстка подвала макетная — здесь только то, чего в ней не было, потому что
   там стояли заглушки: кнопка вместо ссылки на договор, реальный список из 354
   городов в окне выбора и его поиск.
   ========================================================================== */

/* --- Кнопка, притворяющаяся ссылкой --------------------------------------- */

/*
   «Запросить договор» — действие, а не переход: оно открывает окно заявки.
   Кнопка честнее ссылки (её не откроешь в новой вкладке и не скопируешь
   адрес), но выглядеть должна как соседние строки контактов.
*/
.footer .data__link--button {
    padding: 0;
    background: none;
    border: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.footer .data__link--button:hover span,
.footer .data__link--button:focus-visible span {
    color: #ff491a;
}

/* Адрес — семантический `address`, а не div: у него курсив по умолчанию. */
.footer .data__address {
    font-style: normal;
}

/* --- Колонки на планшете --------------------------------------------------- */

/*
   ЧЕТЫРЕ КОЛОНКИ НА ПЛАНШЕТЕ НЕ ПОМЕЩАЮТСЯ. Макет держит их в одну строку
   вплоть до мобильного брейкпоинта, и на 768px первая колонка ужимается до
   83px — в неё не влезает ни логотип, ни строка «Ваш город», а копирайт
   рвётся по слогам. Замер: 83 / 136 / 192 / 200 при равных правах `flex: 1`.

   Ниже 1000px раскладываем по две в ряд: колонка получает половину ширины
   минус промежуток, и содержимое перестаёт ужиматься.
*/
@media only screen and (max-width: 1000px) and (min-width: 768px) {
    .footer .footer-top {
        flex-wrap: wrap;
    }

    .footer .footer-top__col {
        flex: 0 0 calc(50% - 32px);
    }
}

/* --- Выбор города --------------------------------------------------------- */

/*
   ОФОРМЛЕНИЕ — КАК НА СТАРОМ САЙТЕ: цветная шапка во всю ширину окна с
   названием слева и крестиком справа, подпись «Фильтр:» над полем, список
   ссылок в три колонки, «Закрыть» внизу справа. Цвет взят из новой палитры
   (#ff491a), а не из старой: это то же окно, приведённое к текущему сайту.

   Общая рамка окна убрана в ноль, чтобы шапка доходила до краёв; отступы
   переехали внутрь секций.
*/
.city-modal.wrap-modal {
    max-width: 760px;
    padding: 0;
    border-radius: 16px;
    overflow: hidden;
}

.city-modal .city-modal__header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin: 0;
    padding: 18px 24px;
    background: #ff491a;
    text-align: left;
}

.city-modal .city-modal__header .modal-title {
    font-size: 22px;
    line-height: 120%;
    color: #fff;
}

/* Крестик — кнопка, а не значок: его нажимают, и он должен ловиться
   клавиатурой наравне с «Закрыть» внизу. */
.city-modal .city-modal__close {
    flex: none;
    width: 32px;
    height: 32px;
    padding: 0;
    background: none;
    border: 0;
    font-size: 26px;
    line-height: 1;
    color: #fff;
    opacity: .85;
    cursor: pointer;
}

.city-modal .city-modal__close:hover,
.city-modal .city-modal__close:focus-visible {
    opacity: 1;
}

.city-modal .city-modal__body {
    padding: 20px 24px 22px;
}

.city-modal .city-modal__label {
    display: block;
    margin: 0 0 8px;
    font-weight: 700;
    font-size: 15px;
    color: #111;
}

.city-modal .city-modal__filter {
    margin: 0 0 18px;
}

/* Нижняя строка с «Закрыть»: отделена той же линией, что и в старом окне. */
.city-modal .city-modal__footer {
    display: flex;
    justify-content: flex-end;
    margin: 18px 0 0;
    padding: 14px 0 0;
    border-top: 1px solid #ececec;
}

.city-modal .city-modal__close-link {
    padding: 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: 15px;
    color: #ff491a;
    cursor: pointer;
}

.city-modal .city-modal__close-link:hover,
.city-modal .city-modal__close-link:focus-visible {
    text-decoration: underline;
}

/*
   СПИСОК РАСКЛАДЫВАЕТСЯ САМ. В макете было три колонки по двенадцать пунктов,
   набитых вручную; городов 354, и колонки должны считаться по ширине окна, а
   не по числу элементов в разметке.
*/
.wrap-modal--select .choice-city-list--all {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 4px 20px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.wrap-modal--select .choice-city-list__item {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Города — цветные ссылки, как в старом окне: там весь список читается как
   набор переходов, а не как перечисление. */
.wrap-modal--select .choice-city-list__link {
    display: block;
    padding: 5px 0;
    font-size: 15px;
    line-height: 130%;
    color: #ff491a;
    border-bottom: 1px solid transparent;
}

.wrap-modal--select .choice-city-list__link:hover,
.wrap-modal--select .choice-city-list__link:focus-visible {
    text-decoration: underline;
}

/* Текущий город виден и без подсветки — у него `aria-current`. Отличается
   не цветом (цвет здесь у всех), а насыщенностью и тем, что не подчёркнут. */
.wrap-modal--select .choice-city-list__link.is-current {
    font-weight: 700;
    color: #111;
    cursor: default;
}

.wrap-modal--select .choice-city-list__link.is-current:hover {
    text-decoration: none;
}

/*
   Список прокручивается внутри окна: 354 города иначе растянут его на
   несколько экранов, и поле поиска уедет за верхний край.

   `display: block` — не косметика. В макете контейнер был флексом на три
   колонки-`<ul>`, набитые вручную; наш единственный список становился в нём
   флекс-элементом и сжимался до ширины содержимого — замер дал 200px из 712
   доступных, то есть сетка получала ОДНУ колонку вместо четырёх, и увидеть
   это можно было только замером: список выглядел просто узким.
*/
.wrap-modal--select .wrap-choice-city-list {
    display: block;
    max-height: 52vh;
    overflow-y: auto;
    padding-right: 4px;
}

.wrap-modal--select .choice-city-empty {
    padding: 18px 0;
    font-size: 15px;
    color: #667085;
}

@media only screen and (max-width: 767px) {
    /* Заголовок в 22px рядом с крестиком занимает на 358px три строки. */
    .city-modal .city-modal__header {
        padding: 14px 16px;
    }

    .city-modal .city-modal__header .modal-title {
        font-size: 18px;
    }

    .city-modal .city-modal__body {
        padding: 16px 16px 18px;
    }

    .wrap-modal--select .choice-city-list--all {
        grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
        gap: 2px 14px;
    }

    /* На телефоне окно и так во весь экран — списку остаётся меньше места,
       но поле поиска должно оставаться видимым. */
    .wrap-modal--select .wrap-choice-city-list {
        max-height: 46vh;
    }
}

@media only screen and (max-width: 359px) {
    .wrap-modal--select .choice-city-list--all {
        grid-template-columns: 1fr;
    }
}

/* ------------------------------------------------ оговорка о ценах -- */

/*
 * Строка о том, что цены не являются публичной офертой. Печатается один раз, из
 * `config/legal.php`, — как и согласия под формами: юридическую формулировку
 * правят в одном месте.
 *
 * Мелким и приглушённым намеренно: это сноска, а не сообщение посетителю. Она
 * должна быть прочитана тем, кто её ищет, и не перетягивать внимание с контактов.
 *
 * Фон — тот же тёмный, что у нижней полосы подвала (#363636 из вёрстки), и
 * текст на нём светлый. Белая плашка между тёмным подвалом и нижней полосой
 * читалась как чужой блок, случайно попавший в подвал.
 *
 * Цвет здесь повторён числом, а не унаследован: правило нижней полосы живёт в
 * `style.min.css`, который не редактируется (CLAUDE.md §8), и «унаследовать»
 * оттуда нечего — это соседний элемент, а не родитель.
 */
.wrap-footer-note {
    padding: 24px 16px 0;
    background: #363636;
}

.footer-note {
    max-width: 1400px;
    margin: 0 auto;
    font-size: 12px;
    line-height: 150%;
    /*
     * Не чистый белый: сноска не должна спорить с телефонами и меню, которые
     * рядом. Приглушённый светлый читается как примечание и остаётся читаемым —
     * контраст с #363636 около 7:1, выше требования WCAG AA для мелкого текста.
     */
    color: #c9c9c9;
}

/* --- Копирайт в нижней полосе -------------------------------------------- */

/*
 * ЧИТАЕМЫЙ, А НЕ ЕДВА ЗАМЕТНЫЙ. В вёрстке эта строка залита `#535353` на фоне
 * `#363636` — замер даёт контраст 1.57:1 при минимуме 4.5:1 по WCAG для
 * обычного текста. На скриншоте её попросту не видно.
 *
 * `#a8a8a8` — 5.1:1: строка читается и остаётся второстепенной, не споря с
 * телефонами и меню над ней. Тот же приглушённый светлый, что у оговорки о
 * ценах рядом.
 */
.footer-bottom__copy {
    color: #a8a8a8;
}
