/* === 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/service-children.css === */
/* ==========================================================================
   «Отдельные услуги раздела» — карусель карточек под статьёй

   КАРТОЧКА ТУТ ЧУЖАЯ И ЭТО НАРОЧНО: её печатает общий партиал `service-card`,
   а её геометрия живёт в `repair-services.css`, подключённом в обе сборки. В
   этом файле только обрамление блока и то, что делает ряд лентой.
   ========================================================================== */

.service-children {
    margin-top: 20px;
}

/*
 * ЗАГОЛОВОК И СТРЕЛКИ — ОДНА СТРОКА, КАК В БЛОКЕ ПОРТФОЛИО. Полосу под
 * заголовком и оранжевый отрезок слева рисует `photos-block-title` из вёрстки
 * (`::before` + `::after`); здесь только место под стрелки справа, чтобы
 * длинный заголовок не заезжал под них.
 */
.service-children__head {
    position: relative;
}

.service-children__title {
    max-width: calc(100% - 110px);
    margin: 0;
}

/*
 * ОТСТУП ПОСЛЕ ПОДВОДКИ ОБЯЗАТЕЛЕН. Без него абзац прилипал к первому ряду
 * карточек: три блока подряд читались как один сплошной кусок.
 */
.service-children__intro {
    margin: 20px 0 28px 0;
}

.service-children__intro p {
    margin: 0;
}

/*
 * ШИРИНА СЛАЙДА СЧИТАЕТСЯ ОТ КОНТЕЙНЕРА, А НЕ ЗАДАЁТСЯ В ПИКСЕЛЯХ.
 *
 * `slidesPerView: 'auto'` означает «сколько поместится», и поместится ровно
 * столько, сколько разрешит эта ширина. ПЕРВАЯ ПОПЫТКА СТОЯЛА НА 280px, и
 * замер показал, что в ряд встают ТРИ карточки, а не четыре: область страницы
 * на 1440 равна 1146px, за вычетом отступов остаётся 1106, а четыре слайда с
 * тремя промежутками требуют 1192. Глазами эта разница не читается — блок
 * выглядит просто «немного другим», чем витрина на главной.
 *
 * Поэтому ширина выражена долей: четыре карточки в ряд ровно, как в сетке
 * главной (там колонка 256px), и то же число на любой ширине области.
 */
.service-children .swiper-slide {
    width: calc((100% - 3 * 24px) / 4);
    height: auto;         /* карточки одной высоты: её задаёт самая длинная */
}

@media only screen and (max-width: 1100px) {
    .service-children .swiper-slide {
        width: calc((100% - 2 * 24px) / 3);
    }
}

@media only screen and (max-width: 900px) {
    .service-children .swiper-slide {
        width: calc((100% - 24px) / 2);
    }
}

/*
 * КАРТОЧКА ТЯНЕТСЯ НА ВСЮ ВЫСОТУ СЛАЙДА. Без этого кнопки «Расчёт стоимости» у
 * соседей встают на разной высоте — ровно та беда, что уже была в отзывах:
 * равная высота карточек не выравнивает то, что внутри них (§8).
 */
.service-children .swiper-slide > .repair-list__item {
    height: 100%;
}

/*
 * ЛЕНТА НЕ ОБРЕЗАЕТСЯ ПО КРАЮ КОЛОНКИ, А УХОДИТ ЗА НЕЁ. Внутренние отступы
 * `.wrap-text` съедают по 20px с каждой стороны, и слайд, выехавший за границу,
 * иначе виден наполовину под текстом. `overflow: hidden` у самого Swiper это и
 * делает — здесь только возвращаем ему право обрезать.
 */
.service-children__slider {
    overflow: hidden;
}

/* --- Стрелки -------------------------------------------------------------- */

/*
 * СТРЕЛКИ СТОЯТ НА УРОВНЕ ЗАГОЛОВКА, а не под лентой: так их видно до того, как
 * человек доскроллит до конца ряда, и так же сделано в блоке портфолио на
 * главной. `.slider-btns` из вёрстки уже абсолютен — здесь только точка отсчёта.
 */
.service-children__nav {
    position: absolute;
    right: 0;
    bottom: 14px;
    display: flex;
    gap: 9px;
    margin: 0;
    border: 0;
}

/*
 * САМА КНОПКА НЕ ОПИСЫВАЕТСЯ ЗДЕСЬ ВОВСЕ.
 *
 * `.swiper-button-prev/next` в `style.min.css` уже готовы: 38×38, белый фон,
 * оранжевая рамка, скругление 6, значок `icon-arrow3` на 17px и поворот на
 * 180° у «назад». Это ровно тот вид, что в блоке портфолио, — и добавлять к
 * нему нечего.
 *
 * СВОЙ SVG СЮДА СТАВИТЬ НЕЛЬЗЯ: у кнопки в макете стоит
 * `svg{width:100%;height:100%}` плюс поворот для «назад», и любая вставленная
 * картинка растягивается во всю кнопку и разворачивается. Так и получилась
 * стрелка вдвое крупнее и жирнее эталонной.
 */

/*
 * Свою надпись Swiper печатает в `::after` («prev»/«next»); значок вёрстки
 * стоит отдельным `<i>`, и подпись рядом с ним лишняя.
 */
.service-children__nav .swiper-button-prev::after,
.service-children__nav .swiper-button-next::after {
    content: none;
}

