/* ══════════════════════════════════════════════════════════════════
   Ferrara Finishes — SHARED STYLESHEET
   Tokens + components confirmed in "Design System v2.dc.html".
   Edit here once; every page that links this file picks up the change.
   Adopted by all 7 pages: homepage, material-collection, material-page,
   pattern-collection, pattern-page, sample-collection, sample-page.

   2026-07-19 — палитра/шрифты/радиус переведены на редизайн v2 (см.
   public/assets/redesign-v2.css, ferrara-design-spec-v2.md). Значения
   токенов заменены НА МЕСТЕ (имена переменных не менялись) — так изменение
   подхватывают все файлы, уже использующие var(--accent)/var(--ink)/...,
   без правки каждого из них по отдельности. --dark удалён — был объявлен,
   но нигде не использовался (проверено grep'ом по всему public/). --purple
   был удалён тогда же по той же причине, но ошибочно — восстановлен
   2026-08-25, см. комментарий у объявления ниже. --clay/--green/--teal
   (теги «допустимо/недопустимо» у зон
   применения, акцент типа «материал» в фильтрах) намеренно НЕ тронуты —
   для них ещё нет решения по цвету в новом дизайне, отдельная задача,
   когда дойдём до этих конкретных страниц.
   ══════════════════════════════════════════════════════════════════ */

:root{
  /* 2026-08-26 — сайт теперь ДЕЙСТВИТЕЛЬНО поддерживает тёмную тему (Фаза 3
     палитра ниже), поэтому color-scheme честно объявляет обе схемы — не
     "only light", как было, пока тёмных значений не существовало. Реальная
     обработка prefers-color-scheme (см. блок ниже) сама по себе снимает
     исходную причину проблемы (Chrome Auto Dark Theme на Android/Samsung
     Internet принудительно перекрашивал страницу): его документация прямо
     называет критерием форсированного затемнения именно ОТСУТСТВИЕ
     обработки тёмной темы автором, а не факт "only light" самого по себе. */
  color-scheme: light dark;
  /* neutrals */
  --bg-card:#FAF7F0;   /* background for cards/plaques/insets, NOT the page — чуть светлее bg-main, как и было */
  --bg-main:#F6F1E8;   /* ivory — фон страницы */
  --bg-3:#E9DFCB;      /* travertine — заглушка фото/иконных ячеек */
  --bg-4:#E5DCC7;       /* активный фильтр-пилл "all" (sample-page.css) — тёплый нейтральный тон в цвет остальных */
  --line:#d8cbb0;
  --line-2:#c9b998;
  --line-3:#8C8570;     /* = olive из редизайна — граница карточки/поля при hover */
  --ink:#2E2420;        /* espresso — основной текст */
  --ink-dim:#5b5346;
  --ink-faint:#8C8570;  /* = olive — второстепенный/приглушённый текст */
  /* accent — terracotta (было brass/gold) */
  --accent:#B5622E;
  --accent-deep:#8f4c22;
  --accent-bg:rgba(181,98,46,.11);
  --accent-dim:#DAC5A7;
  /* semantic — catalog pages only (пока без изменений, см. комментарий выше) */
  --clay:#A0614A;--clay-bg:rgba(160,97,74,.10);
  --green:#4A8A5A;--green-bg:rgba(74,138,90,.10);
  --teal:#4A8A7A;
  /* --purple — восстановлен 2026-08-25: ошибочно удалён 2026-07-19 как
     "неиспользуемый" (см. комментарий в шапке файла), на деле
     .lvl-badge.pattern (ниже) на него ссылался всё это время и тихо
     съезжал на цвет по умолчанию. Четвёртый, визуально отличный от
     --teal/--clay/--accent цвет для бейджа "pattern". */
  --purple:#8A6ABF;--purple-bg:rgba(138,106,191,.08);
  /* shadows & overlays — Фаза 2 аудита хардкод-цветов (2026-08-25). Alpha
     везде через color-mix(in srgb, var(--X) N%, transparent) вместо
     отдельного токена на каждую прозрачность — этот приём уже был в
     archive/dev-tools/redesign-v2.css, переиспользуем тот же паттерн. */
  --shadow-black:#000;                 /* нейтральные чёрные тени (было голое rgba(0,0,0,x) в ~20 местах) */
  /* --shadow-card(-hover) — тень карточек (.card/.svc-card/.why-card/
     .svc-quote/.mdf-card), буквально повторялась 8+ раз. Раньше был
     отдельный литерал rgba(46,36,26,x) — на 6/255 темнее --ink в синем
     канале, случайный дрейф (извлекались в разное время, никто не сверял).
     2026-08-26: выражена через var(--ink) + color-mix() — тень теперь
     явно "--ink с прозрачностью", а не собственный почти-дублирующий цвет. */
  --shadow-card:color-mix(in srgb, var(--ink) 28%, transparent);
  --shadow-card-hover:color-mix(in srgb, var(--ink) 38%, transparent);
  --overlay-dark:#140f0a;              /* тёплая тёмная подложка под текстом на фото (было 3-4 чуть разных rgba — случайный дрейф одного и того же замысла) */
  --overlay-darkest:#0a0805;           /* тот же смысл, но заметно темнее — полноэкранный лайтбокс (detail-page.css) */
  /* surfaces & inverse text */
  --surface:#ffffff;                   /* непрозрачные UI-поверхности (.bc/.blk-title/.tabbar/.hspop-close и т.п.) — НЕ то же самое, что --bg-card/--bg-main (оба тёплые айвори, не белые) */
  --text-on-accent:#ffffff;            /* белый текст/маркеры на акцентном фоне или поверх фото — намеренно отдельный от --surface токен: остаётся белым и в тёмной теме (см. Фазу 3 ниже), когда --surface уже тёмный */
  --text-inverse:#F0E8DA;              /* тёплый айвори текст поверх тёмных фото-подложек (.card-count-pill, .card--overlay .card-desc и т.п.), а также текст/бордер на постоянно-тёмной стеклянной навигации (.nav и её дропдауны/мобильное меню) — не зависит от темы сайта. Поглотил бывший --glass-nav-text (2026-08-26): оба значения отличались всего на ~13/255, а роль у обоих одна и та же — светлый текст на тёмном, который не должен меняться вместе с темой сайта. */
  /* misc — по одному новому цвету на каждый случай, ранее не тронуто никакой семьёй */
  --progress-fill-done:#9a9a9a;        /* нейтрально-серый сегмент "просмотрено" в прогресс-баре карточки */
  --trade-gold:#E8B888;                /* ссылка на тёмном Trade-баннере (homepage.css) — не связан с --accent */
  --trade-gold-border:rgba(232,184,136,.5);
  --badge-sample-border:rgba(176,141,94,.28); /* бордер .lvl-badge.sample — приглушённая латунь, не совпадает ни с одним другим токеном (не то же, что --accent) */
  --paper-tag-bg:#F3EBD8;              /* бумажный ярлык на бечёвке (.pc-tag, product-paint-bucket.css) — близко к --bg-3/--bg-4, но не совпадает; токен-кандидат, унификация — по визуальному сравнению на странице Design Tokens */
  /* TODO: --clay/--green/--teal — старая семантическая палитра (теги
     «допустимо/недопустимо» у зон применения, акцент «материал» в
     фильтрах), не пересмотрена под редизайн v2. Проверить ближе к финалу
     интеграции — актуальны ли ещё эти конкретные UI-места и нужна ли им
     новая пара цветов, или их можно убрать/заменить. */
  /* type + radius — три роли, не путать: --r (контент — карточки/теги/
     фото, почти острый угол, суть редизайна v2; по прямому запросу теперь
     им же оформлены и .svc-card/.why-card/.svc-quote), 99px впрямую в
     местах, где нужна таблетка (контролы — поиск/фильтры/тумблер/кнопки,
     полное скругление — не токен, а просто "100%" в CSS-смысле, отдельный
     токен для этого не нужен), --r-panel (плавающие панели — .ndrop и
     мобильный .hspop-box (bottom sheet) — раньше было захардкожено 12px в
     каждом месте по отдельности). */
  --display:Georgia,'Times New Roman',serif;
  --body:Georgia,'Times New Roman',serif;
  --mono:'Trebuchet MS',sans-serif;
  --r:2px;
  --r-panel:12px;
}

/* ── ТЁМНАЯ ТЕМА — Фаза 3 (подбор 2026-08-26, утверждена пользователем).
   ВКЛЮЧАЕТСЯ ТОЛЬКО АВТОМАТИЧЕСКИ, по системной теме устройства
   (prefers-color-scheme) — по решению пользователя ручного переключателя
   в интерфейсе сайта НЕТ и не будет (Фаза 4 отменена), поэтому весь
   механизм — один этот media-блок, без атрибутов/JS/localStorage.

   Подобрано вручную, не алгоритмической инверсией: тёплая палитра (то же
   семейство оттенков — терракота/эспрессо/травертин), а не нейтральный
   серый/синий "техно-дарк". Приоритеты:
     - Фон страницы — тёплый почти-чёрный (не чистый #000), поверхности
       "приподнятые" (bg-card/bg-3/bg-4) — светлее фона страницы, тот же
       порядок, что в светлой теме (там bg-card тоже светлее bg-main).
     - Акцент/семантика — ЯРЧЕ, не темнее: та же терракота на тёмном фоне
       обязана быть светлее себя же на светлом, иначе потеряет "вес" и
       контраст. hover у --accent-deep по той же причине ЗЕРКАЛЬНО
       развернут — светлее базового, не темнее (см. комментарий у токена
       на странице Design Tokens).
     - --overlay-dark/-darkest, --text-on-accent, --text-inverse,
       --shadow-black, --trade-gold(-border), --paper-tag-bg — НЕ меняются:
       их роль (тёмный оверлей на фото / гарантированно светлый текст на
       акценте или фото / физический цвет бумаги на иллюстрации) не
       зависит от темы сайта в принципе.
     - --shadow-card(-hover) НЕ может остаться "var(--ink) + color-mix()",
       как в светлой теме — --ink сам стал светлым. Здесь это отдельный,
       более плотный чёрный литерал (тени на тёмном фоне держат меньше
       естественного контраста, нужна бОльшая непрозрачность).

   ИЗВЕСТНЫЙ ОТКРЫТЫЙ ВОПРОС (не решается этим блоком): пара компонентов
   использует var(--ink) как ФОН декоративного "всегда тёмного" элемента,
   а не как цвет текста — например стеклянная .nav при скролле (.trade-banner,
   homepage.css, уже точечно исправлен). Раз --ink теперь становится светлым
   в этой теме, такие места нужно аудировать и точечно поправлять по мере
   обнаружения — простой подменой токена их не починить.

   При правке палитры — значения меняются только здесь, второго места нет. */
@media(prefers-color-scheme:dark){
  :root{
    --bg-card:#251E19;
    --bg-main:#1A1512;
    --bg-3:#362D24;
    --bg-4:#302720;
    --line:#453A2E;
    --line-2:#5A4B3A;
    --line-3:#A79C82;
    --ink:#E8DFCE;
    --ink-dim:#B5A990;
    --ink-faint:#A79C82;
    --accent:#D9824A;
    --accent-deep:#E8A06E;
    --accent-bg:rgba(217,130,74,.16);
    --accent-dim:#8C6A52;
    --clay:#C98567;--clay-bg:rgba(201,133,103,.14);
    --green:#6FB57E;--green-bg:rgba(111,181,126,.14);
    --teal:#6FB5A0;
    --purple:#A98FD1;--purple-bg:rgba(169,143,209,.12);
    --shadow-black:#000;
    --shadow-card:rgba(0,0,0,.45);
    --shadow-card-hover:rgba(0,0,0,.6);
    --overlay-dark:#140f0a;
    --overlay-darkest:#0a0805;
    --surface:#1F1913;
    --text-on-accent:#ffffff;
    --text-inverse:#F0E8DA;
    --progress-fill-done:#767676;
    --trade-gold:#E8B888;
    --trade-gold-border:rgba(232,184,136,.5);
    --badge-sample-border:rgba(196,163,115,.35);
    --paper-tag-bg:#F3EBD8;
  }
}

/* ── BASE RESET ── */
*{box-sizing:border-box;margin:0;padding:0}
/* overflow-x:hidden на html/body (2026-08-24) — источник постоянного
   нижнего горизонтального скролла на всём сайте найден: .rail-bleed
   (полноширинный "breakout" рельсы образцов/продуктов, ниже в этом файле)
   использует width:100vw — эта единица считает ширину вьюпорта ВКЛЮЧАЯ
   вертикальный скроллбар, а реальная видимая ширина страницы (100%) на
   Windows/Chrome УЖЕ уже на ширину скроллбара (~15-17px) — из-за этого
   .rail-bleed оказывался на несколько px шире реального контента, и
   страница целиком получала свой собственный горизонтальный скролл
   (классический баг 100vw). Правильное исправление — не подгонять расчёт
   высоты первого экрана под этот скролл, а убрать сам скролл: обрезаем
   те лишние px здесь, а не считаем их. Внутренние горизонтальные карусели
   (.rail, .rail-tabs, .tabbar — overflow-x:auto/clip на СВОИХ контейнерах)
   не затронуты — это независимый, вложенный scroll-контекст. */
html{scroll-behavior:smooth;overflow-x:hidden}
body{background:var(--bg-main);color:var(--ink);font-family:var(--body);-webkit-font-smoothing:antialiased;font-size:15px;line-height:1.6;overflow-x:hidden}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer;background:none;border:none}
img,svg{display:block;max-width:100%}

/* ── NAV ── */
/* Шапка (2026-08-24, по прямому запросу) — по умолчанию БЕЗ фона/бордера
   (прозрачная, как в референсе FerraraHome_ver1); "стеклянная" плашка
   (блюр+тонкий бордер+тень) появляется только после скролла — класс
   .scrolled переключает initNavScroll() в site.js (общий для всех страниц,
   не только главной). border/box-shadow — с прозрачным стартовым значением
   (не display:none), чтобы transition реально анимировался, а не дёргался. */