/*
 * НЕАКТИВНУЮ СТРЕЛКУ ГАСИТ ВЁРСТКА, А НЕ ЭТОТ ФАЙЛ.
 *
 * У `.swiper-button-disabled` в макете уже стоит `opacity: .35` — та самая
 * бледная кнопка, которая на образце. Своя пара цветов здесь СТОЯЛА и НЕ
 * ПРИМЕНЯЛАСЬ: правило лежит в собранном `inner.css` последним, `matches()` на
 * нём даёт `true`, а вычисленный цвет остаётся оранжевым — причём то же самое
 * происходит и со стилем, вставленным в страницу на лету. Это тот случай, о
 * котором предупреждает §8: `getComputedStyle` и состояние здесь расходятся, и
 * разбирательство стоит дороже, чем пользы от двух своих оттенков.
 *
 * Поэтому цвет не трогаем вовсе — оставляем поведение вёрстки, а от себя
 * добавляем только то, что применяется и проверено: курсор и невозможность
 * нажать.
 */
.service-children__nav .swiper-button-disabled {
    cursor: default;
    pointer-events: none;
}

/*
 * ВСЁ ПОМЕСТИЛОСЬ — СТРЕЛОК НЕТ. Swiper вешает `swiper-button-lock` сам;
 * неактивная стрелка, на которую нельзя нажать, читается как сломанная.
 */
.service-children__nav .swiper-button-lock {
    display: none;
}

.service-children__nav:not(:has(.swiper-button-prev)),
.service-children__nav:has(.swiper-button-lock) {
    display: none;
}

/* --- Точки ---------------------------------------------------------------- */

.service-children .swiper-pagination {
    position: static;
    margin-top: 16px;
}

/*
 * ЛИСТАТЬ НЕЧЕГО — ТОЧЕК НЕТ. Swiper вешает `swiper-pagination-lock` сам, но
 * высоту он обнуляет, а внешний отступ остаётся: под блоком из двух карточек
 * висели бы пустые 16px, и объяснить их было бы нечем.
 */
.service-children .swiper-pagination-lock {
    display: none;
}

@media only screen and (max-width: 767px) {
    .service-children .swiper-slide {
        /* На телефоне карточка занимает почти всю ширину: соседняя видна краем
           и подсказывает, что ленту можно листать. */
        width: 82%;
        max-width: 300px;
    }
}

/* === homepage/css/service-prices.css === */
/* ==========================================================================
   «Ориентировочная стоимость» — строки прайса раздела

   ТАБЛИЦУ РИСУЕТ `price-list.css`, ОБЩИЙ С ГЛАВНОЙ И СО СТРАНИЦЕЙ «ЦЕНЫ». Здесь
   только обрамление блока: заголовок, подводка и отступы вокруг них.
   ========================================================================== */

.service-prices {
    margin-top: 20px;
}

.service-prices__title {
    margin: 0;
}

/*
 * ОТСТУПЫ ВОКРУГ ПОДВОДКИ. Без них абзац стоял вплотную и к заголовку, и к
 * шапке таблицы: три строки разного назначения читались одним куском, и
 * оговорка про материалы терялась ровно там, где она важнее всего.
 */
.service-prices__intro {
    margin: 20px 0 24px 0;
}

.service-prices__intro p {
    margin: 0;
}

/*
 * Кнопка под таблицей — та же, что под блоками главной (`block-cta.css` даёт ей
 * геометрию). Здесь только воздух между ней и последней строкой прайса.
 */
.service-prices__actions {
    margin-top: 28px;
}

/* === 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/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/service-photos.css === */
/* ==========================================================================
   «Примеры работ» раздела каталога — ряд фотографий под статьёй

   ЧЕТЫРЕ В РЯД, КАК БЫЛО. На старом сайте блок печатал четыре кадра в строку, и
   в архиве у 103 разделов из 112 их ровно четыре.

   КОЛОНОК СТОЛЬКО, СКОЛЬКО КАДРОВ (но не больше четырёх), И ЧИСЛО ПЕЧАТАЕТ
   ШАБЛОН. `repeat(auto-fit, minmax(220px, 1fr))` раскладывал четыре кадра на
   768px в ТРИ колонки, и четвёртый оставался в ряду один — это читается как
   «одна фотография не загрузилась». А жёсткие четыре колонки оставляли бы
   половину строки пустой у тех девяти разделов, где кадров два или три.

   НА УЗКОМ — ДВЕ. Замер: на 1440 область страницы 1106px и четыре колонки дают
   по 259px; на 768px те же четыре дали бы по 170px — это уже не фотография
   интерьера, а марка. Две колонки складывают четыре кадра в 2×2 без сироты.
   ========================================================================== */

.service-photos {
    /* Отступ такой же, как между блоками статьи: своей константы не заводим. */
    margin-top: 20px;
}

.service-photos__intro {
    margin: 0 0 24px 0;
}

.service-photos__title {
    /*
     * H2 блока — того же вида, что заголовки разделов статьи. Собственного
     * кегля здесь нет намеренно: два соседних H2 на одной странице, набранные
     * по-разному, читаются как разные уровни.
     */
    margin: 0 0 12px 0;
}

.service-photos__grid {
    display: grid;
    /* Запасное значение — на случай, если переменную не напечатали. */
    grid-template-columns: repeat(var(--service-photos-cols, 4), 1fr);
    gap: 24px;
    /* Это список, и маркеры ему не нужны: содержание несут сами кадры. */
    margin: 0;
    padding: 0;
    list-style: none;
}

.service-photos__item {
    margin: 0;
    padding: 0;
}

/*
 * КАДРЫ РАЗНОЙ ФОРМЫ ВЫРАВНИВАЮТСЯ РАМКОЙ. В архиве 36 фотографий из 446
 * вертикальные, а ширина исходников гуляет от 371 до 2592 px: без общей
 * пропорции ряд получился бы лесенкой. `object-fit: cover` обрезает по краям,
 * а не сжимает, — иначе интерьер выглядел бы растянутым.
 */
.service-photos__img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: 4px;
}