/* Плашка при скролле — тёмная плавающая панель (2026-08-24, по прямому
   запросу; заменяет прежнюю светлую rgba(255,255,255,.72)), уточнение того
   же дня — переведена с CSS-transition/class-toggle на переменную --p (0..1),
   которую каждый кадр считает JS (initNavScroll, site.js) из РЕАЛЬНОГО
   положения скролла: p = scrollY / (25% высоты окна), т.е. полное
   превращение плашки происходит ровно за 25% высоты окна прокрутки, а не
   за фиксированную длительность .3s — "не как отдельная анимация от
   таймера, а от положения прокрутки", по прямому запросу (иначе при
   прокрутке через Lenis, который сам плавно ведёт scrollY, транзишн поверх
   транзишна давал двойное сглаживание/подтормаживание). Параметры при p=1
   (полностью проскроллено) — история значений фона по датам правок:
   60%→50%→35%→(эта правка)65% прозрачности:
     фон:    rgba(46,36,32,.35) — var(--ink) на 35% непрозрачности
             (= 65% прозрачности, по прямому запросу — было 65%/35%)
     блюр:   backdrop-filter:blur(15px)
     бордер: 1px solid rgba(246,239,227,.16) — минимальная толщина (1px)
     отступ от краёв: top:6px, left/right:12px (правка после этой —
             асимметрия намеренная, не опечатка) — плашка не впритык к
             краям; border-radius:14px
     padding: 40px (было 28px) — логотип/Sign In сдвигаются к центру
   При p=0 — все эти значения по calc() сами сходятся к исходным (fon
   прозрачный, blur 0, отступы 0, radius 0, padding 28px) — отдельного
   "непроскрolленного" состояния в CSS больше нет, это просто p=0. */
.nav{--p:0;background:color-mix(in srgb, var(--ink) calc(var(--p) * 35%), transparent);padding:0 calc(28px + 12px * var(--p));height:64px;display:flex;align-items:center;justify-content:space-between;position:fixed;top:calc(6px * var(--p));left:calc(12px * var(--p));right:calc(12px * var(--p));z-index:1000;border:1px solid color-mix(in srgb, var(--text-inverse) calc(var(--p) * 16%), transparent);border-radius:calc(14px * var(--p));box-shadow:0 calc(6px * var(--p)) calc(16px * var(--p)) color-mix(in srgb, var(--overlay-dark) calc(var(--p) * 35%), transparent);backdrop-filter:blur(calc(var(--p) * 15px));-webkit-backdrop-filter:blur(calc(var(--p) * 15px))}
/* Текст шапки при скролле — тёмный (эксперимент, по прямому запросу
   "попробуем как будет выглядеть, если текст тоже тёмным") — раньше был
   светлый (#f6efe3); теперь просто наследует дефолтный var(--ink)
   (.nav-link выше), отдельного override для scrolled-состояния не нужно.
   Бордеры (brand-mark/nav-signin) остаются светлыми, не тронуты — вопрос
   был только про текст. */
.nav.scrolled .brand-mark{border-color:var(--text-inverse)}
.nav.scrolled .nav-signin{border-color:color-mix(in srgb, var(--text-inverse) 50%, transparent)}
.page-body{padding-top:64px}
.brand{display:flex;align-items:center;gap:0px;font-family:var(--display);font-size:15px;color:var(--ink);letter-spacing:-.01em;font-weight:700;transition:color .3s ease}
.brand-mark{width:16px;height:16px;border-radius:50%;border:2px solid var(--ink);flex:0 0 auto}
.brand-logo{display:block;max-width:180px;width:auto;height:auto} /* max-height задаётся инлайн из site_settings.header_logo_height_px (adm-menu.php) */
.brand-logo-mobile-only{display:none} /* показывается только ≤980px и только если задан отдельный мобильный логотип (header_brand_media_id_mobile) — см. media-блок ниже */
/* Блюр+прозрачность логотипа в меню при скролле — только десктоп, только
   главная (--brand-blur/--brand-opacity выставляет initNavScroll() в
   site.js, ТОЛЬКО когда есть window.heroNavP, т.е. на главной; на любой
   другой странице переменные не заданы — var()-фолбэк 0px/1, логотип как
   обычно) — полностью скрыт в начале (p=0: opacity:0, блюр 8px), проявляется
   целиком к концу анимации меню (p=1: opacity:1, блюр 0px), см.
   --hero-brand-blur-max в animation-tokens.css. */
@media(min-width:981px){
  .brand-logo{filter:blur(var(--brand-blur,0px));opacity:var(--brand-opacity,1);transition:filter var(--hero-cover-smoothing,.1s) ease,opacity var(--hero-cover-smoothing,.1s) ease}
}
/* Центрирование по ВСЕЙ ширине шапки, независимо от ширины логотипа/правой
   части (2026-08-24, по прямому запросу) — position:absolute относительно
   .nav (ближайший позиционированный предок — сама .nav, position:fixed;
   .nav-l между ними без своего position, не мешает). */
.nav-mid{display:flex;gap:26px;align-items:center;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}
.nav-link{font-size:18px;color:var(--ink);display:flex;align-items:center;gap:4px;transition:.15s;position:relative;font-weight:700}
.nav-link:hover{color:var(--accent)}
.nav-link .car{font-size:22px;opacity:.5}
/* Плашка выпадающего меню — всегда на тёмной стеклянной плашке (те же
   фон/блюр/бордер/тень, что у .nav в проскролленном состоянии, p=1),
   независимо от текущей прокрутки страницы (по прямому запросу).
   position:fixed, НЕ вложена в .nav (2026-08-25, по прямому запросу —
   backdrop-filter внутри элемента, который сам тоже применяет
   backdrop-filter, у Chromium молча не рендерится; .nav задаёт его
   всегда, даже blur(0px) при p=0) — та же схема, что уже была у .nmobile
   (тоже не потомок .nav). Разметка печатается в header.php сразу после
   </nav>, top/left считает initNavDropdowns() в site.js при наведении на
   .nav-item (getBoundingClientRect триггера) и пишет их в --ndrop-x/-y. */
.ndrop{position:fixed;top:var(--ndrop-y,0);left:var(--ndrop-x,0);transform:translate(-50%,-8px);z-index:1001;
  background:color-mix(in srgb, var(--ink) 35%, transparent);backdrop-filter:blur(15px);-webkit-backdrop-filter:blur(15px);
  border:1px solid color-mix(in srgb, var(--text-inverse) 16%, transparent);border-radius:var(--r-panel);padding:8px;min-width:180px;
  box-shadow:0 6px 16px color-mix(in srgb, var(--overlay-dark) 35%, transparent);opacity:0;visibility:hidden;transition:.18s;display:flex;flex-direction:column;gap:1px;pointer-events:none}
.ndrop.open{opacity:1;visibility:visible;transform:translate(-50%,0);pointer-events:auto}
/* Цвет текста и ховер подпунктов — тот же приём, что у основного меню
   (.nav-link/.nav-link:hover): только смена цвета на accent, без подложки. */
.ndrop a{color:var(--ink);font-size:13px;padding:8px 10px;border-radius:6px;transition:.13s;font-weight:700}
.ndrop a:hover{color:var(--accent)}
.ndrop a.current{color:var(--accent)}
.nav-item{position:relative}
.nav-l{display:flex;align-items:center}
.nav-r{display:flex;gap:14px;align-items:center}
.nicon{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--ink);font-size:14px;transition:.15s;border:1px solid var(--line-2)}
.nicon:hover{background:var(--bg-main)}
/* Вэнити-номер текстом в шапке (2026-08-15) — только когда phone_vanity
   заполнен (см. header.php); иконка расширяется в пилюлю вместо круга.
   На мобильном .nicon целиком скрыт правилом ниже (@980px, п.1 2026-08-18) —
   там нет ни круглой, ни текстовой версии, кнопка звонка не показывается вовсе. */
.nicon.nicon-vanity{width:auto;border-radius:99px;padding:0 16px;gap:7px;font-weight:600;font-size:13px;white-space:nowrap}
.nquote{background:var(--accent);color:var(--text-on-accent);height:34px;padding:0 20px;border-radius:99px;font-size:13px;font-weight:500;transition:.18s;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap}
.nquote:hover{background:var(--accent-deep)}
/* "Sign In" пункт меню справа (2026-08-24, по прямому запросу) — ведёт в
   админку, тот же визуальный язык (пилюля-обводка), что и остальные
   круглые/пилюльные элементы .nav-r выше. */
.nav-signin{height:34px;padding:0 18px;border-radius:99px;border:1px solid var(--line-2);font-size:13px;font-weight:600;color:var(--ink);display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;transition:.15s}
.nav-signin:hover{border-color:var(--ink);background:var(--bg-main)}

/* ── MOBILE NAV (гамбургер + выпадающая панель) — виден только ≤980px, см.
   @media ниже; сама .nmobile всегда в разметке, но схлопнута (max-height:0),
   поэтому на десктопе не видна независимо от display, даже без media-guard. */
.nburger{display:none;width:34px;height:34px;border-radius:50%;align-items:center;justify-content:center;border:1px solid var(--line-2);flex-direction:column;gap:4px;flex:0 0 auto}
.nburger span{display:block;width:15px;height:2px;background:var(--ink);border-radius:1px;transition:transform .2s ease,opacity .2s ease}
.nburger.open span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.nburger.open span:nth-child(2){opacity:0}
.nburger.open span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}
/* Фон/блюр/бордер — то же самое "состояние плашки", что у .nav при
   p=1 (полностью проскроллено, см. .nav{--p:0;...} выше) — тёмная
   полупрозрачная плашка+блюр, не сплошной белый, как было. Слайд вниз —
   translateY(-10px)→0 поверх уже существующего max-height-раскрытия;
   0.3s — заметно глазу (было .16s/.14s, слишком быстро/незаметно).
   visibility НЕ входит в список transition-property выше по неймингу
   свойств — сама по себе она меняется МГНОВЕННО, а не плавно. При
   закрытии это скрывало панель раньше, чем успевали доиграть opacity/
   max-height — animation закрытия была не видна вообще (по факту играла
   "под капотом", но сам блок уже visibility:hidden). Фикс — стандартный
   приём: transition-delay для visibility РАЗНЫЙ у открытия/закрытия
   (delay берётся из ПОСЛЕ-состояния): здесь (закрытое, "после" при
   закрытии) — задержка .3s, значит остаётся visible всю анимацию
   исчезновения; у .open (открытое, "после" при открытии) — задержка 0,
   показывается сразу. */
.nmobile{position:fixed;top:64px;left:0;right:0;z-index:999;
  background:color-mix(in srgb, var(--ink) 35%, transparent);backdrop-filter:blur(15px);-webkit-backdrop-filter:blur(15px);
  border-bottom:1px solid color-mix(in srgb, var(--text-inverse) 16%, transparent);box-shadow:0 6px 16px color-mix(in srgb, var(--overlay-dark) 35%, transparent);
  display:flex;flex-direction:column;padding:8px;gap:2px;
  max-height:0;overflow:hidden;opacity:0;visibility:hidden;transform:translateY(-10px);
  transition:max-height .3s ease,opacity .3s ease,transform .3s ease,visibility 0s .3s}
/* 420 → 600: пункты с подпунктами (см. .nmobile-sub ниже) при раскрытии
   аккордеоном добавляют высоты панели сверх старого фиксированного
   потолка — 600px даёт запас, overflow-y:auto подстрахует, если пунктов
   станет ещё больше (редактируется в admin/adm-menu.php, список не
   ограничен по факту). */
.nmobile.open{max-height:600px;opacity:1;visibility:visible;overflow-y:auto;transform:translateY(0);
  transition:max-height .3s ease,opacity .3s ease,transform .3s ease,visibility 0s}
/* Текст светлый — тот же приём, что у .nav.scrolled .nav-link (shared.css
   выше) — фон плашки теперь тёмный, дефолтный тёмный var(--ink) на ней
   был бы нечитаем. */
.nmobile-link{padding:0 14px;border-radius:8px;font-size:14.5px;font-weight:700;color:var(--text-inverse);min-height:46px;display:flex;align-items:center}
.nmobile-link:hover,.nmobile-link:active{background:color-mix(in srgb, var(--text-inverse) 10%, transparent)}
.nmobile-link.current{color:var(--accent-dim)}

/* ── Пункт меню с подпунктами на мобильном — аккордеон (2026-08-11,
   заменяет прежний плоский список без заголовка-родителя). Если у пункта
   есть СВОЯ ссылка (напр. будущий "Blog" на /blog/) — текст и стрелка
   раздельные тап-таргеты (.nmobile-row: <a> ведёт по ссылке, <button>
   разворачивает список — оба варианта не могут быть одной зоной тапа, см.
   admin/adm-menu.php::Справка). Без своей ссылки (как сейчас "Catalog") —
   весь пункт целиком одна кнопка-тумблер (.nmobile-parent). ── */
.nmobile-row{display:flex;align-items:stretch;gap:0}
/* Текст — по ширине своего содержимого (не flex:1), кнопка-тумблер —
   забирает всё оставшееся пустое место до правого края (было
   flex:0 0 46px — маленький квадрат-таргет только вокруг стрелки), сама
   стрелка внутри прижата к правому краю через justify-content:flex-end +
   тот же 14px паддинг, что и у .nmobile-link, — визуально стрелка стоит
   на том же месте, что и раньше, просто зона тапа кнопки шире. */
.nmobile-row .nmobile-link-text{flex:0 1 auto;min-width:0}
.nmobile-toggle{flex:1 1 auto;display:flex;align-items:center;justify-content:flex-end;padding-right:14px;border-radius:8px}
.nmobile-toggle:hover{background:color-mix(in srgb, var(--text-inverse) 10%, transparent)}
.nmobile-parent{width:100%;justify-content:space-between}
.nmobile-toggle .car,.nmobile-parent .car{font-size:22px;color:color-mix(in srgb, var(--text-inverse) 60%, transparent);transition:transform .15s}
.nmobile-toggle[aria-expanded="true"] .car,.nmobile-parent[aria-expanded="true"] .car{transform:rotate(180deg)}
.nmobile-sub{max-height:0;overflow:hidden;transition:max-height .2s ease;padding-left:14px}
.nmobile-sub.open{max-height:400px}
.nmobile-sublink{display:flex;align-items:center;min-height:40px;padding:0 14px;font-size:13.5px;font-weight:700;color:color-mix(in srgb, var(--text-inverse) 75%, transparent);border-radius:8px}
.nmobile-sublink:hover,.nmobile-sublink:active{background:color-mix(in srgb, var(--text-inverse) 10%, transparent);color:var(--text-inverse)}
.nmobile-sublink.current{color:var(--accent-dim)}

/* .nav padding на узких экранах — раньше было переопределено только в
   homepage.css (20px), на остальных страницах (products/samples/projects/
   master-samples и детальных) .nav оставался с десктопным padding:0 28px —
   тот же паттерн дублирования вместо общего правила, что уже чинили для
   .tabbar/.bc. Единое место — здесь, применяется везде. */
@media(max-width:560px){
  .nav{padding:0 20px}
  .nav-l{max-width:100px}
  /* Вэнити-пилюля + Free Quote в узкой шапке легко не помещаются в одну
     строку текстом (по прямому запросу — лучше мельче шрифт, чем перенос
     на 2 строки). */
  .nicon.nicon-vanity{font-size:11px;padding:0 11px;gap:5px}
  .nquote{font-size:11px;padding:0 14px}
}

/* ── BREADCRUMB (.bc) — общий компонент, использовался в 5 разных страницах
   и был везде идентично продублирован (тот же паттерн, что чинили для
   .tabbar) — теперь одно место. .bc-mobile ("← Back to X") печатается
   вместе с .bc из renderBreadcrumb() (header.php), скрыт на десктопе. */
.bc{height:26px;background:var(--surface);padding:0 28px;display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ink-faint);flex-wrap:nowrap;overflow:hidden;white-space:nowrap}
.bc a{color:var(--ink-faint)}
.bc a:hover{color:var(--accent);text-decoration:none}
.bc .sep{opacity:.35}
.bc-mobile{display:none}

@media(max-width:560px){
  .bc{display:none}
  .bc-mobile{display:flex;align-items:center;height:26px;padding:0 10vw;font-family:var(--mono);font-size:13px;color:var(--ink-dim)}
  .bc-mobile a{color:var(--ink-dim)}
}

/* ── PAGE CONTAINER — базовое центрирование+отступы контента, одинаковое
   на всех страницах (отличается только padding-bottom, который каждая
   страница переопределяет в своём page-specific CSS — переопределение
   безопасно: тот файл грузится ПОСЛЕ shared.css). Раньше .page нигде не
   было общим — страницы без собственного $pageStylesheet (напр. about.php)
   оставались вообще без центрирования/отступов (контент прижимался к
   левому краю на всю ширину экрана). */
.page{max-width:1280px;margin:0 auto;padding:0 40px}
@media(max-width:560px){.page{padding-left:10vw;padding-right:10vw}}

/* Сообщение "не найдено" на детальных страницах (material.php/sample.php/
   project.php/color-system.php) — см. renderNotFoundMessage() в
   includes/not_found_helper.php (SEO-аудит 2026-08-14, находка №9). */
.not-found-page{padding-top:100px;padding-bottom:100px}
.not-found-page .cta-link{display:inline-block;margin-top:20px}

/* Обёртка финального CTA (footer.php, .fcta ниже) — центрирование как у
   .page, но отдельным классом: на главной .page на мобильном переопределён
   под свою ширину карточек (--card-side-pad, см. homepage.css), а .fcta —
   не карточка, там сознательно сохранена своя мобильная ширина 10vw (см.
   homepage.css). Общий .page тут не подходит именно из-за этого нюанса. */
.fcta-wrap{max-width:1280px;margin:0 auto;padding:0 40px}
@media(max-width:560px){.fcta-wrap{padding-left:10vw;padding-right:10vw}}

/* Единый вертикальный ритм секции — .b1-.b5 и .b-areas (per-page "блок"
   внутри .page, разметка везде одинаковая: #sec-bN > .blk-title + div.page
   > section.bN — единственный <main> на страницу теперь выше, в
   header.php/.page-body, см. shared.css:71). Раньше это правило
   дублировалось только в homepage.css
   (только Block 1-5 главной), на остальных страницах классы .b1/.b2 в
   разметке уже были, но без стилей — теперь один источник на весь сайт.
   .b-areas (Service Areas, footer.php) добавлен по той же логике —
   тег/классы вокруг снаружи (.page/section.bX/.section-head/
   .section-title/.section-subtitle) везде одинаковые; внутри .section-head
   у Service Areas остаётся свой двухколоночный .areas-inner (заголовок
   слева, пилюли справа) — это единственное, что сознательно не
   унифицировано, остальное приведено к стандарту (запрос 2026-08-13). */
.b1,.b2,.b3,.b4,.b5,.b-areas,.b-how {padding:15px 0}

/* Внешняя обёртка нумерованной секции страницы (#sec-bN, включая Service
   Areas в footer.php, #sec-areas) — только фон, чередующийся с белым
   между секциями. Раньше было два класса с одинаковым единственным
   свойством (.b1-.b5-content в homepage.css/detail-page.css и
   .areas-content, задублированный в 5 разных файлах) — разные имена, но
   роль одна и та же: просто маркер "это секция страницы". Одно правило,
   один класс. */
.section-content{background:var(--bg-main)}

/* ── BLOCK TITLE BAR (section divider — see Design System v2 example) ── */
.blk-title{background:var(--surface);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.blk-title-inner{max-width:1280px;margin:0 auto;padding:13px 40px}
.blk-title-inner .sec-eyebrow{margin-bottom:0}

/* ── TAB BAR (сегментированная навигация по разделам страницы) — общий
   компонент, используется на главной и на всех детальных страницах
   (material/sample/master-sample/project). Раньше был независимо
   продублирован в homepage.css и detail-page.css — правка одного файла не
   затрагивала другой. Позиционирование (sticky/border-side) остаётся
   page-specific оверрайдом в своём CSS, т.к. на главной .tabbar сам
   sticky, а на детальных sticky даёт обёртка .toolbar-wrap. */
.tabbar{height:44px;background:var(--surface);display:flex;overflow-x:auto;scrollbar-width:none}
.tabbar::-webkit-scrollbar{display:none}
.tabseg{flex:1 1 0;display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer;font-size:12.5px;font-weight:600;color:var(--ink-faint);transition:.18s;border-right:1px solid var(--line);white-space:nowrap;padding:0 6px}
.tabseg:last-child{border-right:none}
.tabseg:hover{color:var(--ink);background:var(--bg-main)}
.tabseg.on{color:var(--ink);background:var(--accent-dim)}

/* Мобильный вид (≤560px) — .tabbar скрыт целиком (дублировал ту же роль
   "активный раздел"), вместо него .blk-title каждого раздела становится
   sticky с ОДНИМ И ТЕМ ЖЕ top (высота nav+хлебных крошек, page-specific,
   задаётся в detail-page.css/homepage.css) — все .blk-title видны, ничего
   не скрываем. При одинаковом top у нескольких sticky-соседей браузер сам
   «выталкивает» предыдущий следующим по мере скролла (нативное поведение
   position:sticky, без JS/z-index-трюков): текущий раздел всегда прилипает
   ровно под nav/крошками, следующий, поравнявшись с ним, сдвигает его
   наверх и занимает его место. Внешний вид — как активный .tabseg (28px,
   центрированный жирный текст).
   ВАЖНО: этот блок должен идти ПОСЛЕ канонических .tabbar/.tabseg выше —
   иначе при равной специфичности `display:flex` из канонического правила
   побеждает `display:none` здесь независимо от @media (тот самый баг с
   порядком каскада, который уже несколько раз ловили в этом проекте). */
@media(max-width:560px){
  .tabbar{display:none}
  /* z-index обязателен: без него sticky .blk-title и любой position:relative
     элемент контента страницы (напр. .svc-quote) стоят в одном слое
     "positioned, z-index:auto" и порядок отрисовки решает DOM-порядок — тот
     контент оказывается позже в DOM и рисуется поверх закреплённого бара. */
  .blk-title{position:sticky;z-index:5;border-top:none;height:28px;display:flex}
  .blk-title-inner{padding:0;display:flex;align-items:center;justify-content:center;width:100%}
  .blk-title-inner .sec-eyebrow{margin-bottom:0;font-size:11.5px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--ink);background:var(--accent-dim);width:100%;height:100%;display:flex;align-items:center;justify-content:center}
}

/* ── FILTER GROUP — сворачиваемый блок фильтров на страницах коллекций
   (products/samples/projects/master-samples). ОДИН и тот же .fgroup-toggle
   показывает/прячет .fgroup-body на ЛЮБОЙ ширине (запрос 2026-08-21) —
   раньше на десктопе был отдельный статичный текст "Filters" (.fgroup-label,
   не кликабельный), а сворачиваемой кнопкой .fgroup-toggle был только на
   мобильном — два разных элемента для одной и той же роли. Теперь это один
   элемент, только СТАРТОВОЕ состояние разное по ширине: десктоп по
   умолчанию развёрнут, мобильный (≤560px) по умолчанию свёрнут — задаётся
   чистым CSS ниже, без класса на .fgroup при первом рендере (сервер не
   знает ширину экрана посетителя). Клик — делегированный листенер в
   site.js — читает ТЕКУЩЕЕ отображаемое состояние (computed style, зависит
   от текущей ширины) и явно проставляет .is-open/.is-closed; специфичность
   этих правил (0,3,0) выше дефолтных (0,1,0) ниже, поэтому побеждают
   независимо от брейкпоинта — один обработчик работает корректно на любой
   ширине, а явный выбор пользователя переживает ресайз окна. */
.fgroup-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:8px 0;background:none;border:none;cursor:pointer;font-family:var(--body);font-weight:600;font-size:12.5px;color:var(--ink)}
.fgroup-car{font-size:22px;color:var(--ink-faint);transition:transform .15s ease;transform:rotate(180deg)}
.fgroup-body{display:block;padding-top:6px}
/* .fgroup-head — строка "Filters"-кнопка + поле поиска рядом, в одну
   строку (2026-08-21, по прямому запросу, samples/products) — раньше поиск
   лежал ВНУТРИ .fgroup-body и был скрыт, пока фильтры не раскрыты. Внутри
   этого враппера .fgroup-toggle теряет width:100% (иначе сам занял бы всю
   строку и вытолкнул поиск на следующую) — специфичность выше базового
   правила выше, побеждает без .fgroup-toggle!important. .search-wrap —
   flex:1 с min-width:0, чтобы честно делить строку с кнопкой на любой
   ширине (включая мобильный) и никогда не переносился — конкретный
   max-width/базовые стили инпута остаются в CSS каждой страницы, .search-wrap
   не общий компонент. */
.fgroup-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.fgroup-head .fgroup-toggle{width:auto;flex:0 0 auto}
.fgroup-head .search-wrap{flex:1 1 auto;min-width:0}
@media(max-width:560px){
  .fgroup-car{transform:none}
  .fgroup-body{display:none}
}
.fgroup.is-open .fgroup-car{transform:rotate(180deg)}
.fgroup.is-closed .fgroup-car{transform:none}
.fgroup.is-open .fgroup-body{display:block}
.fgroup.is-closed .fgroup-body{display:none}

/* ── SECTION HEADING KIT ── */
.sec{max-width:1280px;margin:0;padding:96px 40px}
.section-head{padding-bottom:0px}
/* display:block явно (2026-08-17) — раньше .sec-eyebrow был <span> ВСЕГДА
   внутри .blk-title-inner (padding уже создавал отступ, margin-bottom на
   инлайн-элементе никак не мешал), теперь используется и САМ ПО СЕБЕ прямо
   в .section-head (главная, редизайн) — margin-bottom на строчном элементе
   по спецификации CSS не влияет на раскладку вообще, без display:block
   заголовок H2 сразу под ним прижимался бы без отступа. */
.sec-eyebrow{display:block;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-bottom:14px}
.section-title{margin-bottom: 15px;  font-family:var(--display);font-size:clamp(22px,3.4vw,36px);font-weight:700;line-height:1.08;letter-spacing:-.01em;max-width:75ch}
.section-title em{font-style:normal;font-weight:700;color:var(--accent)}
.section-subtitle{color:var(--ink-dim);font-size:14.5px;line-height:1.7;max-width:75ch;margin-bottom:15px}
.sec-desc{color:var(--ink-dim);font-size:14.5px;max-width:36ch;line-height:1.7}
.sec-count{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-dim);background:var(--bg-card);border:1px solid var(--line-2);padding:5px 12px;border-radius:99px;margin-bottom:14px}
.sec-count b{color:var(--ink);font-weight:600}
.sec-viewall{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:var(--ink);border-bottom:1.5px solid var(--ink);transition:.15s}
.sec-viewall:hover{color:var(--accent);border-color:var(--accent)}
.sec-viewall.accent{color:var(--accent);border-color:var(--accent)}
.sec-viewall.accent:hover{color:var(--accent-deep);border-color:var(--accent-deep)}

/* "Покрепче" (2026-08-19, перенесено из homepage.css — по прямому запросу
   "ссылки sec-viewall accent которые Explore сделай как на главной") —
   ссылка "Explore …" получает вид кнопки вместо простого текста с
   подчёркиванием; залита акцентом всегда, не только на hover. color/border —
   !important, т.к. .sec-viewall.accent{color:var(--accent)} выше специфичнее
   одного класса .sec-viewall--strong и иначе победил бы. */
.sec-viewall--strong{background:var(--accent);color:var(--text-on-accent)!important;border:1px solid var(--accent-deep);border-radius:var(--r);padding:12px 26px;font-weight:600;text-decoration:none!important;box-shadow:0 4px 14px color-mix(in srgb, var(--accent) 32%, transparent)}
.sec-viewall--strong:hover{background:var(--accent-deep);border-color:var(--accent-deep);color:var(--text-on-accent)!important}
/* Обёртка ссылки "Explore …"/"Browse …" под секцией — раньше инлайн-стилем
   (text-align:left;margin-top:20px) прямо в разметке, вынесено в класс,
   чтобы её можно было переопределить в media query (см. #sec-b1 .sec-more
   в homepage.css, мобильная версия). */
.sec-more{text-align:left;padding-top:15px; padding-bottom: 15px;}

/* ── SERVICE AREAS ── */
/* Внешние отступы/центрирование теперь даёт .page (шире) + section.b-areas
   (сверху/снизу), как у bN — здесь остаётся только сама раскладка "заголовок
   слева / пилюли справа" (сознательно не приведена к одноколоночной bN). */
.areas-inner{display:grid;grid-template-columns:1fr 1.4fr;gap:60px;align-items:center}
.area-list{display:flex;flex-wrap:wrap;gap:10px}
.area-pill{padding:9px 18px;border:1px solid var(--line-2);background:var(--bg-card);border-radius:99px;font-size:12.5px;color:var(--ink-dim);display:inline-flex;align-items:center}
.area-pill-icon{width:var(--area-pill-icon-size);height:var(--area-pill-icon-size);object-fit:cover;border-radius:50%;margin-right:6px;flex:0 0 auto}

/* ── FOOTER CTA ── */
/* Центрирование/боковые отступы теперь у .fcta-wrap (обёртка, см. выше) —
   здесь остаётся только сам крупный центрированный вид. Вертикальный
   отступ 100px убран целиком по прямому запросу (2026-08-18, уточнение —
   именно этот, верхнеуровневый на .fcta, а не компактные #sec-fcta-*
   оверрайды в homepage.css) — секция и так получает отступ от соседей
   (.section-content/.b-areas и т.п.), собственный был лишним. */