/*
 * Порог 1100px — тот же, на котором вёрстка убирает левую колонку и на котором
 * уже считают крошки и фильтр портфолио. Своего числа здесь не заводим.
 */
@media only screen and (max-width: 1099px) {
    .service-photos__grid {
        /* Не больше двух — и не больше, чем кадров: у пары свои две колонки. */
        grid-template-columns: repeat(min(var(--service-photos-cols, 4), 2), 1fr);
    }
}

/*
 * НА ТЕЛЕФОНЕ — ПО ОДНОЙ, КАК В ПОРТФОЛИО. Замер на 390px: две колонки дают
 * кадр 171×128, и разглядеть в нём интерьер невозможно — это уже не пример
 * работы, а пиктограмма. Сетка портфолио (`minmax(250px, 1fr)`) на той же
 * ширине показывает работы по одной, и порог здесь взят из неё: две колонки
 * живут ровно до тех пор, пока каждая шире 250px.
 */
@media only screen and (max-width: 559px) {
    .service-photos__grid {
        grid-template-columns: 1fr;
        gap: 16px;
    }
}

/* === 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/breadcrumbs.css === */
/*
 * Хлебные крошки внутренних страниц.
 *
 * В макете нового сайта их нет: он рисовался под одну длинную главную, где
 * идти некуда. Внутренним страницам путь нужен — это и навигация, и то, из чего
 * поисковая система строит цепочку в выдаче.
 *
 * ОФОРМЛЕНИЕ ВЗЯТО У СТРАНИЦЫ, А НЕ ПРИДУМАНО: те же 16px внешних отступов и
 * та же максимальная ширина, что у контейнера статьи, — иначе крошки поедут
 * относительно текста под ними.
 */

.breadcrumbs {
    padding: 16px 0 0;
    font-size: 13px;
    line-height: 140%;
    color: #999;
}

.breadcrumbs__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 8px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/*
 * Разделитель рисуется псевдоэлементом ПУНКТА, а не отдельным узлом: лишний
 * <span> между ссылками скринридер зачитывает как содержимое, и путь начинает
 * звучать «Главная слэш О компании».
 */
.breadcrumbs__item + .breadcrumbs__item::before {
    content: "→";
    margin-right: 8px;
    color: #c9c9c9;
}

.breadcrumbs__link {
    color: #999;
    text-decoration: none;
    border-bottom: 1px solid rgba(153, 153, 153, 0.4);
}

.breadcrumbs__link:hover {
    color: #ff491a;
    border-bottom-color: rgba(255, 73, 26, 0.4);
}

.breadcrumbs__link:focus-visible {
    outline: 2px solid #ff5c00;
    outline-offset: 2px;
    border-radius: 2px;
}

/* Текущая страница — не ссылка: она уже открыта. */
.breadcrumbs__current {
    color: #666;
}

@media only screen and (max-width: 560px) {
    .breadcrumbs {
        font-size: 12px;
    }
}

/* --- Выравнивание по тексту статьи --------------------------------------- */

/*
 * Крошки идут по той же линии, что первая буква текста под ними.
 *
 * ЧИСЛА ВЗЯТЫ У `.wrap-text` ИЗ ВЁРСТКИ: 60px, 20px ниже 1620 и 16px ниже
 * 1100. Своей константы тут быть не может — это чужой внутренний отступ, и
 * повторяются ровно его брейкпоинты, чтобы линия не разъехалась на промежутке.
 *
 * Ниже 1100 отступа нет вовсе: там `.wrap-text` растягивается на всю ширину
 * контейнера и его 16px совпадают с полем самой страницы — крошки и текст
 * встают по одной линии сами.
 */
.page--inner .breadcrumbs {
    padding-left: 60px;
    padding-right: 60px;
}

@media only screen and (max-width: 1620px) {
    .page--inner .breadcrumbs {
        padding-left: 20px;
        padding-right: 20px;
    }
}

@media only screen and (max-width: 1100px) {
    .page--inner .breadcrumbs {
        padding-left: 0;
        padding-right: 0;
    }
}

/* === 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/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/sidebar-nav.css === */
/* ==========================================================================
   Боковая колонка внутренней страницы

   ЧЕГО ЗДЕСЬ НЕТ. Ширины колонки, её липкости и отступов виджетов — всё это
   приходит из вёрстки и одинаково на главной и на внутренних страницах. В этом
   файле только то, что вёрстка не могла предусмотреть: у главной в колонке
   девятнадцать пунктов, у внутренней страницы — один.
   ========================================================================== */

/*
   ВЫСОТА МЕНЮ — ПО СОДЕРЖИМОМУ, А НЕ ПО МАКЕТУ ГЛАВНОЙ.

   В вёрстке у `.navigation` стоит `height: 430px` со скроллом: столько занимают
   девятнадцать пунктов главной. На внутренней странице пункт обычно один, и эти
   430px оставались пустыми — между «Наши работы» и следующим виджетом зияло
   386px белого поля, а фильтр работ уезжал вниз от меню, к которому относится.

   `max-height` вместо `height` сохраняет исходное поведение там, где оно нужно:
   длинный список по-прежнему прокручивается внутри колонки, а короткий занимает
   ровно свою высоту. Число то же самое, макетное, — своей константы здесь не
   заводится.
*/
.page--inner .navigation {
    height: auto;
    max-height: 430px;
}