.fcta{padding:0px;text-align:center}
.fcta h2{font-family:var(--display);font-size:clamp(30px,4.4vw,58px);font-weight:700;line-height:1.06;letter-spacing:-.01em;margin-bottom:18px}
.fcta h2 em{font-style:normal;font-weight:700;color:var(--accent)}
.fcta p{color:var(--ink-dim);font-size:15px;max-width:44ch;margin:0 auto 30px}
.fcta-row{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.fcta-main{background:var(--accent);color:var(--text-on-accent);padding:16px 30px;border-radius:99px;font-size:14px;letter-spacing:.02em;transition:.18s;display:inline-block}
.fcta-main:hover{background:var(--accent-deep)}
.fcta-sub{border:1px solid var(--line-2);padding:16px 30px;border-radius:99px;font-size:14px;transition:.18s;display:inline-block}
.fcta-sub:hover{border-color:var(--ink)}

/* Вэнити-номер под кнопками FCTA (2026-08-15, по прямой просьбе владельца —
   номер куплен специально ради этого вида, раньше нигде не был виден текстом
   на видном месте) — печатается, только если phone_vanity заполнен. */
.fcta-phone{margin-top:18px;font-size:14px;color:var(--ink-dim)}
.fcta-phone a{color:var(--ink);font-weight:600;text-decoration:underline;text-decoration-color:var(--line-2);text-underline-offset:3px}
.fcta-phone a:hover{color:var(--accent);text-decoration-color:var(--accent)}

/* Trust-бейджи (2026-08-14, tz-license-insurance-scaffold.md) — независимые
   строки "✓ Licensed"/"✓ Insured"/"✓ N+ Years Experience" под кнопками FCTA,
   каждая условна (renderFctaSection() в fcta.php печатает только заполненные). */
.trust-badges{display:flex;gap:10px 22px;justify-content:center;flex-wrap:wrap;margin-top:22px;font-size:13px;color:var(--ink-dim)}
.trust-badges span{display:inline-flex;align-items:center;gap:5px}

/* ── FOOTER ── */
.foot{background:var(--bg-main);color:var(--ink);margin-top: 30px; padding:40px 40px 40px;border-top:1px solid var(--line)}
.foot-inner{max-width:1280px;margin:0 auto}
.foot-top{display:grid;grid-template-columns:1.4fr repeat(var(--foot-col-count,4),1fr);gap:40px;margin-bottom:60px} /* --foot-col-count печатается из footer.php — точное число видимых колонок, настраивается в adm-footer.php */
.foot-brand{font-family:var(--display);font-size:22px;font-weight:700;margin-bottom:14px;color:var(--ink)}
.foot-brand-logo{display:block;max-width:280px;width:auto;height:auto} /* max-height задаётся инлайн из site_settings.footer_logo_height_px (adm-footer.php) */
.foot-desc{color:var(--ink-dim);font-size:13px;line-height:1.7;max-width:32ch}
.foot-col h5{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:16px}
.foot-col a{display:flex;align-items:center;color:var(--ink-dim);font-size:13.5px;margin-bottom:10px;transition:.15s}
.foot-col a:hover{color:var(--ink)}
/* Иконка (footer_links.icon — глиф, ИЛИ footer_link_icon_img — картинка из
   медиатеки) и текст ссылки — flex-строка (не inline-обтекание), иконка и
   текст гарантированно в один ряд и по центру вертикали независимо от
   реального размера картинки-иконки. ── Кнопки-иконки без подписи
   (footer_links.icon_only, напр. колонка Social) — круглые, идут в ряд
   (inline-flex сам оборачивается по строкам, без отдельного flex-
   контейнера на родителе). */
.foot-col a > :first-child{margin-right:6px;flex:0 0 auto}
.foot-link-icon-img{width:var(--foot-link-icon-size,16px);height:var(--foot-link-icon-size,16px);object-fit:contain;flex:0 0 auto} /* --foot-link-icon-size печатается из footer.php — site_settings.footer_link_icon_size_px, adm-footer.php */
.foot-icon-btn{display:inline-flex !important;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;border:1px solid var(--line);margin:0 10px 10px 0 !important;font-size:15px}
.foot-icon-btn .foot-link-icon-img{vertical-align:middle}
.foot-icon-btn:hover{background:var(--bg-card);color:var(--ink)}
.foot-legal{border-top:1px solid var(--line);padding-top:22px}
.foot-trust{font-size:12px;color:var(--ink-faint);margin-bottom:10px}
.foot-bottom{display:flex;justify-content:space-between;font-size:12px;color:var(--ink-faint);flex-wrap:wrap;gap:10px}

/* ── INFO-CARD FAMILY — base / .icon / .quote / .row (Design System v2 §Info-card) ──
   Real class names kept as-is (.svc-card, .why-card, .svc-quote, .svc-materials);
   this is the same underlying component (bg-card + line + radius), 4 content slots.
   .svc-desc/.svc-grid перенесены сюда из homepage.css (2026-07-16) — About
   тоже использует эту же карточную семью, а не только главная. */
.svc-desc{color:var(--ink-dim);font-size:15px;max-width:52ch;line-height:1.7;margin-top:16px}
.svc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px;margin-top:44px}
@media(max-width:900px){.svc-grid{grid-template-columns:1fr}}
.svc-card{background:var(--bg-card);border:1px solid var(--line);border-radius:var(--r);padding:20px 20px;box-shadow:0 4px 10px var(--shadow-card)}
.svc-card h4{font-family:var(--display);font-size:18.5px;font-weight:700;margin-bottom:10px;letter-spacing:-.005em}
.svc-card p{color:var(--ink-dim);font-size:13.5px;line-height:1.65}
/* По прямому запросу — иконка слева, заголовок+текст справа (раньше
   .why-ic шла первой в вертикальном стеке, margin-bottom:20px отделял её
   от заголовка сверху вниз). display:flex на самой карточке + .why-ic
   больше не даёт отступ вниз (не нужен в горизонтальной раскладке),
   вместо этого — общий gap у родителя. align-items:flex-start — чтобы
   иконка стояла у верхнего края текста, а не по центру всей карточки
   (особенно заметно, если текст переносится на несколько строк). */
.why-card{display:flex;align-items:flex-start;gap:16px;background:var(--bg-card);border:1px solid var(--line);border-radius:var(--r);padding:20px 20px;box-shadow:0 4px 10px var(--shadow-card)}
.why-ic{width:46px;height:46px;flex:0 0 auto;border-radius:50%;background:var(--bg-3);display:flex;align-items:center;justify-content:center;font-size:19px;color:var(--accent);overflow:hidden}
/* Картинка вместо иконки (см. includes/why_card.php) — тот же круглый
   слот 46×46, но заполнен фото (object-fit:cover), а не символом. */
.why-ic--img img{width:100%;height:100%;object-fit:cover}
.why-card-body{min-width:0}
.why-card h4{font-family:var(--display);font-size:16px;font-weight:700;margin-bottom:10px;letter-spacing:-.005em}
.why-card p{color:var(--ink-dim);font-size:13.5px;line-height:1.65}
.svc-quote{margin-top:44px;padding:36px 40px;background:var(--bg-card);border:1px solid var(--line);border-radius:var(--r);position:relative;box-shadow:0 4px 10px var(--shadow-card)}
.svc-quote::before{content:'"';position:absolute;top:14px;left:22px;font-family:var(--display);font-size:60px;line-height:1;color:var(--accent-dim);font-weight:700}
.svc-quote p{font-family:var(--display);font-size:clamp(17px,1.8vw,20px);font-weight:500;line-height:1.5;letter-spacing:-.005em;max-width:62ch;color:var(--ink);position:relative;padding-left:6px}
.svc-quote-sig{margin-top:18px;font-size:13px;color:var(--accent);font-weight:600;padding-left:6px}
.svc-materials{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;margin-top:36px;padding:20px 24px;background:var(--bg-card);border:1px solid var(--line);border-radius:var(--r-panel);font-size:13.5px;color:var(--ink-dim)}
.svc-materials b{color:var(--ink);font-weight:600}
.svc-materials .dot{color:var(--accent-dim);opacity:.8}

/* ── LEVEL BADGE — decorative pill marking which catalog tier a detail page
   describes (Material / Pattern / Sample). Purely a visual label, no logic. ── */
.lvl-badge{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:500;padding:6px 13px;border-radius:99px;white-space:nowrap}
.lvl-badge.material{color:var(--teal);background:var(--green-bg);border:1px solid color-mix(in srgb, var(--teal) 25%, transparent)}
.lvl-badge.pattern{color:var(--purple);background:var(--purple-bg);border:1px solid color-mix(in srgb, var(--purple) 28%, transparent)}
.lvl-badge.sample{color:var(--accent);background:var(--accent-bg);border:1px solid var(--badge-sample-border)}
.lvl-badge.project{color:var(--clay);background:var(--clay-bg);border:1px solid color-mix(in srgb, var(--clay) 28%, transparent)}

@media(max-width:560px){.lvl-badge{padding:2px 7px}}

/* ── CATALOG CARD — .card — canonical, редизайн v2 (см. ferrara-design-spec-v2.md,
   обкатано в public/dev-preview-cards.php). ОДИН класс на функцию для ВСЕХ
   четырёх типов карточек (Product/Sample/Project/Master Sample) — раньше
   здесь было 4 почти байт-в-байт идентичных семейства (.pcard/.mcard/
   .pjcard/.scard, каждое ~35 строк CSS + свой мобильный блок) с
   расхождениями, не несущими смысла (разная скорость зума фото, разный
   паддинг тела). Теперь один набор классов, разница между типами карточек —
   только в том, что реально отличается (Sample — цветной кружок, Project/
   Master Sample — filmstrip, Product — единственная карточка с описанием на
   мобильном, .card--peek). .pgrid/.mgrid (обёртки-сетки) НЕ переименованы —
   это отдельный вопрос ширины/колонок конкретной страницы, не самой
   карточки, трогать их сейчас не нужно. */
.pgrid,.mgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
/* box-shadow (2026-08-19, по прямому запросу — "привести всё к одному
   виду, тёплый") — раньше 0 20px 46px rgba(0,0,0,.16), чёрная и гораздо
   крупнее, чем у остальных плашек/карточек сайта. Теперь тот же тёплый
   стандарт (тон --ink, не голый чёрный), что уже используется в
   .svc-card/.why-card/.svc-quote и в тени карточек How We Work
   (homepage.css, .how-card) — везде один и тот же язык теней. */
.card{border-radius:var(--r);border:1px solid var(--line);background:var(--bg-card);overflow:hidden;display:flex;flex-direction:column;height:580px;box-shadow:0 4px 10px var(--shadow-card);transition:.18s;--hover-y:0px;transform:translateY(var(--hover-y));transform-origin:50% 100%}
/* Тень на hover — расширяется и темнеет (по прямому запросу — "так же как
   у карточек образцов"): у .mdf-card (mdf-card.css) на hover тень растёт
   0.55→1.3 по смещению и .28→.38 по альфе (~×2.4 крупнее/темнее) — здесь
   та же ПРОПОРЦИЯ роста применена к своей (другой) базе 4px/10px, не
   скопированы абсолютные пиксели образца (тот масштабирован под свой,
   гораздо более мелкий, размер карточки через --mdf-unit). translateY(-4px)
   (2026-08-19, по прямому запросу — единый подъём для ВСЕХ карточек сайта:
   проектов/образцов/материалов, см. тот же -4px у .mdf-card в mdf-card.css
   и .pc-card в product-paint-bucket.css) — единственная карточка ЭТОГО
   класса, которую поднимает hover: .card используется на каждой публичной
   странице (project/classic.php+default.php, sample/flat.php,
   master-sample/default.php, product/default.php).
   НАЙДЕНО И ИСПРАВЛЕНО (2026-08-19): раньше hover сразу задавал
   transform:translateY(-4px) целиком — пульс карточки (зум фото по свотчу,
   см. site.js) тоже полностью владеет transform на время анимации, из-за
   чего в начале/конце анимации транспонировался в scale(1) без translateY —
   карточка на миг "падала" на 4px вниз и обратно вместе с пульсом, вместо
   ожидаемого "не опускать, пока курсор не ушёл". Подъём переведён на
   CSS-переменную --hover-y — meняется только она, реальный transform
   (переменная база .card, читает --hover-y) остаётся под общим
   transition:.18s и не зависит от того, идёт ли параллельно другая
   transform-анимация (пульс карточки, JS Element.animate(), см. site.js, —
   его keyframes читают ту же переменную). transform-origin:50% 100% (низ по
   центру, не 50% 50%, задан здесь статично, а не в момент пульса) — рост
   пульса должен идти ТОЛЬКО вверх и в бока, низ карточки не должен
   сдвигаться вниз: якорь трансформации внизу — верхний край уезжает вверх,
   левый/правый края расходятся симметрично в стороны, нижний край стоит на
   месте. На простом translateY(hover) этот origin никак не влияет (перенос
   не зависит от точки опоры, только scale/rotate зависят), можно держать
   его тут постоянно. */
.card:hover{border-color:var(--line-3);box-shadow:0 10px 19px var(--shadow-card-hover);--hover-y:-4px}

.card-photo{position:relative;flex:1;min-height:0;overflow:hidden;display:block;background:var(--bg-3)}
.card-photo img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .35s}
/* Клик по миниатюре filmstrip (.card-ft, site.js) — вместо мгновенной
   подмены src создаётся клон картинки свотча, "выпрыгивающий" из ТОЧКИ в
   центре кликнутого свотча (0×0) — bubble-эффект: растёт И одновременно
   едет центром к центру .card-photo, до размера цели+5px (перелёт), затем
   откатывается ровно до размера цели. Вся анимация (клон и пульс карточки)
   разыгрывается в JS через Element.animate() (Web Animations API, не CSS
   @keyframes+классы) — НАЙДЕНО И ИСПРАВЛЕНО (2026-08-19, 7-й заход):
   классовый retrigger-приём (remove класса → reflow → add класса) у пульса
   карточки иногда не запускал анимацию заново (add/remove происходили
   практически мгновенно, без реального проигрывания) — .animate() создаёт
   новый Animation-объект на каждый клик, никакого retrigger'а не нужно,
   .finished — нативный promise, надёжнее самодельной connect
   классы/событие-слушатели цепочки. Разметка ниже — только базовое,
   "состояние покоя" клона; конкретные left/top/width/height/opacity на
   каждом кадре анимации ставит JS. z-index:1 — НИЖЕ .card-photo-bottom
   (пилюли/filmstrip, z-index:2) и выше статичного главного фото (z-index не
   задан) — свотчи остаются видимыми/кликабельными поверх растущей картинки
   всё время анимации. */
.card-photo .card-photo-zoom-clone{position:absolute;top:0;left:0;width:0;height:0;opacity:0;z-index:1;object-fit:cover;pointer-events:none;display:block}
/* :not(...) — исключает фото карусели (.card-photo-slide-bg/-fg, редизайн
   v2): у них уже СВОЙ transform (translateX слайда + scale(1.15) у
   размытой подложки, см. ниже), а этот hover-zoom (0,3,1 по специфичности)
   их перебивал — .card-photo-slide-bg вместо scale(1.15) получал scale(1.05)
   при наведении, подложка «сжималась» и обнажала свой край понизу слайда
   (баг, пойман по скриншоту). Обычные одиночные фото карточек (Sample/
   Project и т.п.) зумируются как и раньше. */