/*
   ГРАНИЦА МЕЖДУ ВИДЖЕТАМИ КОЛОНКИ.

   На главной их два — меню и баннер, — и баннер отделяет себя сам: он тёмная
   картинка во всю ширину. У внутренней страницы между меню и фильтром работ
   такой границы не было: два блока с одинаковыми отступами и одинаковыми
   оранжевыми заголовками читались как один длинный список, и «Подобрать
   работы» выглядело вторым пунктом навигации.

   Линия, а не отступ: воздуха между ними и так 44px (по 22 от паддинга
   каждого виджета), и увеличение промежутка проблему не решало — сливались
   они не от тесноты, а от отсутствия края.

   ЦВЕТ ВЗЯТ У СОСЕДА. #efefef — та же линия, что вёрстка рисует под
   заголовком виджета; своего цвета колонка не заводит.

   ПЕРВЫЙ ВИДЖЕТ ЛИНИИ НЕ ПОЛУЧАЕТ: `+` выбирает только тех, у кого есть
   предшественник, — иначе черта повисла бы под шапкой колонки.
*/
.page--inner .sidebar > * + * {
    border-top: 1px solid #efefef;
}

/* === homepage/css/portfolio-page.css === */
/* ==========================================================================
   Раздел «Портфолио»: перечень работ и страница одной работы

   ЧЕГО ЗДЕСЬ НЕТ. Сетки страницы, колонок и отступов контейнера — они приходят
   из вёрстки, как у главной и у «О компании». В этом файле только то, чего в
   макете не было вовсе: карточка работы, фильтр и галерея объекта.

   ЦВЕТА — МАКЕТНЫЕ. #ff491a это фирменный оранжевый (62 упоминания в вёрстке),
   #667085 — второстепенный текст, #e6e6e6 — рамка. Своей палитры раздел не
   заводит.
   ========================================================================== */

/* --- Заголовок и вступление ---------------------------------------------- */

.portfolio-page__title,
.portfolio-project__title {
    margin-bottom: 20px;
}

.portfolio-page__intro,
.portfolio-project__lead {
    margin-bottom: 16px;
    color: #535353;
}

/* --- Фильтр --------------------------------------------------------------- */

/*
   ДВА МЕСТА, ОДНА РАЗМЕТКА. Ниже 1100px левая колонка перестаёт быть колонкой:
   вёрстка делает её выезжающей панелью бургер-меню. Фильтр работ там не найдут,
   поэтому он печатается дважды и каждый раз скрыт в чужом диапазоне. Порог тот
   же, что у крошек и у самой вёрстки, — своего числа тут быть не может.
*/
.portfolio-filters--inline {
    margin-bottom: 28px;
    padding: 16px 16px 20px;
    border: 1px solid #e6e6e6;
    border-radius: 8px;
    background: #fbfbfb;
}

@media only screen and (min-width: 1101px) {
    .portfolio-filters--inline {
        display: none;
    }
}

@media only screen and (max-width: 1100px) {
    .portfolio-filters--side {
        display: none;
    }
}

/* --- Шапка фильтра -------------------------------------------------------- */

/*
   ШАПКА В КОЛОНКЕ — ТА ЖЕ, ЧТО У МЕНЮ. Оранжевый цвет, кегль, нижнюю линию и
   короткий оранжевый отрезок под ней даёт класс `widget-title` из вёрстки;
   здесь только раскладка значка рядом с текстом, потому что заголовок вёрстки
   про иконку ничего не знает. Переписывать его размеры своими числами нельзя:
   это была бы вторая редакция одного заголовка, и разъехались бы они при первой
   правке колонки.
*/
.portfolio-filters__head {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 12px;
    align-items: baseline;
    justify-content: space-between;
}

.portfolio-filters--inline .portfolio-filters__head {
    margin-bottom: 12px;
}

.portfolio-filters__title {
    display: flex;
    gap: 8px;
    align-items: center;
}

/* У блока над карточками своего виджета нет — заголовок описан целиком. */
.portfolio-filters--inline .portfolio-filters__title {
    margin: 0;
    font-size: 18px;
    line-height: 1.25;
}

/*
   Заголовок виджета — блочный элемент во всю ширину колонки, иначе оранжевая
   линия под ним обрежется по длине текста и перестанет отделять фильтр от
   соседнего виджета.
*/
.portfolio-filters--side .portfolio-filters__title {
    width: 100%;
}

/*
   Значок наследует цвет заголовка (`currentColor` у обводки), поэтому иконка и
   текст не могут разойтись по цвету при первой же правке.
*/
.portfolio-filters__icon {
    flex: 0 0 auto;
    color: currentColor;
    transition: transform 0.2s ease;
}

.portfolio-filters--inline .portfolio-filters__icon {
    color: #ff491a;
}

.portfolio-filters__reset {
    color: #667085;
    font-size: 13px;
    text-decoration: underline;
    transition: color 0.18s ease;
}

.portfolio-filters__reset:hover {
    color: #ff491a;
}

/* Сброс в колонке стоит под заголовком, а не рядом: места на строку нет. */
.portfolio-filters--side .portfolio-filters__reset {
    margin-bottom: 10px;
}

/* --- Группа признака ------------------------------------------------------ */

.portfolio-filters__group + .portfolio-filters__group {
    margin-top: 10px;
}

/*
   ОРАНЖЕВАЯ ШАПКА ГРУППЫ — вид со старого сайта: там признак назывался белым по
   фирменному оранжевому, и по этим плашкам фильтр читался с одного взгляда.
   Отличие только в скруглении и высоте — они от нового оформления.
*/
.portfolio-filters__label {
    padding: 7px 12px;
    border-radius: 6px 6px 0 0;
    background: #ff491a;
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: 0.02em;
}

.portfolio-filters__options {
    display: flex;
    flex-direction: column;
    padding: 4px 0;
    border: 1px solid #ececec;
    border-top: 0;
    border-radius: 0 0 6px 6px;
    background: #fff;
}

/* --- Вариант --------------------------------------------------------------- */

/*
   ВАРИАНТ — ССЫЛКА, ОФОРМЛЕННАЯ ПЕРЕКЛЮЧАТЕЛЕМ. Роль состояния несёт
   `aria-pressed`, а не цвет: цвет читают глазами, состояние — скринридером.
*/
.portfolio-filters__option {
    display: flex;
    gap: 8px;
    align-items: flex-start;
    padding: 6px 12px;
    color: #363636;
    font-size: 14px;
    line-height: 1.3;
    transition: background-color 0.18s ease, color 0.18s ease;
}

.portfolio-filters__option:hover {
    background: #fff5f2;
    color: #363636;
}

.portfolio-filters__option.is-on {
    color: #d92c00;
    font-weight: 500;
}

/* Квадратик с галочкой — тот же элемент управления, что был на старом сайте. */
.portfolio-filters__box {
    display: flex;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    margin-top: 1px;
    align-items: center;
    justify-content: center;
    border: 1px solid #c9c9c9;
    border-radius: 3px;
    background: #fff;
    color: #fff;
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.portfolio-filters__option:hover .portfolio-filters__box {
    border-color: #ffa693;
}

.portfolio-filters__option.is-on .portfolio-filters__box {
    border-color: #ff491a;
    background: #ff491a;
}

/*
   ГАЛОЧКА ПОЯВЛЯЕТСЯ, А НЕ ВОЗНИКАЕТ. Полтора десятка вариантов переключаются
   подряд, и мгновенная подстановка читается как подёргивание списка; 0.18s
   хватает, чтобы глаз заметил, что именно изменилось, и не успел заскучать.
*/
.portfolio-filters__box svg {
    opacity: 0;
    transform: scale(0.5);
    transition: opacity 0.18s ease, transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1);
}

.portfolio-filters__option.is-on .portfolio-filters__box svg {
    opacity: 1;
    transform: scale(1);
}

.portfolio-filters__option-name {
    flex: 1 1 auto;
    min-width: 0;
}

.portfolio-filters__option-count {
    flex: 0 0 auto;
    color: #a9a9a9;
    font-size: 12px;
    line-height: 1.5;
}

.portfolio-filters__option.is-on .portfolio-filters__option-count {
    color: #ff8a6b;
}

.portfolio-filters__option:focus-visible {
    outline: 2px solid #ff5c00;
    outline-offset: -2px;
    border-radius: 4px;
}

/*
   ДВИЖЕНИЕ — НЕ ОБЯЗАТЕЛЬНАЯ ЧАСТЬ ИНТЕРФЕЙСА. Человеку, который попросил
   систему не анимировать, фильтр обязан работать так же; переходы здесь
   украшение, и снять их можно целиком.
*/
@media (prefers-reduced-motion: reduce) {
    .portfolio-filters__icon,
    .portfolio-filters__option,
    .portfolio-filters__box,
    .portfolio-filters__box svg,
    .portfolio-filters__reset {
        transition: none;
    }
}

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

/*
   ШИРИНУ ЗАДАЁТ КОНТЕЙНЕР, А НЕ ОКНО. Раздел лежит в правой колонке страницы
   (`.container .content`), поэтому раскладка описана через `auto-fill` и
   минимальную ширину карточки: медиазапрос по ширине окна здесь промахнулся бы
   — колонка уже окна на ширину боковой панели (§8).

   250px — НЕ КРУГЛОЕ ЧИСЛО, А ПОРОГ ЧЕТВЁРТОЙ КОЛОНКИ. Страница отдаёт восемь
   работ, чтобы они легли двумя рядами по четыре; при 280px четвёртая колонка
   появлялась только на очень широком экране, и на распространённых 1440px ряд
   ломался на 3 + 3 + 2.

   СЧИТАТЬ НАДО ОТ ШИРИНЫ СЕТКИ, А НЕ КОЛОНКИ КОНТЕНТА. Первая попытка взяла
   1146px у `.content` и дала 260px — не хватило шести пикселей: сетка лежит
   внутри `.wrap-text`, и его боковые отступы (20px на этой ширине) съедают 40.
   Настоящая ширина 1106px, значит карточка не шире (1106 − 3×24) / 4 = 258px.
   250 оставляет запас на другой отступ и на полосу прокрутки.
*/
.portfolio-page__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 24px;
    margin-bottom: 28px;
}

.portfolio-card {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid #e6e6e6;
    border-radius: 8px;
    background: #fff;
}

/*
   РАМКА КАДРА ФИКСИРОВАННОЙ ПРОПОРЦИИ. В архиве 677 вертикальных фотографий из
   1110, и без общей пропорции ряд карточек шёл бы ступенькой: у одной кадр
   400 px высотой, у соседней 900. `object-fit: cover` кадрирует по центру, а
   `aspect-ratio` держит место до загрузки — вместе с `width`/`height` у самой
   картинки это снимает сдвиг вёрстки (§7).
*/
.portfolio-card__photo {
    display: block;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    background: #f5f5f5;
}

/*
   `<picture>` ОБЯЗАН РАСТЯНУТЬСЯ, ИНАЧЕ `height: 100%` У КАРТИНКИ НЕ ОТ ЧЕГО
   СЧИТАТЬ.

   Это и был дефект. `<picture>` — строчный элемент без своей высоты, поэтому
   процент у вложенного `img` не разрешался, `object-fit` не срабатывал и кадр
   рисовался в собственных пропорциях: рамка `aspect-ratio` оставалась, а
   картинка её игнорировала. Замер показывал высоты 209, 209, 156 и 376 px в
   одном ряду — у вертикальных снимков (а их в архиве 677 из 1110) в карточку
   попадала верхняя часть комнаты, то есть потолок.
*/
.portfolio-card__photo picture {
    display: block;
    width: 100%;
    height: 100%;
}