.card:hover .card-photo img:not(.card-photo-slide-bg):not(.card-photo-slide-fg){transform:scale(1.05)}

/* Верхний край поверх фото — тип-бейдж слева, счётчики (Material — «N
   patterns»/«N samples») справа; justify-content:space-between вместо
   отдельных absolute-позиций для каждого — оба просто flex-элементы. */
.card-photo-top{position:absolute;top:0;left:0;right:0;z-index:2;display:flex;justify-content:space-between;gap:8px;padding:10px}
.card-badge{background:color-mix(in srgb, var(--bg-card) 94%, transparent);color:var(--accent);font-family:var(--mono);font-size:10px;letter-spacing:2px;text-transform:uppercase;padding:5px 10px;border-radius:var(--r)}
.card-count-badge{display:flex;gap:6px}
.card-count-pill{padding:4px 10px;border-radius:99px;font-family:var(--mono);font-size:10px;letter-spacing:.04em;background:color-mix(in srgb, var(--shadow-black) 55%, transparent);color:var(--text-inverse);white-space:nowrap}
/* Карточка продукта в каталоге материалов (2026-08-19, по прямому запросу
   "X projects"/"Y samples", прозрачность .05 у фона плашки и .1 у текста) —
   scoped через .card--peek (уникален для продукта, см. product/default.php),
   чтобы НЕ задеть тот же .card-count-pill на карточке Master Sample (там
   он остаётся непрозрачным, как раньше). */
.card--peek .card-count-pill{background:color-mix(in srgb, var(--shadow-black) 15%, transparent);color:color-mix(in srgb, var(--text-inverse) 50%, transparent)}
/* Sample-only — цветной кружок, правый верхний угол (левый занят .card-badge). */
.card-color-dot{position:absolute;top:9px;right:9px;width:18px;height:18px;border-radius:50%;border:1.5px solid color-mix(in srgb, var(--text-on-accent) 75%, transparent);box-shadow:0 1px 6px color-mix(in srgb, var(--shadow-black) 35%, transparent);z-index:2}

/* Filmstrip миниатюр (Project/Master Sample, >1 фото) — клик по миниатюре
   меняет главное фото, делегирование в site.js (.card-ft). */
.card-filmstrip{position:absolute;bottom:0;left:0;right:0;display:flex;gap:4px;padding:8px;background:linear-gradient(transparent,color-mix(in srgb, var(--overlay-dark) 70%, transparent));z-index:2}
.card-ft{width:34px;height:34px;border-radius:var(--r);overflow:hidden;cursor:pointer;border:1.5px solid transparent;flex:0 0 auto;background:color-mix(in srgb, var(--text-on-accent) 20%, transparent)}
.card-ft.on{border-color:var(--bg-3)}
.card-ft img{width:100%;height:100%;object-fit:cover;display:block}
/* Зона нажатия мельче рекомендованных ~44px на сенсорном экране (SEO/UX-
   аудит, находка №34) — на мобильном (где это реально имеет значение,
   мышью точность не проблема) увеличиваем сам видимый тайл, а не только
   invisible-хитбокс: filmstrip и так лежит в узкой полосе снизу карточки,
   псевдоэлемент-расширитель поверх overflow:hidden тут не сработал бы
   (обрезался бы тем же overflow, что держит миниатюру круглой). */
@media(max-width:560px){
  .card-ft{width:40px;height:40px}
}

.card-body{padding:10px 22px;flex:0 0 auto}
.card-eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:0}
.card-name{font-size:19px;font-style:italic;font-weight:700;line-height:1.2;margin:0 0 18px;color:var(--ink)}
/* Карточка Product — тот же шрифт/размер, что у .mdf-name (переключаемая
   MDF-плашка, см. assets/mdf-card.css), по прямому запросу (2026-08-21) —
   .card--peek уникален для карточки продукта (см. product/default.php),
   не задевает .card-name у sample/project/master-sample карточек и у
   .card-id-pill (мелкая плашка поверх фото — своя, более узкая правка ниже). */
.card--peek .card-body .card-name{font-family:var(--display, Georgia, serif);font-size:14px}
.card-subtitle{font-size:12px;color:var(--ink-faint);font-style:italic;margin:-22px 0 -6px}
/* Пробная строка метрик проекта (площадь/сроки/сумма) — обсуждение
   редизайна главной 2026-08-17, доп.2.4, см. cards/project/classic.php.
   Реальных колонок под это в БД нет, значение сейчас заглушка. */
.card-metrics{font-family:var(--mono);font-size:11.5px;letter-spacing:.02em;color:var(--ink-faint);margin:8px 0 0}
/* Модификатор для оверлея поверх фото (.proj-cell) — тот же приём
   контраста, что у .card-id-name (text-shadow вместо плашки-фона). */
.card-metrics--overlay{color:color-mix(in srgb, var(--text-on-accent) 88%, transparent);margin-top:6px;text-shadow:0 1px 4px color-mix(in srgb, var(--shadow-black) 55%, transparent),0 1px 10px color-mix(in srgb, var(--shadow-black) 35%, transparent)}
.card-label{font-family:var(--mono);font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--ink-faint);margin-bottom:0}
.card-desc{font-size:13.5px;line-height:1.65;color:var(--ink-dim);margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;line-clamp: 2;-webkit-box-orient:vertical;overflow:hidden;min-height:3.3em}
/* Карточка проекта (default.php/classic.php, 2026-08-20): текст уже обрезан
   на PHP (cardDescExcerpt(), card_render.php) под ширину карточки — сам
   -webkit-line-clamp здесь больше не нужен и снят, т.к. давал баг: видна
   срезанная верхушка следующей строки (computed display у .card-desc
   оказывался flow-root, а не -webkit-box — клип срабатывал не по границе
   строки). overflow:hidden оставлен только страховкой для редкой узкой
   ширины (переход 2кол→1кол) — там просто вырастет на строку, без обрезки. */
.card-desc--trunc{display:block;-webkit-line-clamp:unset;-webkit-box-orient:unset}
.card-field{margin-top:9px}
.card-field:first-of-type{margin-top:0}
.card-value{font-size:13px;color:var(--ink)}
.card-code-suffix{font-family:var(--mono);font-size:13px;color:var(--ink-faint);font-style:normal}

.card-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:4px}
.tag{display:inline-block;border:1px solid var(--line-3);border-radius:var(--r);font-family:inherit;font-size:13px;color:var(--ink);padding:2px 7px;white-space:nowrap;text-decoration:none}
a.tag:hover{border-color:var(--accent);color:var(--accent)}

.card-footer{padding:0 22px;height:35px;display:flex;align-items:center;border-top:1px solid var(--line);margin-top:auto}
.cta-link{font-family:var(--mono);color:var(--accent);font-size:12px;letter-spacing:1.5px;text-transform:uppercase;text-decoration:none}
.cta-link:hover{color:var(--accent-deep)}

/* ══ МОБИЛЬНЫЙ КОМПАКТНЫЙ ВИД (≤560px), одна на все 4 типа карточек ══
   Тело/футер прячутся, фото растягивается на всю карточку (1:1), id-пилюли
   поверх фото снизу-слева + стрелка снизу-справа. */
.card-id-pills{display:none;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}
.card-id-pill{display:flex;align-items:baseline;gap:6px;background:color-mix(in srgb, var(--bg-card) 96%, transparent);border-radius:var(--r);padding:5px 5px;margin: 10px; box-shadow:0 8px 20px color-mix(in srgb, var(--shadow-black) 16%, transparent);max-width:100%}
/* Название карточки Project — просто текст поверх фото, без плашки-фона
   (по прямому запросу); text-shadow вместо белого фона держит читаемость
   на любом фото. */