.portfolio-card__photo img {
    width: 100%;
    height: 100%;
    /*
       Кадрируем по центру — и по горизонтали, и по вертикали. У вертикального
       снимка это середина комнаты, а не её верх: интерьер узнаётся по мебели и
       отделке стен, а не по потолку.
    */
    object-fit: cover;
    object-position: center center;
    transition: transform 0.3s;
}

.portfolio-card__photo:hover img {
    transform: scale(1.03);
}

.portfolio-card__body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    padding: 16px 18px 18px;
}

.portfolio-card__title {
    margin: 0 0 8px;
    font-size: 18px;
    line-height: 1.3;
}

.portfolio-card__title a {
    color: #363636;
}

.portfolio-card__title a:hover {
    color: #ff491a;
}

.portfolio-card__description {
    margin: 0 0 12px;
    color: #535353;
    font-size: 15px;
    line-height: 1.45;
}

.portfolio-card__facts {
    margin: 0 0 16px;
}

.portfolio-card__fact {
    display: flex;
    gap: 10px;
    justify-content: space-between;
    padding: 5px 0;
    border-bottom: 1px solid #f1f1f1;
    font-size: 14px;
}

.portfolio-card__fact dt {
    color: #667085;
}

.portfolio-card__fact dd {
    margin: 0;
    color: #363636;
    text-align: right;
}

/*
   КНОПКА ПРИЖАТА КНИЗУ. Описания у работ разной длины (от 15 до 130 символов),
   и без этого кнопки в ряду вставали на разной высоте — тот же дефект, что уже
   разбирался в карточках отзывов (§8): одинаковая высота карточек не выравнивает
   то, что внутри них.

   ОФОРМЛЕНИЕ СВОЁ, ПОТОМУ ЧТО МАКЕТНОГО НЕТ. Кнопка носила классы `link
   ghost-btn text-uppercase` из макета, но `.ghost-btn` в вёрстке отсутствует —
   и оставалась системной синей ссылкой. Заливка фирменным оранжевым: это
   основное действие карточки, и выглядеть оно должно действием.
*/
.portfolio-card__more,
.portfolio-project__back {
    display: inline-block;
    padding: 11px 20px;
    border-radius: 6px;
    background: #ff491a;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    transition: background-color 0.18s ease;
}

.portfolio-card__more {
    margin-top: auto;
    align-self: flex-start;
}

.portfolio-card__more:hover,
.portfolio-project__back:hover,
.portfolio-card__more:focus-visible,
.portfolio-project__back:focus-visible {
    background: #d92c00;
    color: #fff;
}

.portfolio-card__more:focus-visible,
.portfolio-project__back:focus-visible {
    outline: 2px solid #ff5c00;
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .portfolio-card__more,
    .portfolio-project__back {
        transition: none;
    }
}

/*
   ОДИНАКОВАЯ ВЫСОТА КАРТОЧЕК НЕ ВЫРАВНИВАЕТ ТО, ЧТО ВНУТРИ НИХ (§8).

   Ряд растянут `stretch`, но каждая карточка раскладывает содержимое по длине
   собственного текста: заголовок в одну строку или в три, описание в две или в
   четыре — и таблица характеристик у соседей начиналась на разной высоте.
   Замер по ряду из четырёх: 1416, 1373, 1394, 1371 — разброс 45px, и глаз его
   ловит сразу, потому что подписи «Дизайн-проект» стоят лесенкой.

   ВЫРАВНИВАЕТ СЕТКА, А НЕ ФИКСИРОВАННАЯ ВЫСОТА. Высота в строках (`height:
   calc(3 * 1.3em)`) решала бы это ценой обрезки: у 217 городских работ, которые
   ещё не показываются, названия вроде «Ремонт однокомнатной квартиры на
   Двинской улице в Санкт-Петербурге» занимают пять строк, и константа их
   срезала бы. `subgrid` берёт высоту каждой полосы по самому длинному соседу в
   ряду — и подстроится сам, когда состав работ изменится.

   СТРОКА У КАЖДОЙ ЧАСТИ СВОЯ И ЗАДАНА ЯВНО. Описание есть не у всех работ, и
   при автоматическом размещении карточка без него сдвинула бы характеристики
   на полосу выше — то есть ровно то, от чего мы уходим.

   БЕЗ ПОДДЕРЖКИ `subgrid` ОСТАЁТСЯ ПРЕЖНЯЯ РАСКЛАДКА: карточка живёт flex-ом,
   как жила. Правило целиком под `@supports`, потому что половина этих свойств
   во flex-контейнере означает не то же самое.
*/
@supports (grid-template-rows: subgrid) {
    /*
       `row-gap: 0` ОБЯЗАТЕЛЕН. Субсетка наследует не только полосы родителя, но
       и его `gap` — а между карточками он 24px. Без сброса эти 24px оказывались
       ВНУТРИ карточки, между фотографией и заголовком, между описанием и
       таблицей: зазоры выросли с 8–16px до 32–40px, а сами карточки — почти на
       сотню пикселей. Промежуток между РЯДАМИ карточек это не трогает: он
       принадлежит родительской сетке, а не субсетке.
    */
    .portfolio-card {
        display: grid;
        /* Пять полос ряда: фото, заголовок, описание, характеристики, кнопка. */
        grid-row: span 5;
        grid-template-rows: subgrid;
        row-gap: 0;
    }

    .portfolio-card__body {
        display: grid;
        grid-row: span 4;
        grid-template-rows: subgrid;
        row-gap: 0;
    }

    .portfolio-card__title {
        grid-row: 1;
    }

    .portfolio-card__description {
        grid-row: 2;
    }

    .portfolio-card__facts {
        grid-row: 3;
        /* Таблица прижата к верху своей полосы: строки характеристик должны
           начинаться на одном уровне, а не расходиться от её длины. */
        align-self: start;
    }

    .portfolio-card__more {
        grid-row: 4;
        /* В сетке кнопку по горизонтали держит `justify-self`, а не
           `align-self`: тот отвечает за вертикаль и растянул бы её на всю
           полосу. */
        justify-self: start;
        align-self: end;
        margin-top: 0;
    }
}

/* --- Пустое состояние ----------------------------------------------------- */

.portfolio-page__empty {
    padding: 24px;
    border: 1px dashed #e6e6e6;
    border-radius: 8px;
    background: #fbfbfb;
    color: #535353;
}

/* --- Постраничность ------------------------------------------------------- */

.portfolio-pagination .pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.portfolio-pagination .pagination__item__link {
    display: inline-flex;
    min-width: 38px;
    height: 38px;
    align-items: center;
    justify-content: center;
    padding: 0 10px;
    border: 1px solid #e6e6e6;
    border-radius: 6px;
    background: #fff;
    color: #363636;
}

.portfolio-pagination .pagination__item.active .pagination__item__link {
    border-color: #ff491a;
    background: #ff491a;
    color: #fff;
}

.portfolio-pagination .pagination__item.is-disabled .pagination__item__link {
    color: #d0d0d0;
}

.portfolio-pagination a.pagination__item__link:hover {
    border-color: #ff491a;
    color: #ff491a;
}

/* --- Страница одной работы ------------------------------------------------ */

.portfolio-project__subtitle {
    margin: 28px 0 14px;
    font-size: 22px;
    line-height: 1.3;
}

.portfolio-project__facts {
    margin: 0 0 8px;
    max-width: 560px;
}

.portfolio-project__fact {
    display: flex;
    gap: 12px;
    justify-content: space-between;
    padding: 8px 0;
    border-bottom: 1px solid #f1f1f1;
}

.portfolio-project__fact dt {
    color: #667085;
}

.portfolio-project__fact dd {
    margin: 0;
    color: #363636;
    text-align: right;
}

.portfolio-project__text p {
    margin: 0 0 14px;
}

/* Вид кнопки описан вместе с «Подробнее» — здесь только её место на странице. */
.portfolio-project__back {
    margin-top: 12px;
}

/* --- Галерея объекта ------------------------------------------------------ */

.portfolio-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 16px;
}

.portfolio-gallery__item {
    margin: 0;
}

.portfolio-gallery__link {
    display: block;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    border-radius: 6px;
    background: #f5f5f5;
}

/* Та же причина, что у карточки: без этого процент высоты не от чего считать. */
.portfolio-gallery__link picture {
    display: block;
    width: 100%;
    height: 100%;
}

.portfolio-gallery__link img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center center;
    transition: transform 0.3s;
}

.portfolio-gallery__link:hover img {
    transform: scale(1.04);
}

.portfolio-gallery__caption {
    margin-top: 6px;
    color: #667085;
    font-size: 14px;
    line-height: 1.35;
}

/* --- Узкий экран ---------------------------------------------------------- */

@media (max-width: 480px) {
    .portfolio-page__grid,
    .portfolio-gallery {
        grid-template-columns: 1fr;
    }

    .portfolio-filters--inline {
        padding: 14px 14px 16px;
    }
}

/* === 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/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/prices-page.css === */
/*
 * Страница «Цены» — только заголовок, вступление и подпись даты.
 *
 * Сам прайс рисует `price-list.css`, тот же файл, что и на главной: у страницы
 * своей таблицы нет и быть не должно. Здесь остаются три отступа, потому что в
 * вёрстке у `h1` и `p` внутри `.wrap-text` обнулены и `margin-top`, и
 * `margin-bottom`: без этих правил заголовок, абзац, дата и первая вкладка
 * прайса склеиваются в одну плотную кладку.
 *
 * Числа те же, что у страницы портфолио (`portfolio-page.css`): 20px под
 * заголовком и 16px под абзацем. Это не совпадение и не копия ради копии —
 * страницы стоят в одном ряду в меню, и разные отступы под одинаковыми
 * заголовками читались бы как сбой, а не как замысел.
 */

.prices-page__title {
    margin-bottom: 20px;
}

.prices-page__intro {
    margin-bottom: 16px;
    color: #535353;
}

/*
 * Дата актуальности — служебная строка, а не часть текста: тот же кегль, но
 * приглушённая, и с отступом до таблицы. Точное значение цен она не описывает,
 * поэтому и не должна спорить за внимание с ними.
 */
.prices-page__updated {
    margin-bottom: 24px;
    color: #999;
    font-size: 14px;
}

/* === homepage/css/contacts-page.css === */
/*
 * Страница «Контакты»: список способов связи, просьба о звонке и карта.
 *
 * Своей раскладки у страницы нет — сетка, колонки и отступы приходят из вёрстки
 * вместе с общим каркасом внутренних страниц. Здесь только то, чего в макете не
 * было вовсе: строки контактов, рамка карты и плашка с просьбой.
 *
 * ОТСТУПЫ ТЕ ЖЕ, ЧТО У СОСЕДНИХ СТРАНИЦ (20px под заголовком, 16px под абзацем):
 * страницы стоят в одном ряду в меню, и разные отступы под одинаковыми
 * заголовками читались бы как сбой, а не как замысел.
 */

.contacts-page__title {
    margin-bottom: 20px;
}

.contacts-page__intro {
    margin-bottom: 24px;
    color: #535353;
}

.contacts-page__subtitle {
    margin: 0 0 12px;
    font-size: 20px;
    line-height: 1.3;
}

/* ------------------------------------------------------------- контакты -- */