.card-id-name{display:flex;align-items:baseline;max-width:100%;text-shadow:0 1px 4px color-mix(in srgb, var(--shadow-black) 55%, transparent),0 1px 10px color-mix(in srgb, var(--shadow-black) 35%, transparent)}
.card-id-name .card-name{font-size:15px;margin:0;color:var(--text-on-accent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.card-id-pill .card-eyebrow,.card-id-pill .card-label,.card-id-pill .card-name,.card-id-pill .card-value{line-height:1}
.card-id-pill .card-eyebrow,.card-id-pill .card-label{flex:0 0 auto}
.card-id-pill .card-name{font-size:14px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.card-id-pill .card-value{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.card-id-arrow{display:none;flex:0 0 auto;align-items:center;justify-content:center;line-height:1;white-space:nowrap;padding:6px 7px 4px}

.card-photo-bottom{display:none}
.card-photo-row{display:flex;align-items:flex-end;justify-content:space-between;gap:10px}

@media(max-width:560px){
  .card{height:auto}
  .card-photo{flex:none;height:auto;aspect-ratio:1/1}
  .card-body{display:none}
  .card-footer{display:none}
  .card-photo-bottom{position:absolute;left:0;right:0;bottom:0;z-index:2;display:flex;flex-direction:column;gap:10px;padding:10px}
  .card-filmstrip{position:static;padding:0}
  .card-id-pills,.card-id-arrow{display:flex}
  .card-id-pills{flex:1 1 auto;min-width:0}
}

/* .card--overlay (Project, по прямому запросу 2026-08-16) — тот же
   мобильный оверлей (имя/материал/стрелка/filmstrip поверх фото), что
   выше, но НА ЛЮБОЙ ширине, а не только ≤560px, и БЕЗ смены пропорций
   карточки — .card/.card-photo размер/aspect-ratio не трогаем (высота
   остаётся 580px как у обычной .card, фото просто заполняет её целиком —
   .card-photo — flex:1, единственный ребёнок; card-body/card-footer для
   этого варианта вообще не рендерятся в PHP, см.
   includes/cards/project/default.php, поэтому здесь нет отдельного правила
   display:none для них — прятать нечего). Только Project — другие типы
   карточек (.card без этого модификатора), а также вариант
   includes/cards/project/classic.php продолжают показывать обычный
   десктопный .card-body ниже фото, как раньше. */
.card--overlay .card-photo-bottom{ position:absolute; left:0; right:0; bottom:0; z-index:2; display:flex; flex-direction:column; gap:10px; padding:10px }
.card--overlay .card-filmstrip{ position:static; padding:0 }
.card--overlay .card-id-pills,
.card--overlay .card-id-arrow{ display:flex }
.card--overlay .card-id-pills{ flex:1 1 auto; min-width:0 }
/* Описание проекта (2026-08-19, project_translations.description) — поверх
   фото, тот же язык скрима+светлого текста, что у .card--peek/.rail--product
   выше, но line-clamp:2 наследуется от базового .card-desc как есть (по
   прямому запросу — не 3 строки, как у Product). Скрим-градиент нужен только
   когда есть текст поверх фото — добавлен на сам .card-desc фоном под текст
   (не на весь .card-photo-bottom), чтобы не затемнять пилюли/filmstrip зря,
   когда описания нет.*/
.card--overlay .card-photo-bottom .card-desc{margin:0;padding:6px 8px;border-radius:var(--r);background:linear-gradient(to top, color-mix(in srgb, var(--overlay-dark) 20%, transparent) 0%, color-mix(in srgb, var(--overlay-dark) 20%, transparent) 100%);color:color-mix(in srgb, var(--text-inverse) 92%, transparent);font-size:12px;line-height:1.5;min-height:0}

/* Мобильная карточка Project — выше, чем общий 1:1 у остальных типов
   (.card-photo{aspect-ratio:1/1} из блока ≤560px выше) — по прямому
   запросу 3:4 (та же пропорция, что у .main-viewer детальных страниц и
   у .mdf-card). .card на мобильном уже height:auto (см. ≤560px выше),
   единственный ребёнок — .card-photo (вся разметка card--overlay внутри
   <a class="card-photo">), поэтому переопределения aspect-ratio на самой
   .card-photo достаточно — высота карточки следует за ним. */
@media(max-width:560px){
  .card--overlay .card-photo{aspect-ratio:3/4}
}

/* Product-only-исключение — mcard остаётся ЕДИНСТВЕННОЙ карточкой с кратким
   описанием на мобильном (у Sample/Project/Master Sample — только пилюли
   поверх фото). .card--peek — ДОПОЛНИТЕЛЬНО к .card только на карточке
   Product. Описание теперь не отдельным блоком ПОД фото, а поверх самого
   фото внутри .card-photo-bottom (второй экземпляр текста — тот же приём,
   что и у .card-id-pill .card-name/.card-body .card-name дублируется под
   разные раскладки), градиент+светлый текст — тот же язык, что у карусели
   Product на главной (см. .rail--product .card-photo-bottom выше) и у
   .mv-cap на детальных страницах. */
@media(max-width:560px){
  .card--peek{aspect-ratio:1/1}
  .card--peek .card-photo{flex:1;height:auto}
  .card--peek .card-body{display:none}
  .card--peek .card-photo-bottom{background:linear-gradient(to top, color-mix(in srgb, var(--overlay-dark) 80%, transparent) 0%, color-mix(in srgb, var(--overlay-dark) 45%, transparent) 55%, transparent 100%)}
  .card--peek .card-photo-bottom .card-desc{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;margin:0;font-size:12px;line-height:1.5;color:color-mix(in srgb, var(--text-inverse) 90%, transparent)}
}

/* Десктоп Product (>560px) — пилюля "Open ↗" поверх фото, правый нижний
   угол (та же .card-id-arrow разметка, что уже есть в includes/cards/
   product/default.php для мобильного вида выше) — заменяет прежнюю
   текстовую ссылку "View product →" из .card-footer (убрана из разметки,
   по прямому запросу — "как на карточке project"). .card-body
   (имя/About/Suitable for) под фото не трогаем — только сама
   ссылка-футер заменена на плашку над фото. Только .card-id-arrow, БЕЗ
   .card-id-pills (там было бы имя) — имя уже показано в .card-body ниже,
   дублировать не нужно. justify-content:flex-end — свой, отдельно от
   базового .card-photo-row{justify-content:space-between} (общий для всех
   карточек, там ещё и .card-id-pills видим) — с одним видимым
   flex-элементом space-between прижал бы пилюлю к левому краю. */
@media(min-width:561px){
  .card--peek .card-photo-bottom{display:flex;flex-direction:column;position:absolute;left:0;right:0;bottom:0;z-index:2;padding:10px}
  .card--peek .card-photo-row{justify-content:flex-end}
  .card--peek .card-id-arrow{display:flex}
  /* .card-desc внутри .card-photo-bottom — только мобильный приём (см.
     блок ≤560px ниже, с собственным градиентом-скримом); на десктопе
     описание уже показано в .card-body (.card-field--desc) под фото —
     без этого display:none оно бы задублировалось прямо на фото без
     скрима, т.к. родитель .card-photo-bottom из display:none стал flex. */
  .card--peek .card-photo-bottom .card-desc{display:none}
}

/* ── ГОРИЗОНТАЛЬНАЯ КАРУСЕЛЬ ГЛАВНОЙ (Product/Sample/Portfolio) — canonical,
   обкатано в public/dev-preview-cards.php, редизайн v2. Full-bleed-обёртка
   (.rail-bleed) выносит рельсу за пределы .page на всю ширину экрана — см.
   применение в index.php. */
.rail-bleed{width:100vw;margin-left:calc(50% - 50vw)}
.rail{width:100%}
@media(min-width:980px){
  .rail{max-width:1200px;margin-left:auto;margin-right:auto}
}

/* ── PRODUCT — единственный слайд, ВСЕГДА компактный (мобильный) вид
   карточки, даже на широком экране (см. .card мобильный блок выше — те же
   свойства продублированы здесь под .rail--product, без media-запроса; тот
   же приём и то же обоснование, что и в прототипе: применить один и тот же
   набор правил по контексту, а не по ширине экрана, без copy-paste в CSS
   невозможно). На ПК высота капается 800px (не 1:1 — квадрат 1200×1200
   был бы слишком высоким), см. media-запрос ниже. */
/* .rail-viewport — рамка фиксированного размера вокруг единственной
   Product-карточки: раньше размер (aspect-ratio/height) держала сама
   .card--peek, теперь — эта обёртка, а .card становится position:absolute;
   inset:0 внутри неё. Нужно для push-slide переключения материала (см.
   index.php::initProductRail()) — во время смены внутри viewport на
   мгновение лежат ДВЕ карточки (уходящая + приезжающая), edut синхронно
   через translateX, overflow обрезает вылезающую за рамку часть, как у
   .card-photo (тот же приём, что и .card-photo-slide).
   overflow-x:clip (а не overflow:hidden/visible) — по X viewport должен
   СТРОГО резать реальный контент карточек по своей границе ВСЕГДА, включая
   время push-slide анимации: во время слайда через границу видимого окна и
   проходит соседняя (въезжающая/уезжающая) карточка — это и есть сам
   механизм анимации, обрезка по чёткой границе тут не баг, а требование
   (без неё было бы видно лишний кусок соседней карточки раньше/дольше
   положенного). По Y — overflow-y:visible, карточки НИКОГДА не двигаются
   вертикально (только translateX), поэтому обрезка сверху/снизу вообще не
   нужна ни в покое, ни во время слайда — там нет риска "утечки" соседней
   карточки, только собственная тень .card, которой ничего не мешает
   всегда быть видна.
   НАЙДЕНО И ИСПРАВЛЕНО (несколько раз, итог): раньше пытались одним общим
   overflow-clip-margin (и по X, и по Y сразу) дать запас "только для
   тени" и убирать его на время .is-sliding — margin не различает "это
   тень" и "это реальный сосед в паре пикселей от границы", а единое
   значение на ОБЕИХ осях било по вертикали, где утечки в принципе не
   может быть. Пробовали ещё компенсировать ОТДЕЛЬНОЙ постоянной тенью на
   самом .rail-viewport вместо тени карточек — работало без утечки, но
   тень исчезала из зазоров МЕЖДУ карточками (видна была только по
   контуру всего блока целиком, не между отдельными карточками).
   ФИНАЛЬНОЕ РЕШЕНИЕ: тень возвращена на сами .card (см. .rail--product
   .card{box-shadow} ниже) — по Y она теперь видна всегда (overflow-y:
   visible), включая тень в зазорах между карточками. По X (единственная
   ось реального риска утечки) обрезается строго всегда, без исключений —
   у самой ЛЕВОЙ и ПРАВОЙ карточек окна тень с внешней (не обращённой к
   соседям) стороны остаётся приглушена, это единственный оставшийся,
   сознательно принятый компромисс (в отличие от зазоров между карточками
   и верха/низа — не заметен так же явно, это самый край всего блока). */
.rail--product .rail-viewport{position:relative;overflow-x:clip;overflow-y:visible;aspect-ratio:1/1}
@media(min-width:980px){
  .rail--product .rail-viewport{aspect-ratio:auto;height:800px}
}
.rail--product .card{position:absolute;inset:0;width:100%;height:100%;transition:transform .5s ease}
.rail--product .card-photo{flex:none;height:auto;aspect-ratio:1/1}
.rail--product .card-body{display:none}
.rail--product .card-footer{display:none}
.rail--product .card-photo-bottom{position:absolute;left:0;right:0;bottom:0;z-index:2;display:flex;flex-direction:column;gap:10px;padding:10px}
.rail--product .card-id-pills,.rail--product .card-id-arrow{display:flex}
.rail--product .card-id-pills{flex:1 1 auto;min-width:0}
.rail--product .card--peek{height:100%}
.rail--product .card--peek .card-photo{flex:1;height:auto}
/* Описание материала теперь не отдельным блоком ПОД фото (.card-body — см.
   правило display:none выше), а последним элементом прямо внутри
   .card-photo-bottom — поверх самого фото, тот же язык, что у .mv-cap на
   детальных страницах (detail-page.css): градиент-затемнение снизу фото +
   светлый текст. Состав и положение остальных элементов карточки
   (бейдж/пилюли/индикатор/стрелка) не менялись — только описание
   переехало с фото. */
.rail--product .card-photo-bottom{background:linear-gradient(to top, color-mix(in srgb, var(--overlay-dark) 80%, transparent) 0%, color-mix(in srgb, var(--overlay-dark) 45%, transparent) 55%, transparent 100%)}
.rail--product .card-photo-bottom .card-desc{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;margin:0;font-size:12px;line-height:1.5;color:color-mix(in srgb, var(--text-inverse) 90%, transparent)}
/* .rail-slide-enter-r/-l — стартовая позиция ВЪЕЗЖАЮЩЕЙ карточки (JS ставит
   класс сразу при создании, до появления в DOM/reflow), .rail-slide-out-l/-r
   — куда должна уехать УХОДЯЩАЯ (JS переключает классы после форс-reflow,
   .card уже имеет transition — переход анимируется). */
.rail--product .card.rail-slide-enter-r{transform:translateX(100%)}
.rail--product .card.rail-slide-enter-l{transform:translateX(-100%)}
.rail--product .card.rail-slide-out-l{transform:translateX(-100%)}
.rail--product .card.rail-slide-out-r{transform:translateX(100%)}

/* Автослайдшоу фото ВНУТРИ слайда (галерея одного материала, см. index.php
   inline <script>) — едет влево, следующее фото «приклеено» справа и въезжает
   следом (translateX), а не кроссфейд. Три состояния: по умолчанию —
   translateX(100%), ждёт своей очереди справа за кадром; .on — 0, видимый
   слайд; .prev — -100%, только что уехавший налево. .card-photo (родитель)
   уже overflow:hidden — сдвинутые за пределы слайды не видны. */
.card-photo-slide{position:absolute;inset:0;transform:translateX(100%);transition:transform .6s ease;overflow:hidden}
.card-photo-slide.on{transform:translateX(0)}
.card-photo-slide.prev{transform:translateX(-100%)}
/* Тот же приём, что у .media-fit/.media-fit-bg/.media-fit-fg на детальных
   страницах (detail-page.css), на любой ширине экрана (по прямому запросу —
   раньше было только ПК): фото вписывается целиком (object-fit:contain, без
   обрезки), пустые поля по краям заполняет та же картинка позади, растянутая
   cover+blur, а не пустой леттербокс/обрезка. Селектор с префиксом
   .card-photo (класс+класс) — НЕ просто .card-photo-slide-fg{...} —
   иначе .card-photo img{object-fit:cover} (класс+тег) специфичнее и
   перебивает contain независимо от порядка правил в файле (найденный баг:
   contain ни разу не применялся, фото всегда обрезалось как cover). */
.card-photo .card-photo-slide-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:blur(32px) saturate(1.15) brightness(.72);transform:scale(1.15)}
.card-photo .card-photo-slide-fg{position:relative;z-index:1;width:100%;height:100%;object-fit:contain;display:block}
/* Лёгкий зум при наведении — вернул отдельным правилом (не общим
   .card:hover .card-photo img выше, тот сюда намеренно не достаёт, см.
   :not() там) только для видимого фото (-fg); размытая подложка (-bg)
   своего scale(1.15) не теряет — та самая причина прошлого бага. */
.card:hover .card-photo .card-photo-slide-fg{transform:scale(1.05)}
/* Индикатор автослайдшоу фото — сегментные полоски (Instagram-Stories
   принцип: одна полоска на фото, активная заполняется слева направо за
   время до смены), а не кружки/спиннер — тот же язык, что уже даёт
   .rail-progress/.rail-seg (переключение материалов) выше, не новая форма
   в интерфейсе. Ширина заливки управляется JS напрямую через inline-style +
   transition (см. index.php::playProgressSegment()), НЕ через @keyframes —
   с animation-fill-mode:forwards снятие/навешивание класса .active↔.done не
   всегда мгновенно "отпускало" отрисованное состояние предыдущего сегмента
   (найденный баг: он оставался залит, будто ещё активен). Здесь CSS отвечает
   только за ЦВЕТ (.done — серый), ширину задаёт исключительно JS. */
.card-photo-progress{display:flex;gap:4px}
.card-photo-progress-seg{flex:1;height:2px;border-radius:var(--r);background:color-mix(in srgb, var(--bg-card) 35%, transparent);overflow:hidden}
.card-photo-progress-fill{display:block;height:100%;width:0;background:var(--bg-card)}
.card-photo-progress-seg.done .card-photo-progress-fill{background:var(--progress-fill-done)}

/* ── SAMPLE / PROJECT — мобильный (<980px): 1 карточка компактно на всю
   ширину; ПК (≥980px): 3 обычные десктопные карточки в ряд. В отличие от
   .rail--product здесь ДЕЙСТВИТЕЛЬНО нужен media-query — переключение
   зависит от реальной ширины экрана.
   НАЙДЕНО И ИСПРАВЛЕНО: мобильный вариант раньше прятал 2-ю/3-ю карточку
   структурно (:not(:first-child)) — это завязано на реальный порядок узлов
   в DOM и полностью игнорировало JS-переключение (index.php,
   initWindowRail() двигает .win-hidden/order, а не сами узлы DOM) — стрелки/
   вкладки на мобильном визуально ничего не меняли. Теперь и мобильный, и ПК
   вид управляются ОДНИМ и тем же .win-hidden — JS сам считает окно нужного
   размера (1 на мобильном, 3 на ПК, см. windowSize() в initWindowRail())
   через matchMedia, а не полагается на CSS-структуру. */
.card-grid{display:grid}
@media(max-width:979px){
  .rail--sample .card,.rail--project .card{height:auto}
  .rail--sample .card-photo,.rail--project .card-photo{flex:none;height:auto;aspect-ratio:1/1}
  .rail--sample .card-body,.rail--project .card-body{display:none}
  .rail--sample .card-footer,.rail--project .card-footer{display:none}
  .rail--sample .card-photo-bottom,.rail--project .card-photo-bottom{position:absolute;left:0;right:0;bottom:0;z-index:2;display:flex;flex-direction:column;gap:10px;padding:10px}
  .rail--sample .card-filmstrip,.rail--project .card-filmstrip{position:static;padding:0}
  .rail--sample .card-photo-row,.rail--project .card-photo-row,.rail--sample .card-id-pills,.rail--project .card-id-pills,.rail--sample .card-id-arrow,.rail--project .card-id-arrow{display:flex}
  .rail--sample .card-id-pills,.rail--project .card-id-pills{flex:1 1 auto;min-width:0}
  .rail--sample .card-grid,.rail--project .card-grid{grid-template-columns:1fr}
}
@media(min-width:980px){
  .rail--sample .card-grid,.rail--project .card-grid{grid-template-columns:repeat(3,1fr);gap:20px}
}
/* Без media-запроса — JS сам решает, сколько карточек показать (windowSize()
   в initWindowRail()), CSS только прячет то, что JS пометил лишним, на
   любой ширине экрана. */
.rail--sample .card.win-hidden,.rail--project .card.win-hidden{display:none}

/* .rail-viewport — та же роль и то же финальное решение по теням, что и у
   Product (см. .rail--product .rail-viewport выше — overflow-x:clip
   строгий и постоянный по X, overflow-y:visible по Y, тень на самих .card
   видна всегда сверху/снизу и в зазорах, приглушена только с внешней
   стороны крайних карточек окна): фиксированный размер, чтобы .card-grid
   можно было position:absolute;inset:0 и на время push-slide переключения
   "окна" (см. initWindowRail(), index.php) держать в этом месте
   одновременно ДВА .card-grid — уходящий снимок (cloneNode прежнего
   состояния) поверх и уже переключённый настоящий снизу, синхронно едущие
   в одну сторону. Размер задан так же, как у самих карточек: на мобильном
   (окно из 1 карточки) — квадрат по aspect-ratio фото, на ПК (окно из 3,
   обычная высота карточки 580px, см. .card{height} в начале файла). */
.rail--sample .rail-viewport,.rail--project .rail-viewport{position:relative;overflow-x:clip;overflow-y:visible}
@media(max-width:979px){
  .rail--sample .rail-viewport,.rail--project .rail-viewport{aspect-ratio:1/1}
}
@media(min-width:980px){
  .rail--sample .rail-viewport,.rail--project .rail-viewport{height:580px}
}
/* --rail-slide-dist — на сколько реально едет весь .card-grid при
   переключении. НАЙДЕНО И ИСПРАВЛЕНО: раньше всегда 100% (вся ширина
   грида) — на мобильном (окно из 1 карточки) это и есть один шаг, верно,
   но на ПК (окно из 3) грид — это целиком все 3 карточки, и сдвиг на
   100% визуально уводил весь набор целиком, будто заменились все 3, а не
   на одну позицию, хотя по данным (win-hidden/order) реально смещалось
   ровно на 1 индекс (см. layout() в index.php — 2 из 3 карточек и там, и
   там одни и те же). Сдвиг на 100%/windowSize (1 ширина карточки, не
   всего грида) даёт правильную иллюзию: у уходящего снимка и приезжающего
   грида пути карточек 2-3 совпадают пиксель-в-пиксель (одна и та же
   карточка, тот же путь), визуально движется только выходящая с одного
   края и входящая с другого — ровно "сдвиг на одну карточку".
   На ПК значение больше НЕ фиксированный процент (33.334%) — из-за
   gap:20px между карточками (см. ниже) шаг в 1/3 ширины грида был неточным
   на несколько px, что выглядело как запаздывание в начале анимации и
   рывок в конце (см. updateSlideDist() в index.php, устанавливает точный
   px через тот же --rail-slide-dist инлайновым style — побеждает это
   правило по специфичности). 100% ниже остаётся дефолтом только для
   самого первого рендера (до первого go()) и для мобильного (окно из 1
   карточки без зазора, там 100% и так точно). */
.rail--sample .card-grid,.rail--project .card-grid{position:absolute;inset:0;width:100%;height:100%;transition:transform .5s ease;--rail-slide-dist:100%}
.rail--sample .card-grid.rail-slide-enter-r,.rail--project .card-grid.rail-slide-enter-r{transform:translateX(var(--rail-slide-dist))}
.rail--sample .card-grid.rail-slide-enter-l,.rail--project .card-grid.rail-slide-enter-l{transform:translateX(calc(-1 * var(--rail-slide-dist)))}
.rail--sample .card-grid.rail-slide-out-l,.rail--project .card-grid.rail-slide-out-l{transform:translateX(calc(-1 * var(--rail-slide-dist)))}
.rail--sample .card-grid.rail-slide-out-r,.rail--project .card-grid.rail-slide-out-r{transform:translateX(var(--rail-slide-dist))}

/* ── УПРАВЛЕНИЕ (прогресс/навигация) — 90% от ширины рельсы (=ширины
   карточки), не во всю ширину. .rail-progress — только для закрытых
   списков, показанных целиком (пока используется только у Product), Sample/
   Project без прогресса, только стрелки+вкладки. */
.rail-progress{position:relative;display:flex;gap:6px;width:90%;margin:14px auto 0}
.rail-seg{flex:1 1 0;height:3px;border-radius:2px;background:var(--line)}
.rail-seg.on{background:var(--accent)}
/* position:relative — НАЙДЕНО И ИСПРАВЛЕНО: .rail-viewport{position:relative}
   выше — позиционированный элемент, а такие в CSS всегда рисуются поверх
   обычных (position:static) соседей независимо от порядка в DOM, поэтому
   его постоянная box-shadow (см. комментарий у .rail-viewport) визуально
   перекрывала .rail-nav снизу. Без z-index (не нужен) — просто попадает в
   тот же "позиционированный" слой, что и .rail-viewport, и красится поверх
   него по естественному порядку в DOM (идёт после). */
.rail-nav{position:relative;display:flex;align-items:center;gap:10px;width:90%;margin:14px auto 0}
.rail-arrow{width:34px;height:34px;flex:0 0 auto;border-radius:var(--r);border:1px solid var(--line);background:var(--bg-card);color:var(--ink);display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;cursor:pointer}
.rail-arrow:hover{border-color:var(--accent);color:var(--accent)}
/* Тот же приём, что у .card-ft выше (находка №34) — .rail-tabs рядом
   overflow-x:auto сам управляет своей шириной, увеличение стрелок на
   мобильном ничего не обрезает. */
@media(max-width:560px){
  .rail-arrow{width:40px;height:40px}
}
/* padding:5px 10px — запас по высоте И ширине под transform:scale() активной/
   hover пилюли ниже: overflow-x:auto здесь принудительно включает и
   overflow-y (спецификация CSS — нельзя auto только по одной оси), так что
   без вертикального запаса выросшая пилюля обрезалась бы сверху/снизу этим
   же контейнером (тот же механизм, что и в найденном ранее баге с обрезкой
   box-shadow у .rail); горизонтальный запас — чтобы крайняя слева/справа
   пилюля не обрезалась при увеличении о собственный край скролл-контейнера.
   gap:16px (было 12px) — при scale(1.12) широкие пилюли (длинные названия)
   иначе слегка наезжали друг на друга краями (transform не влияет на layout/
   gap, только на отрисовку, поэтому нужен запас "на глаз", а не расчёт).
   position:relative — БЕЗ этого offsetLeft у .tag (см. initTabLoop(),
   index.php) считается не относительно .rail-tabs, а относительно ближайшего
   ПОЗИЦИОНИРОВАННОГО предка выше по DOM (может не найтись вплоть до body) —
   несовместимая система координат со scrollLeft контейнера (тот всегда
   относительно самого .rail-tabs; найдено и исправлено раньше — без этого
   то весь скролл улетал в конец ленты, то обрезало пилюли).
   mask-image — растворяет пилюли у краёв ленты в фон (там же, где стрелки),
   вместо жёсткого обрезания на границе видимой области: активная пилюля
   теперь всегда ЦЕНТРИРУЕТСЯ (см. initTabLoop()::focusTab()), а не
   выравнивается по границам — значит с краёв почти всегда будет виден
   кусок соседней пилюли, и это осознанно оставлено как есть, но подано как
   "тут ещё есть, прокрути" через плавное затухание, а не обрезано резко.
   scroll-snap здесь больше НЕ используется (было раньше) — snap-align:start
   спорил бы с центрированием (это разные, несовместимые цели позиции). */
.rail-tabs{position:relative;display:flex;align-items:center;gap:16px;flex:1 1 auto;min-width:0;padding:5px 10px;overflow-x:auto;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,transparent 0,#000 28px,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,transparent 0,#000 28px,#000 calc(100% - 28px),transparent 100%)}
.rail-tabs::-webkit-scrollbar{display:none}
.rail-tabs .tag{flex:0 0 auto;cursor:pointer;transform:scale(1);transition:transform .15s ease,border-color .15s ease,color .15s ease}
/* Вкладки — <span>, не <a> (переключение через JS, не переход по ссылке),
   поэтому обычный a.tag:hover (см. блок CATALOG CARD выше) их не задевает —
   нужен свой hover, тот же самый терракотовый акцент. :not(.tag--active) —
   у уже активной пилюли тот же акцентный цвет и так, hover не должен второй
   раз "включать" то, что уже включено. Увеличение (scale) при наведении
   убрано по прямому запросу — остаётся только смена цвета, активная пилюля
   при этом по-прежнему крупнее (см. .rail-tabs .tag--active ниже). */
.rail-tabs .tag:hover:not(.tag--active){border-color:var(--accent);color:var(--accent)}
.tag--active{border-color:var(--accent);color:var(--accent)}
/* Активная пилюля — чуть крупнее неактивных, только в rail-nav (не трогаем
   .tag--active вне карусели, если он где-то ещё используется). */
.rail-tabs .tag--active{transform:scale(1.12)}

/* ── HOTSPOT MARKER — canonical, shared between project.php (точки на фото
   проекта) и homepage.php (точки на hero-фото). transform:translate(-50%,-50%)
   центрирует маркер на точке left/top — без этого left:X%;top:Y% ставит
   ЛЕВЫЙ ВЕРХНИЙ угол маркера в нужное место, и точка визуально съезжает
   вправо-вниз на половину своей ширины/высоты. Наведение (десктоп) или
   тап (мобильный) сразу открывает попап в развёрнутом виде — см. #hspop
   ниже, site.js::showHotspotCard().
   Кликабельная зона кнопки (36px) БОЛЬШЕ видимого белого кружка (22px,
   рисуется отдельным слоем через ::after) — иначе рядом с точкой легко
   промахнуться и попасть по фото (на карусели проекта клик по фото вне
   точки открывает лайтбокс) — см. запрос пользователя 2026-07-16. Кнопка
   сама прозрачная, поэтому лишняя площадь клика визуально не видна. */
.hotspot{position:absolute;width:36px;height:36px;transform:translate(-50%,-50%);border-radius:50%;background:transparent;border:none;padding:0;font:inherit;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:5;pointer-events:auto}
.hotspot::before{content:'';position:absolute;inset:-2px;border-radius:50%;background:var(--text-on-accent);opacity:.3;animation:hspulse 2.2s ease-out infinite}
@keyframes hspulse{0%{transform:scale(.7);opacity:.4}100%{transform:scale(2.1);opacity:0}}
/* content:'i' (не '+') — доп.6 доп.задачи: сторонний тестировщик прочитал
   «+» как «добавить», а не «доступна доп. информация». font-style:italic —
   тот же приём, что у повсеместного значка ⓘ, отличает букву от простого
   «i» и однозначно читается как «инфо», в отличие от «?» (скорее «помощь»)
   или лупы (скорее «поиск/зум»). */
.hotspot::after{content:'i';width:22px;height:22px;border-radius:50%;background:var(--text-on-accent);color:var(--ink);font-size:14px;font-style:italic;font-weight:700;display:flex;align-items:center;justify-content:center;line-height:1;transition:background .15s ease,color .15s ease}
/* Подсветка открытой точки — .hspop-hotspot-ghost ниже, не сама .hotspot:
   настоящая точка лежит глубоко внутри карусели, и у некоторых контейнеров
   (#mv-hotspots и т.п., detail-page.css) собственный z-index создаёт свой
   стекинг-контекст — никакой z-index на самой точке не поднял бы её выше
   scrim. */

/* .hspop-hotspot-ghost — "двойник" точки поверх scrim (см. carousel.php) —
   тот же вид, что .hotspot::after, но как независимый элемент на самом
   верхнем уровне стекинга (z-index выше scrim), site.js подставляет его
   точно на координаты настоящей точки на время открытого попапа. Цвет —
   белый, тот же, что и у обычной (неоткрытой) точки — по прямому запросу
   без изменений цвета, только чтобы не темнел под scrim. Раньше была
   скрыта на мобильном (≤560px) — НАЙДЕНО И ИСПРАВЛЕНО: на мобильном точка
   под scrim гасла точно так же, как на десктопе, подсветка нужна там же. */
.hspop-hotspot-ghost{position:fixed;width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:var(--text-on-accent);color:var(--ink);font-size:14px;font-style:italic;font-weight:700;display:flex;align-items:center;justify-content:center;line-height:1;z-index:2101;opacity:0;transition:opacity .15s ease;pointer-events:none}
.hspop-hotspot-ghost.show{opacity:1}

/* Линия-коннектор от точки до попапа (site.js::updateHotspotConnectorLine(),
   вызывается из positionHotspotPopover()). На десктопе — до ближайшего угла
   плавающей карточки. На мобильном (≤560px, bottom sheet) — НАЙДЕНО И
   ИСПРАВЛЕНО, раньше линия была скрыта целиком: теперь видна и там, до
   середины верхней грани .hspop-box (единственная содержательная точка у
   sheet'а, который сам всегда прижат к низу экрана во всю ширину — левой/
   правой/нижней грани, к которым имело бы смысл тянуть линию, у него нет).
   Толщина 3px (была 1px, по прямому запросу — "потолще") — div, повёрнутый
   transform:rotate() на нужный угол, обычный приём для "линии между двумя
   точками" в CSS, без SVG. z-index ВЫШЕ scrim (2099), не 2050, как было —
   иначе scrim, лежащий поверх, приглушал цвет линии (тот же найденный
   баг, что и у подсветки точки выше). Цвет — белый, по прямому запросу
   (был var(--line)), без изменений — только не темнеть под scrim. */
.hspop-line{position:fixed;height:3px;background:var(--text-on-accent);transform-origin:0 50%;z-index:2101;pointer-events:none;opacity:0;transition:opacity .15s ease}
.hspop-line.show{opacity:1}

/* ── HOTSPOT POPOVER — canonical, shared between project.php и homepage.php.
   Минималистичный (design-spec-v2.md §6): свотч + название + подзаголовок +
   одна CTA-ссылка, без описания/тегов/списков — данные приходят с сервера
   структурой из hotspot_resolver.php, DOM строит site.js::
   buildHotspotPopoverDom(). Текст переиспользует те же классы, что и
   карточки каталога (.card-eyebrow/.card-name/.card-subtitle/.cta-link,
   см. блок CATALOG CARD выше) — тот же приём типографики, не третий набор
   стилей текста.

   Десктоп — маленькая горизонтальная карточка (.popup) у самой точки
   хотспота, появление/исчезание — fade+scale всего попапа. Мобильный
   (≤560px) — тот же #hspop/.hspop-box становится bottom sheet (fixed внизу
   экрана, slide-up) — .popup прячется, показывается .sheet-row/.sheet-footer,
   см. media-запрос ниже. Оба режима — со scrim (.hspop-scrim) позади,
   гасящим фон, чтобы случайный клик мимо попапа не улетал в то, что под
   ним — тот же .show-класс, что и у .hspop, включает/выключает и его. */
/* pointer-events ВСЕГДА none, включая .show — НАЙДЕНО И ИСПРАВЛЕНО: раньше
   .show включал pointer-events:auto, и scrim (z-index 2099, поверх точки —
   у неё z-index:5) при hit-testing "перекрывал" точку для браузера, даже
   без какого-либо участия JS — сам факт, что scrim физически выше точки в
   стекинге, заставлял браузер слать точке mouseleave, стоило scrim
   появиться, что запускало цикл закрытие→точка снова видна→mouseenter→
   попап открывается заново (бесконечное мигание курсора). Закрытие по
   клику "мимо" не зависит от pointer-events самого scrim — оно уже ловится
   общим document-level обработчиком клика (см. site.js, проверяет
   e.target.closest('.hspop')/.hotspot) независимо от того, что физически
   было под курсором в момент клика. */
.hspop-scrim{position:fixed;inset:0;z-index:2099;background:color-mix(in srgb, var(--overlay-dark) 45%, transparent);opacity:0;visibility:hidden;transition:opacity .15s ease,visibility 0s linear .15s;pointer-events:none}
.hspop-scrim.show{opacity:1;visibility:visible;transition:opacity .15s ease,visibility 0s linear}

/* transform:scale() убран из анимации появления (НАЙДЕНО И ИСПРАВЛЕНО —
   повторные жалобы на "рывок" после появления, меньше 10px вверх + пошире
   на 15px под крестик): даже с transform-origin у ближнего к точке края
   (site.js::positionHotspotPopover()) масштаб 0.94→1 делал любую
   неточность в размере/позиции (в т.ч. из-за web-шрифтов, у которых метрики
   могут чуть доуточниться после первого layout) визуально заметной как
   рост/сдвиг. Только opacity — тот же плавный вход, но без риска
   "доразрастания". */
.hspop{position:fixed;left:0;top:0;z-index:2100;opacity:0;visibility:hidden;transition:opacity .15s ease,visibility 0s linear .15s;pointer-events:none}
.hspop.show{opacity:1;visibility:visible;transition:opacity .15s ease,visibility 0s linear;pointer-events:auto}

/* Без фиксированного max-width (320px/420px пробовали раньше) — по прямому
   запросу ширина не ограничена, растёт под контент, чтобы название всегда
   помещалось в одну строку (см. .popup-body .card-name{white-space:nowrap}
   ниже). calc(100vw - 24px) — единственный предел, и это не декоративное
   число, а тот же зазор (margin:12px с каждой стороны), которым
   positionHotspotPopover() в site.js зажимает попап в границах окна —
   иначе на узком экране очень длинное название всё равно вытолкнуло бы
   попап за пределы страницы. */
/* border — то же обрамление, что и у карточек (.card, border:1px solid
   var(--line)) — раньше попап держался только на тени, без явной обводки;
   теперь есть свой цвет, который переиспользуют линия-коннектор и подсветка
   хотспота ниже — единая "нить" от точки до попапа одного цвета с его же
   рамкой. */
.hspop-box{position:relative;max-width:calc(100vw - 24px);border:1px solid var(--line);border-radius:var(--r);background:var(--bg-card);box-shadow:0 20px 46px color-mix(in srgb, var(--shadow-black) 20%, transparent);overflow:hidden}
.hspop-close{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;background:color-mix(in srgb, var(--surface) 92%, transparent);border:1px solid var(--line-2);font-size:15px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:3}
.hspop-close:hover{background:var(--surface)}

/* Переключение между разными хотпоинтами, пока попап уже открыт — сперва
   гасим текущее содержимое (.switching), затем (см. showHotspotCard() в
   site.js) подменяем DOM и включаем обратно, чтобы старая карточка
   успевала исчезнуть, а не подменялась мгновенно. */
.hspop-mount{transition:opacity .08s ease}
.hspop.switching .hspop-mount{opacity:0}

/* ── ДЕСКТОП — .popup: свотч слева + текст справа (CTA прижата к низу
   текстовой колонки через margin-top:auto). ── */
.popup{display:flex}
.popup[data-clickable]{cursor:pointer}
.popup-swatch{width:120px;height:120px;min-width:120px;aspect-ratio:1/1;overflow:hidden;background:var(--bg-3)}
.popup-swatch img{width:100%;height:100%;object-fit:cover;display:block}
/* Отступы/margin приведены 1:1 к прототипу (redesign-v2.css, .rdv2-popup-
   body/.rdv2-sheet-text) — раньше здесь были другие числа (16px 18px вместо
   5px 10px и т.д.), из-за чего боевой попап визуально отличался от
   dev-preview-cards.php (найденный баг). */
/* padding-right больше остальных — резерв под .hspop-close (26px кнопка +
   8px отступ от края = 34px), иначе длинное однострочное название (см.
   white-space:nowrap ниже) заезжало под крестик. */
.popup-body{padding:5px 34px 5px 10px;flex:1;min-width:0;display:flex;flex-direction:column}
.popup-body .card-name{font-size:16px;margin-bottom:2px;white-space:nowrap}
.popup-body .card-subtitle{margin:-4px 0 0}
/* white-space:nowrap — НАЙДЕНО И ИСПРАВЛЕНО: без него браузер при расчёте
   "естественной" ширины попапа (shrink-to-fit) считал себя вправе перенести
   текст ссылки на 2 строки вместо того, чтобы посчитать её как часть
   максимальной ширины контента — JS-замер (site.js::positionHotspotPopover())
   получал ширину БЕЗ учёта полной ссылки, и уже показанный попап "донашивал"
   недостающую ширину следующим кадром (тот самый рывок/расширение). Теперь
   ссылка всегда учитывается в расчёте с самого начала. */
.popup-body .cta-link{margin-top:auto;align-self:flex-start;display:inline-block;white-space:nowrap}

/* ── МОБИЛЬНЫЙ — bottom sheet. .sheet-* скрыты по умолчанию (видны только
   ≤560px ниже), .hspop-box на десктопе остаётся обычной плавающей
   карточкой — свою позицию/форму меняет только в мобильном медиа-запросе. ── */
.sheet-handle{display:none}
.sheet-row{display:none}
.sheet-footer{display:none}

@media(max-width:560px){
  /* На мобильном анимирует не сам #hspop (fade+scale), а .hspop-box —
     slide-up снизу экрана (transform:translateY), поэтому у #hspop
     scale-анимация здесь отключена, чтобы не дублировать движение. */
  .hspop,.hspop.show{transform:none}
  .hspop-box{
    position:fixed;left:0;right:0;bottom:0;top:auto;max-width:none;
    border-radius:var(--r-panel) var(--r-panel) 0 0;padding:10px 20px 22px;
    transform:translateY(100%);transition:transform .22s ease;
    touch-action:none; /* см. enableHotspotSheetSwipeClose() в site.js — без этого браузер может начать скроллить страницу под пальцем ДО того, как JS успеет preventDefault() */
  }
  .hspop.show .hspop-box{transform:translateY(0)}
  .hspop-close{top:12px;right:16px}

  .popup{display:none}
  .sheet-handle{display:block;width:36px;height:4px;background:var(--line);border-radius:2px;margin:0 auto 14px}
  .sheet-row{display:flex;align-items:center;gap:14px}
  .sheet-row[data-clickable]{cursor:pointer}
  .sheet-swatch{width:100px;height:100px;min-width:100px;aspect-ratio:1/1;border-radius:var(--r);overflow:hidden;background:var(--bg-3)}
  .sheet-swatch img{width:100%;height:100%;object-fit:cover;display:block}
  .sheet-text{min-width:0;align-self:center}
  .sheet-footer{display:block;margin-top:16px;padding-top:14px;border-top:1px solid var(--line)}
}

@media(prefers-reduced-motion:reduce){
  .hspop,.hspop-box,.hspop-scrim,.hspop-mount{transition:none}
}

@media(max-width:980px){
  .areas-inner{grid-template-columns:1fr;gap:30px}
  .foot-top{grid-template-columns:1fr 1fr;gap:30px}
  .nav-mid{display:none}
  .nicon{display:none} /* кнопка-телефон в navbar — по умолчанию скрыта, не помещается в узкой шапке (запрос 2026-08-12) */
  .brand-logo-desktop-only{display:none}
  .brand-logo-mobile-only{display:block}
  /* Кнопка звонка и CTA "Get a Free Quote" скрыты на мобильном целиком
     (2026-08-18, по прямому запросу) — остаётся только гамбургер: звонок/
     заявку теперь так же легко найти через сам сайт (FCTA-секции на каждой
     странице, на главной ещё и hero-CTA + выпадающее меню WhatsApp), а
     узкой шапке эти два элемента больше не нужны. Раньше кнопка звонка
     возвращалась на мобильный, если был загружен компактный мобильный
     логотип (.nav.has-mobile-logo .nicon{display:flex}) — правило снято,
     теперь .nicon скрыт на мобильном независимо от логотипа. */
  .nquote{display:none}
  .nav-signin{display:none} /* на мобильном пункт Sign In скрыт целиком (по прямому запросу) — раньше дублировался ссылкой в .nmobile, см. header.php */
  .nburger{display:flex}
  .pgrid{grid-template-columns:repeat(2,1fr)}
  .mgrid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:620px){
  .foot-top{grid-template-columns:1fr}
}
/* НАЙДЕНО И ИСПРАВЛЕНО: .pgrid/.mgrid схлопывались в 1 колонку на ≤620px,
   а компактный мобильный вид самих карточек (mcard/pcard — фото 1:1 +
   пилюли) включается только на ≤560px (см. media-блоки выше) — в разрыве
   561-620px получалась одна широкая колонка с ПОЛНОРАЗМЕРНОЙ десктопной
   карточкой (580px высоты), а не квадратной. Брейкпоинт сведён к тому же
   560px, что и у карточек — разрыва больше нет. */
@media(max-width:560px){
  .pgrid{grid-template-columns:1fr}
  .mgrid{grid-template-columns:1fr}
}

/* ── ПЛАВАЮЩИЙ СТЕК КНОПОК (чат + "назад") — общий для ВСЕХ публичных
   страниц (2026-08-19, по прямому запросу "добавь кнопку меню чата на всех
   остальных публичных страницах"), разметка — includes/footer.php. Раньше
   .wa-fab-* жили только в assets/homepage.css и печатались только на
   index.php — перенесено сюда, т.к. shared.css подключён на каждой
   странице, а homepage.css — только на главной. JS (initWaFab()/
   initBackFab()) теперь в site.js, тоже общий, не в index.php.
   .fab-stack — единый fixed-якорь в правом нижнем углу; .wa-fab-root внутри
   него больше НЕ position:fixed сам по себе (обычный flex-элемент стека),
   .back-fab — второй элемент, ниже кнопки чата (flex-direction:column,
   порядок в DOM = визуальный порядок сверху вниз, по прямому запросу
   "разместить ниже кнопки чата"). */
.fab-stack{position:fixed;right:20px;bottom:calc(20px + env(safe-area-inset-bottom));z-index:200;display:flex;flex-direction:column;align-items:flex-end;gap:12px}

.wa-fab-root{position:relative}
.wa-fab{display:inline-flex;align-items:center;gap:10px;min-height:52px;padding:0 22px;border-radius:99px;background:var(--accent);border:1px solid var(--accent-deep);color:var(--text-on-accent);font-family:var(--body);font-size:15px;font-weight:600;box-shadow:0 0 10px color-mix(in srgb, var(--overlay-dark) 55%, transparent),0 6px 10px color-mix(in srgb, var(--overlay-dark) 50%, transparent);cursor:pointer}
.wa-fab:hover{background:var(--accent-deep)}
.wa-fab-ico{font-size:17px}
.wa-fab-menu{position:absolute;right:0;bottom:calc(100% + 10px);display:flex;flex-direction:column;align-items:flex-end;gap:10px}
/* НАЙДЕНО И ИСПРАВЛЕНО: display:flex выше побеждал UA-правило [hidden]{display:
   none} (авторские стили всегда сильнее UA независимо от специфичности) —
   меню было видно по умолчанию, хотя атрибут hidden стоял в разметке. Явно
   возвращаем display:none, когда атрибут есть — та же специфичность
   класса, но идёт позже в каскаде (побеждает по source order). */
.wa-fab-menu[hidden]{display:none}
.wa-fab-item{display:flex;align-items:center;justify-content:center;width:60px;height:60px;flex:0 0 auto;color:var(--ink)}
.wa-fab-item .wa-fab-ico{display:inline-block;font-size:34px;filter:drop-shadow(0 3px 8px color-mix(in srgb, var(--overlay-dark) 35%, transparent));transition:transform .15s ease}
.wa-fab-item:hover .wa-fab-ico{color:var(--accent);transform:scale(1.18)}
.wa-fab-item-img{width:56px;height:56px;object-fit:contain;display:block;filter:drop-shadow(0 3px 8px color-mix(in srgb, var(--overlay-dark) 30%, transparent));transition:transform .15s ease}
.wa-fab-item:hover .wa-fab-item-img{transform:scale(1.18)}

@keyframes waFabPop{
  0%{opacity:0;transform:scale(0)}
  60%{opacity:1;transform:scale(1.15)}
  100%{opacity:1;transform:scale(1)}
}
@keyframes waFabPopOut{
  0%{opacity:1;transform:scale(1)}
  100%{opacity:0;transform:scale(0)}
}
.wa-fab-menu--open .wa-fab-item{animation:waFabPop .46s cubic-bezier(.25,.7,.4,1) both}
.wa-fab-menu--open .wa-fab-item:nth-last-child(1){animation-delay:0ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(2){animation-delay:40ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(3){animation-delay:80ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(4){animation-delay:120ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(5){animation-delay:160ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(6){animation-delay:200ms}
.wa-fab-menu--open .wa-fab-item:nth-last-child(n+7){animation-delay:240ms}
.wa-fab-menu--closing .wa-fab-item{animation:waFabPopOut .3s ease-in both}
.wa-fab-menu--closing .wa-fab-item:nth-child(1){animation-delay:0ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(2){animation-delay:40ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(3){animation-delay:80ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(4){animation-delay:120ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(5){animation-delay:160ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(6){animation-delay:200ms}
.wa-fab-menu--closing .wa-fab-item:nth-child(n+7){animation-delay:240ms}
@media(prefers-reduced-motion:reduce){
  .wa-fab-menu--open .wa-fab-item,
  .wa-fab-menu--closing .wa-fab-item{animation:none}
}

/* Кнопка "назад" (2026-08-19, по прямому запросу) — под кнопкой чата, в том
   же .fab-stack, той же акцентной заливкой, что и .wa-fab (уточнение
   2026-08-19, "backFab сделай акцентную" — раньше акцент был только на
   hover, нейтральный фон в состоянии покоя). history.back() восстанавливает
   и URL, и прокрутку страницы средствами самого браузера (нативный scroll
   restoration) — без ручного сохранения позиции в JS, см. initBackFab() в
   site.js. Скрыта по умолчанию (атрибут hidden в разметке footer.php) — JS
   показывает её, только если есть куда возвращаться (referrer того же
   домена + history.length > 1), чтобы не обещать "назад" в никуда. */
/* Пилюля с иконкой+подписью (2026-08-24, по прямому запросу — раньше была
   круглая кнопка-иконка без текста, тот же переход, что уже сделан для
   .wa-fab). Высота 40px — по отдельному уточнению (сначала стояла 52px,
   как у .wa-fab, затем попросили уменьшить именно эту кнопку). Тень —
   добавлен второй, безофсетный слой (0 0 20px) поверх уже бывшего
   направленного (0 6px 20px) — по прямому запросу "тень вокруг", не только
   снизу; тот же второй слой добавлен и в .wa-fab ниже. */
.back-fab{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 18px 0 14px;border-radius:99px;background:var(--accent);border:1px solid var(--accent-deep);color:var(--text-on-accent);font-family:var(--body);font-size:15px;font-weight:600;box-shadow:0 0 10px color-mix(in srgb, var(--overlay-dark) 55%, transparent),0 6px 10px color-mix(in srgb, var(--overlay-dark) 50%, transparent);cursor:pointer;transition:.15s}
.back-fab:hover{background:var(--accent-deep)}
.back-fab[hidden]{display:none}
.back-fab-ico{display:inline-flex;font-size:18px}

@media(max-width:560px){
  .fab-stack{right:14px;bottom:calc(14px + env(safe-area-inset-bottom));gap:10px}
  .wa-fab-label{display:none} /* только иконка на мобильном — экономия места */
  .wa-fab{padding:0;width:52px;justify-content:center}
  .wa-fab .wa-fab-ico{font-size:36px} /* крупнее (2026-08-24, по прямому запросу — сначала 26px, затем ещё крупнее до 36px) — на десктопе рядом есть текст "Chat with us", тут иконка одна на всю кнопку 52px, мелкой (17px, дефолт) выглядела потерянной */
  .wa-fab-item{width:50px;height:50px}
  .wa-fab-item .wa-fab-ico{font-size:28px}
  .wa-fab-item-img{width:46px;height:46px}
}