/*
 * Строки — двумя колонками на desktop и в столбик на узком экране. Подпись
 * прижата к ширине самого длинного слова («Дополнительный телефон»), но не
 * больше трети: иначе на 320px значению остаётся полоска в несколько символов.
 */
.contacts-page__list {
    margin: 0 0 28px;
}

.contacts-page__row {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 16px;
    padding: 12px 0;
    border-bottom: 1px solid #efefef;
}

.contacts-page__row:first-child {
    border-top: 1px solid #efefef;
}

.contacts-page__label {
    flex: 0 0 auto;
    min-width: 180px;
    max-width: 33%;
    color: #999;
    font-size: 15px;
    line-height: 1.4;
}

.contacts-page__value {
    flex: 1 1 200px;
    margin: 0;
    font-size: 16px;
    font-weight: 700;
    line-height: 1.4;
}

/*
 * Ссылка телефона и почты — обычным цветом текста с подчёркиванием на наведении:
 * синяя системная ссылка среди чёрных значений читается как чужой элемент.
 */
.contacts-page__link {
    color: inherit;
    text-decoration: none;
}

.contacts-page__link:hover,
.contacts-page__link:focus-visible {
    color: #ff491a;
}

/*
 * График работы — отдельной строкой под списком, а не значением к подписи: в
 * данных это готовое предложение («Время работы с 9:00 до 18:00 в пн-пт»), и
 * подпись рядом читалась бы дважды. Отрицательный отступ сверху убирает зазор
 * после последней строки списка — она уже отделена линией.
 */
.contacts-page__hours {
    margin: -16px 0 28px;
    color: #535353;
}

/* ------------------------------------------------------- каналы связи --- */

.contacts-page__channels {
    margin: 0 0 28px;
}

/*
 * Тот же ряд кнопок, что в шапке и подвале, только крупнее и с обычным
 * межстрочным поведением: здесь у него не хватает соседей, чтобы выровняться по
 * ним, и без явного `gap` иконки слипаются.
 */
.btns--contacts {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
}

/* -------------------------------------------------------------- просьба -- */

/*
 * Плашка с просьбой согласовать визит. Не предупреждение об опасности и не
 * ошибка — поэтому спокойный светлый фон, а не красный: человек всего лишь
 * может не застать никого на месте.
 */
.contacts-page__notice {
    margin: 0 0 28px;
    padding: 16px 20px;
    border-left: 3px solid #ff491a;
    border-radius: 4px;
    background: #faf7f5;
}

.contacts-page__notice p {
    margin: 0;
    color: #535353;
}

/* ---------------------------------------------------------------- карта -- */

.contacts-page__map {
    margin: 0 0 8px;
}

/*
 * Рамка карты держит высоту САМА, до загрузки iframe. Без неё ленивая карта
 * появляется рывком и сдвигает всё, что под ней, — а сдвиг после загрузки это
 * ровно то, что считает CLS.
 */
.contacts-page__map-frame {
    overflow: hidden;
    border-radius: 8px;
    background: #f4f4f4;
}

.contacts-page__map-frame iframe {
    display: block;
    width: 100%;
    height: 360px;
    border: 0;
}

@media only screen and (max-width: 560px) {
    .contacts-page__label {
        min-width: 0;
        max-width: none;
        flex-basis: 100%;
    }

    .contacts-page__map-frame iframe {
        height: 280px;
    }
}

/* === homepage/css/service-page.css === */
/*
 * Страница раздела каталога: заголовок и текст.
 *
 * Своей раскладки у страницы нет — сетка, колонки и отступы приходят из вёрстки
 * вместе с общим каркасом внутренних страниц. Здесь только два отступа, потому
 * что в вёрстке у `h1` и `p` внутри `.wrap-text` обнулены и `margin-top`, и
 * `margin-bottom`: без них заголовок и текст склеиваются.
 *
 * Числа те же, что у соседних страниц (20px под заголовком, 16px под абзацем):
 * страницы стоят в одном каталоге, и разные отступы под одинаковыми
 * заголовками читались бы как сбой, а не как замысел.
 */

.service-page__title {
    margin-bottom: 20px;
}

.service-page__intro {
    margin-bottom: 16px;
    color: #535353;
}

/*
 * ЗАГОЛОВОК ПЕРВОГО ЭКРАНА БАЛАНСИРУЕТСЯ ПО СТРОКАМ.
 *
 * ЗАЧЕМ. Названий разделов 121, городов 354, и ширину строки маска заголовка
 * угадать не может. Раньше в ней стоял жёсткий перенос после запятой: замер по
 * всем 121 странице на колонке 474 px дал 73 заголовка в две строки, 45 в три
 * и 3 в четыре, причём вторая строка часто оказывалась одним словом («…прованс
 * в / Туле, / с выгодой до 30%»). Без переноса в две строки ложатся 112, в
 * четыре — ни одного.
 *
 * ЧТО ДОБАВЛЯЕТ БАЛАНСИРОВКА. Оставшимся девяти длинным названиям — ровные
 * строки вместо обрубка в конце: «Сантехник, сантехнические / работы в Брянске,
 * с выгодой до / 30%» превращается в «…работы в Брянске, / с выгодой до 30%».
 *
 * ГДЕ НЕ ПОДДЕРЖИВАЕТСЯ — просто обычный перенос: свойство необязательное, и
 * без него заголовок остаётся таким же верным, только строки чуть разной длины.
 *
 * ТОЛЬКО ВНУТРЕННИЕ СТРАНИЦЫ: у главной заголовок один, его длину знают заранее,
 * и строки в нём разбиты нарочно.
 */
.page--inner .calc__title {
    text-wrap: balance;
}

/* === 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;
}
