/* ==========================================================================
   Сайт-приглашение. База: шрифты, переменные, сброс, типографика, ритм секций.
   Оформление блоков — ниже по мере вёрстки Фазы 3.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Шрифты. Самохостинг, без внешних CDN.
   Кириллица и латиница объявлены раздельно с unicode-range: браузер скачивает
   только те сабсеты, которые реально встретились на странице.
   font-display: swap — текст виден сразу, ещё до загрузки гарнитуры.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Lora';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/lora-cyrillic-400-normal.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Lora';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/lora-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2070-209F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Lora';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/lora-cyrillic-500-normal.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Lora';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/lora-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2070-209F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Pompadur — рукописная гарнитура от заказчиков. Одним файлом, без разделения на
   сабсеты: в ней всего 92 глифа, а латиницы по сути нет — только I, V, X. Делить
   такую надвое нечего, весь файл весит 20 КБ.

   ВНИМАНИЕ: нет заглавных Ъ, Ы, Ь и нет знаков « » — – ·. Набирать ею капслоком
   нельзя: буква пропадёт молча, без ошибки. Ограничитель — в правиле `.accent`. */
@font-face {
  font-family: 'Pompadur';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/pompadur-400-normal.woff2') format('woff2');
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-cyrillic-400-normal.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2070-209F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/inter-cyrillic-500-normal.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/inter-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2070-209F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   Переменные
   -------------------------------------------------------------------------- */

:root {
  /* PALETTE:START — активный вариант оформления.
     Содержимое между маркерами целиком заменяется файлом из
     assets/variants/<вариант>/palette.css. Состав ролей менять нельзя: на них
     завязана вся вёрстка, переименование потребовало бы прохода по всем блокам.
     Цветовые литералы допустимы ТОЛЬКО здесь — остальной файл работает на ролях.

     Сейчас активен: blush (розоватая пудра, по assets/source/palettes/2.jpg),
     но с 2026-08-06 (T082+T063) второй акцентный цвет (розовый) снят: снимки
     стали чёрно-белыми, и золото (--m-gold, MATERIAL ниже) остаётся
     ЕДИНСТВЕННЫМ цветом страницы — роли палитры пересчитаны в его сторону
     (тёплый кремовый + графит + приглушённое золото вместо розового), состав
     ролей не менялся. --c-ink — тёплый графит по решению заказчиков (было
     коричневым #43302a). Контраст (WCAG, посчитан по формуле относительной
     яркости, не на глаз) на обоих фонах страницы — --c-bg и --m-glass
     (стекло реплик, см. MATERIAL): --c-ink 12.06:1 / 10.11:1 · --c-muted
     6.03:1 / 5.05:1 · --c-accent-deep (текстовая роль — ссылки, .accent)
     6.84:1 / 5.73:1 — все ≥ 4.5:1. --c-accent и --c-line — нетекстовая
     графика (рамки, обводки), порог 3:1: 3.97:1 / 3.33:1 и 3.67:1 / 3.08:1.
     --m-gold на обоих фонах см. в комментарии MATERIAL — это единственная
     пара, которая порог не держит на стекле (держит на странице, см. ниже).

     2026-08-06: заказчики попросили «золото чуть ярче». Осветление подняло
     бы яркость ценой контраста (светлее = ближе к светлому --c-bg), поэтому
     --c-accent/--c-accent-deep (и --m-gold в MATERIAL) сдвинуты по
     насыщенности вверх при той же или чуть меньшей светлоте — золото
     читается звонче, а контраст не упал, а вырос: --c-accent 3.68→3.97 (bg),
     3.09→3.33 (glass); --c-accent-deep 5.96→6.84 (bg), 4.99→5.73 (glass). */
  --c-bg: #f9f5ef;
  --c-bg-soft: #ece2d2;
  --c-ink: #33302e;
  --c-muted: #655c53;
  --c-accent: #9b7322;
  --c-accent-deep: #734e12;
  --c-line: #887e70;
  /* Правка заказчиков 2026-08-06: сердце календаря (#date, календарь
     торжества) — «матовый красный», а не золото, единственное сознательное
     отступление от D11 «золото — единственный цвет страницы»
     (memory-bank/resources/decisions.md), по прямой просьбе заказчиков.
     Матовость — тот же принцип, что у золота (MATERIAL ниже, tools/
     gold_extract.py MATTE/GAIN): не яркий чистый тон, а сжатая по
     насыщенности и притемнённая «краска». Источник — цвет листа сердец
     8310.eps #AC1F40 (H 346°, S 0,69, L 0,40): насыщенность ×0,7, светлота
     -0,03, финальный гейн ×0,9 — тот же путь «сжать разброс + притемнить»,
     что и у золота, только без второго шага (нет диапазона пикселей для
     сжатия к телу цвета — это плоская CSS-роль, не растр). Контраст к
     стеклу реплик (--m-glass) 7.04:1, к фону страницы (--c-bg) 8.40:1 —
     далеко выше порога 3:1 для нетекстовой графики (сердце decorative,
     aria-hidden). */
  --c-heart: #7e2b3f;
  /* T118 (замечание пользователя 2026-08-12): плавающая кнопка «К анкете»
     (.rsvp-float) — единственное место, где фон-плашка одновременно (а) на
     ней лежит настоящий читаемый текст (кегль ниже порога WCAG «крупного
     текста») и (б) требуется «золотой, а не тёмный» цвет для привлечения
     внимания. Ни --c-accent (3.97:1 с --c-bg — держит только 3:1-порог для
     нетекстовой графики), ни --c-accent-deep (это и есть тот «тёмный»
     цвет, который заказчик просит заменить) не годятся одновременно. Токен
     — точка на прямой между --c-accent-deep (#734e12) и --c-accent
     (#9b7322) в сторону более светлого/золотого, до которой контраст с
     --c-bg ещё держит 4.5:1 с запасом (расчёт — WCAG относительная
     яркость, не на глаз): 4.84:1. Заметно золотистее --c-accent-deep,
     заметно контрастнее --c-accent — роль узкая (только эта кнопка), не
     общая замена ни одному из двух базовых токенов. */
  --c-cta-gold: #8c651b;
  /* PALETTE:END */

  /* MATERIAL:START — цвета материала: не роли палитры, вариантом не подменяются.
     Роли палитры выше подменяются целиком при смене варианта оформления (три
     тёплых варианта). Голубое стекло и золото рамы зеркала — не роль оформления,
     а свойство самого декоративного растра: золотой орнамент (вензели, разделители)
     напечатан один раз и одинаков во всех вариантах, а стекло — его неизменная пара.
     Значит этот дуэт обязан пережить смену палитры, поэтому вынесен отдельным блоком.
     --m-gold замерен как медиана непрозрачных пикселей готового орнамента
     (site/img/decor/divider.webp) — так рисованная CSS-линия рамы и растровый
     вензель сходятся в одном тоне, а не расходятся на глаз.

     --m-glass (T082, решение заказчиков 2026-08-06): голубое стекло было
     вторым цветовым акцентом наравне с золотом — рядом с чёрно-белой съёмкой
     оно спорило со снимками и с золотом. Уведено в тёплый серый, почти
     бумажный (близко к --c-bg, но заметно отличимо — иначе реплики перестали
     бы читаться стеклом на фоне страницы, bg↔glass 1,19:1 отличия по яркости
     при том же оттенке). Текстовые роли на нём держат 4.5:1 (см. PALETTE
     выше), --c-accent/--c-line — 3:1.

     2026-08-06, та же правка «золото чуть ярче»: --m-gold сдвинут по той же
     насыщенности вверх при чуть меньшей светлоте (см. PALETTE-комментарий про
     путь через насыщенность, не осветление). На --c-bg это неожиданно
     ЗАКРЫЛО старый долг — было 2,90:1 (ниже порога 3:1 для нетекстовой
     графики), стало 3,03:1. На --m-glass контраст тоже вырос (2,43:1 → 2,54:1),
     но порог 3:1 по-прежнему не держит — известное ограничение: стекло не
     может стать значительно темнее бумаги и остаться «почти бумажным», и это
     не менялось этой правкой, только слегка смягчилось. */
  --m-glass: #e6e2d5;
  --m-gold: #ac8939;
  /* Светлая кромка блика на стекле — тоже свойство материала, не палитры:
     блик должен оставаться светлее заливки одинаково во всех вариантах
     оформления. Вынесена сюда, а не литералом внутри градиента. */
  --m-sheen: #ffffff;
  /* Бумага скрапбук-фото (T078) — тоже материал, не роль оформления: лист
     остаётся тем же бумажным оттенком независимо от тёплого варианта
     палитры (как и стекло/золото зеркала выше). Чистый белый заказчики
     забраковали отдельно («рамка плохо заметна, она белая») — сдвинут в
     серую сторону, чтобы читаться на светлом `--c-bg`. */
  --m-paper: #e4e0d8;

  /* Свет заката на тиснении (T122, пожелание пользователя 2026-08-13: «как
     будто лучи солнца, но более мягкие»). Два тона, а не один: у низкого
     солнца прямой луч янтарный, а рассеянный по краю тени уходит в розовое —
     на этом различии тень и читается закатной, а не просто коричневой.
     Почему тона живут в ТЕНИ, а не в блике — причина НЕ в яркости, а в
     различимости; см. замер в комментарии к --m-emboss ниже. */
  --m-sunset: #b4713a;
  --m-sunset-deep: #9d5a44;
  /* Блик тиснения. Тёплый белый, а не чистый: чистый был единственным
     ахроматичным пятном страницы (фон H36°, стекло H46°, золото H42° — всё
     тёплое). Разница по различимости ничтожна (1.53 против 1.58 к фону),
     по попиксельному просчёту — не больше 8 из 255, то есть глазом её нет;
     берётся ради связности палитры, а не ради вида. */
  --m-emboss-light: #fffaf0;

  /* Тиснение золотого декора — свойство материала, а не оформления: венок
     обложки, вензели зеркал и разделители обязаны быть выдавлены одинаково.
     Раньше рецепт стоял литералом в трёх местах — правка «сделай ярче»
     каждый раз требовала трёх одинаковых редактирований, и один из трёх
     однажды отставал. Здесь один источник.

     T106 + T117: тиснение — это ОРЕОЛ, а не обводка. Разница вся в размытии:
     узкий и плотный drop-shadow вплотную к контуру читается как нарисованная
     кромка, широкий и слабый — как свечение. Первый заход T117 ошибся именно
     здесь (0.06rem/100% + холодная кромка) — вышла жирная обводка, и декор
     распался на «фигура + линия вокруг» («бросается в глаза и выглядит не
     цельно», 2026-08-12). Яркость набирается СЛОЯМИ, а не плотностью одного
     узкого: плотность при малом размытии и превращает свечение обратно в
     обводку. Холодная кромка (проба «серебряного свечения») убрана — вторая
     контрастная линия у контура ломала цельность сильнее всего.

     T122, замер 2026-08-13: симметричный ореол читался как «свечение», а не
     как тиснение — источника света у него нет, и глазу не за что зацепиться.
     Свет теперь идёт сверху слева, как принято в печати и как уже падает
     тень страницы:
     1) блик — со смещением вверх-влево, узким слоем; это НЕ обводка: он
        смещён, а значит ложится только на одну сторону контура;
     2) широкий разлёт остаётся симметричным (рассеянный свет комнаты, у него
        направления нет) — он и держит мягкость, не давая блику стать кромкой;
     3) тень — вниз-вправо, ровно напротив блика.
     Смещения намеренно КРОШЕЧНЫЕ (0.05rem ≈ 0.8 px): орнамент — тонкая
     филигрань, при заметном сдвиге блик отрывается от контура и читается
     вторым, призрачным орнаментом рядом.

     ВАЖНО ПРО ФОН — и это ловушка, на которой разбор 2026-08-13 споткнулся
     дважды. Декор лежит НЕ на кремовой бумаге. Под всей страницей стоит
     .page-bg — фотография рассвета, `position: fixed`, `z-index: -2`, под
     матовым слоем --c-bg всего 40 % (см. комментарий у .page-bg). Отрицательный
     z-index кладёт её ПОВЕРХ фона body, поэтому обход предков по
     background-color её не видит: она не предок, а фиксированный сосед. Первый
     замер именно так и ошибся — насчитал «кремовую бумагу #f9f5ef, запас до
     белого 6/10/16» и вывел из этого, что закатные тона в блик поставить
     нельзя. Проверять фон под элементом — композитом, а не деревом предков.
     Настоящая поверхность (просчёт фото под матовым слоем): медиана RGB
     193/156/131, запас до белого 62/99/124 — в десять с лишним раз больше.
     Из-за `fixed` она ещё и ЕДЕТ под декором при прокрутке: яркость участка
     гуляет от 0.14 до 0.97 по луминансу. Один рецепт неизбежно ложится на
     разное — отсюда и ощущение «эффект то есть, то нет».
     Почему при этом закат всё же в тени, а блик почти белый: само золото
     отделяется от снимка всего на 1.30-1.93 (ниже порога 3:1 для нетекстовой
     графики) — блик здесь не украшение, а то, что делает филигрань различимой.
     Замер отделяемости блика от фона: белый 1.58-2.25, тёплый белый 1.53-2.17,
     закатный #ffdcae — 1.26-1.78, на светлом небе орнамент начинает
     растворяться в рассвете. Тёплый белый стоит столько же, сколько чистый, —
     его и берём (--m-emboss-light).
     Тень двухслойная: плотный янтарь вплотную к контуру и мягкий розовый
     разлёт шире и ниже — так свет обтекает рельеф, а не кладёт под него одно
     ровное пятно. Прежняя тень цвета --c-ink убрана: нейтрально-коричневая,
     она годилась для «объёма вообще», но закатного тона в ней нет.

     ⚠️ ПОРЯДОК СЛОЁВ ЗДЕСЬ — НЕ КОСМЕТИКА. Цепочка drop-shadow каскадная:
     каждый следующий слой отбрасывает тень от РЕЗУЛЬТАТА предыдущего, а не от
     исходной картинки. Пока закатные слои стояли последними, тень отбрасывал
     не орнамент, а орнамент, уже обёрнутый белым ореолом в 0.95rem: тёплый
     тон уезжал наружу и размазывался в розовую дымку по фону, а у контура
     оставалось белое свечение — пользователь так и сказал, «не вижу
     изменений, белый присутствует». Просчёт четырёх вариантов попиксельно
     (tools scratch, 2026-08-13) показал разницу с исходником: направление —
     max 11 по каналу, закат последними слоями — 26, закат ПЕРВЫМИ — 84.
     Поэтому: сначала обе закатные тени (их отбрасывает чистый силуэт, они
     ложатся вплотную), блик — последним, поверх.
     Широкий белый разлёт 0.95rem убран совсем: он размывал контур сильнее,
     чем добавлял света, и именно он разбавлял закат. */
  --m-emboss:
    drop-shadow(0.05rem 0.12rem 0.28rem color-mix(in srgb, var(--m-sunset) 38%, transparent))
    drop-shadow(0.09rem 0.22rem 0.7rem color-mix(in srgb, var(--m-sunset-deep) 26%, transparent))
    drop-shadow(-0.05rem -0.05rem 0.28rem color-mix(in srgb, var(--m-emboss-light) 85%, transparent));
  /* MATERIAL:END */

  --font-display: 'Lora', 'Times New Roman', Georgia, serif;
  /* Рукописная — от заказчиков. Запасные системные скрипты кириллицы почти не
     держат, поэтому последним стоит наш же антиквенный, а не cursive. */
  --font-script: 'Pompadur', 'Lora', cursive;
  /* Основной текст (заказчики, минималистичная эстетика): та же антиква, что
     и в заголовках, а не гротеск — страница должна читаться как единый
     печатный лист, а не как интерфейс с вкраплениями декора. Роль раньше
     называлась --font-body и указывала на Inter — имя стало враньём в
     тот момент, когда решили набирать прозу антиквой, поэтому переименована
     под новое назначение. */
  --font-text: 'Lora', 'Times New Roman', Georgia, serif;
  /* Интерфейсная роль — там, где важна чёткость, а не эстетика: кнопки, поля
     и подписи анкеты, время в программе дня, адреса, цифры отсчёта, мелкие
     служебные подписи. Прежний --font-body занимал эту нишу неявно (был по
     умолчанию для всего текста) — теперь роль явная и уже, а не роль
     умолчания. */
  --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Плавная типографика: размеры тянутся между 320 px и десктопом,
     поэтому промежуточных брейкпоинтов под текст не нужно.

     Cormorant Garamond был заменён на Lora по решению заказчиков 2026-08-05
     («шрифт резкий и острый»): высокий контраст штриха и острые клиновидные
     засечки — свойство рисунка, настройками не смягчается. У Lora засечки
     мягче, контраст спокойнее — но и x-height заметно больше, чем у
     Cormorant (замер canvas TextMetrics, кегль 100px: x-height Cormorant
     400 ≈ 0,40 кегля, Lora 400 ≈ 0,50, Inter 400 ≈ 0,56). Раньше кегли ниже
     были подняты примерно на треть, чтобы компенсировать МЕЛКИЕ строчные
     Cormorant — теперь строчные у Lora сами по себе крупнее, и та же
     инфляция кегля дала бы страницу заметно КРУПНЕЕ прежней, то есть ровно
     то, на что заказчики жаловались изначально («шрифт большой»). Поэтому
     все три роли ниже (--fs-hero/h2/h3/body) умножены на 0,40/0,50 = 0,8 —
     во столько раз у Lora крупнее x-height по сравнению с Cormorant.
     Итог: видимая высота строчных (px) остаётся такой же, как была на
     странице, одобренной заказчиками по масштабу, — меняется гарнитура,
     а не размер. --fs-ui (Inter) в этот пересчёт не входит: метрика Inter
     не менялась. */
  --fs-hero: clamp(1.52rem, 0.42rem + 5.48vw, 4rem);
  --fs-h2: clamp(1.18rem, 0.66rem + 2.6vw, 2.4rem);
  --fs-h3: clamp(0.9rem, 0.66rem + 1.18vw, 1.4rem);
  --fs-body: clamp(1.08rem, 0.96rem + 0.32vw, 1.28rem);
  /* Кегль для Inter-текста НЕ на службе у --fs-body: та формула компенсирует
     маленький x-height антиквы (см. комментарий выше) и в кегле Inter дала бы
     противоположный эффект — адрес/факты стали бы КРУПНЕЕ, чем были, а не
     тише. --fs-ui — прежняя формула --fs-body (Inter не менял метрику),
     уменьшается только вместе с общим плавным --root (html, ниже), как и
     весь остальной интерфейс. */
  --fs-ui: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fs-small: clamp(0.95rem, 0.88rem + 0.2vw, 1.05rem);

  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 1.75rem;
  --space-lg: 3rem;
  --space-xl: clamp(3.5rem, 2rem + 7vw, 6.5rem);

  /* Заказчики 2026-08-06: «фотка маленькая по сравнению с экраном — экран
     можно чуть сузить, а фотку увеличить». --measure сузили с 34rem до
     30rem (порядка 65 знаков в строке — ещё в комфортном диапазоне, ниже
     30rem чтение начинает сбиваться, дальше не уменьшать). --container
     расширен с 60rem до 68rem, вся прибавка (8rem) уходит фотографии —
     колонка переписки как была заперта на полном --measure, так и осталась.
     Потолок для фото задан исходниками (couple-heart 959px, couple-studio
     1280px по длинной стороне): слот шире ~480px на экране с удвоенной
     плотностью потребовал бы апскейла — фото начнёт мылиться. */
  --measure: 30rem; /* комфортная длина строки */
  --container: 76rem;
  --radius: 2px;
}

/* --------------------------------------------------------------------------
   Сброс
   -------------------------------------------------------------------------- */

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

* {
  margin: 0;
}

/* Атрибут `hidden` обязан побеждать любой наш `display`. В стилях браузера он
   объявлен как `[hidden] { display: none }` — правило с меньшей специфичностью,
   чем `.chat__turn { display: grid }`, поэтому без этой страховки скрытая
   реплика анкеты молча остаётся на экране, и пошаговое раскрытие не работает. */
[hidden] {
  display: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Базовый размер — плавный (заказчики: «сам экран можно сделать меньше и
     масштабировать его вместе с экраном»). Всё, что считается в rem (кегли,
     --space-*, --measure, --container), уезжает согласованно вместе с этой
     величиной — раньше она была жёстко 16px, и только отдельные --fs-*
     тянулись сами по себе, а отступы и ширина колонки стояли колом.
     Проценты, а не px: значение остаётся относительным к настройке размера
     шрифта в браузере гостя, а не переопределяет её. Диапазон 88–92%
     (12% меньше прежнего на 320px, 8% на 1280px, между ними — линейно) —
     «заметно» меньше, примерно на десятую часть, как просили. Коэффициент
     при vw посчитан явно (см. отчёт задачи), а не подобран на глаз: 1vw —
     ЕДИНИЦА ДЛИНЫ (сотая доля ширины окна в px), а не сотая доля числа
     процентов слева, и на глаз коэффициент легко промахивается на порядок
     мимо диапазона min/max клампа (проверено эмпирически: 0.42vw держал
     preferred-ветку клампа выше верхней границы на любой ширине из
     диапазона, и на выходе получался не плавный переход, а плоские 92%
     везде). Хочешь пересчитать диапазон — бери целевые PX-значения на двух
     концах диапазона ширин, дели их разницу на разницу ширин в px — это и
     есть коэффициент при vw; свободный член — px-значение на левом конце
     минус (коэффициент × левая ширина), переведённое в проценты от 16px. */
  font-size: clamp(88%, 86.6667% + 0.0667vw, 92%);
}

/* Гость мог отключить анимации в системе — уважаем это. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    /* animation-timeline: none здесь НЕ ставим, хотя соблазн есть: она бы
       разом выключила и прокруточный блик. Но `animation-timeline` не
       различает анимации по природе — она отвязывает от шкалы ЛЮБУЮ, и
       обычная временная анимация повисает в своём первом кадре навсегда.
       Для появления реплик (`chat-turn-in`, первый кадр — opacity: 0) это
       означало вечно невидимый вопрос пары: анимация не кончается, событие
       animationend не приходит, класс не снимается. Блик выключаем адресно —
       правилом сразу под его же объявлением. */
  }
}

body {
  min-height: 100vh;
  background-color: var(--c-bg);
  color: var(--c-ink);
  font-family: var(--font-text);
  font-size: var(--fs-body);
  font-weight: 400;
  /* Было 1.65 — заказчики попросили плотнее; такая же плотная выключка, как
     у заголовков (1.15), для абзацного текста была бы тесной — 1.45 держит
     воздух между строк. При замене Cormorant на Lora (см. --fs-body выше)
     значение перепроверено: у Lora строчные и так крупнее, кегль пересчитан
     вниз пропорционально — на глаз и по замеру строки не сдвинулись теснее
     прежнего, 1.45 оставлен без изменений. */
  line-height: 1.45;
  /* Страница не должна ехать вбок ни при какой ширине (FR-008) */
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* --------------------------------------------------------------------------
   Фон страницы (T074) — фотография рассвета над морем под матовым слоем,
   с медленным смещением по прокрутке. `<div class="page-bg">` — первый
   потомок body (см. index.html), не background у самого body: смещаться
   должен только этот слой (transform), а не вся страница.

   Автор фото — muhammad.abdullah, бесплатная лицензия (реестр [[design-assets]]).
   Исходник 8736x4896/5,5 МБ порезан tools/prepare_background.py на пять WebP
   под брейкпоинты ниже (640/960/1440/1920/2560 px) — под матовым слоем
   детализация не видна, картинка работает пятном, резкость не нужна.
   -------------------------------------------------------------------------- */
.page-bg {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Высота больше вьюпорта РОВНО на величину смещения (--bg-shift ниже):
     слой едет вверх на translateY(-var(--bg-shift)), и без запаса снизу
     обнажился бы край под ним. Запас снизу, не сверху — смещение всегда
     в одну сторону (вверх, вслед за прокруткой). */
  height: calc(100vh + var(--bg-shift));
  z-index: -2;
  background-repeat: no-repeat;
  background-position: center top;
  background-size: cover;
  background-image: url('../img/bg-sunrise-640.webp');
  pointer-events: none;
}

@media (min-width: 40em) {
  .page-bg {
    background-image: url('../img/bg-sunrise-960.webp');
  }
}

@media (min-width: 60em) {
  .page-bg {
    background-image: url('../img/bg-sunrise-1440.webp');
  }
}

@media (min-width: 90em) {
  .page-bg {
    background-image: url('../img/bg-sunrise-1920.webp');
  }
}

@media (min-width: 120em) {
  .page-bg {
    background-image: url('../img/bg-sunrise-2560.webp');
  }
}

/* Матовый светлый полупрозрачный слой поверх фотографии — тот же --c-bg, что
   и у бумаги страниц, смешан с прозрачностью через color-mix (тот же приём,
   что у блика зеркала выше по файлу). Держит контраст текста, который лежит
   прямо на фоне без своей карточки (.cover__names, .cover__date, подпись и
   дата в подвале — последняя набрана .muted, самым низкоконтрастным цветом
   из трёх).

   55% (первая прикидка по луминансу самого тёмного пикселя всего исходника)
   на практике не хватило: снимок страницы и замер РЕАЛЬНОГО фона под подписью
   в подвале (там, где сквозь матовый слой проступает тень волны у горизонта)
   дал контраст .muted к фону ~2.3:1 — исходная прикидка считала теоретический
   глобальный минимум по всей фотографии, а не то, что фактически оказывается
   под конкретным текстом. Поднято по факту (снимками, не расчётом — см. отчёт
   задачи T074) до 88%: под подписью в подвале контраст держал 4.5:1 у .muted.

   T090 (правка 1, решение заказчика 2026-08-07): при 88% фон работал ровным
   кремовым пятном — не читались ни диск солнца, ни дорожка отражения на воде,
   ни гребни волн, а именно ради них выбирали фото. Заказчик увидел риск (при
   низкой плотности контраст .muted в подвале снова падает ниже 4.5:1) и принял
   его осознанно: «скрути просто контраст, не выдумывай велосипед, текст виден
   хорошо» — без локальной подложки под подвалом, без градиентов, без
   backdrop-filter, просто ниже процент. Снижено до 40% (подобрано снимками
   страницы, не расчётом): фон читается в полную силу, текст в подвале
   по-прежнему разборчив на глаз, но контраст .muted к фону НИЖЕ 4.5:1 —
   это принятый размен, а не недосмотр. Смена исходника фона или отступов
   подвала — переснять на глаз, что текст остаётся читаемым; порог 4.5:1
   здесь сознательно не цель.

   Правка заказчика 2026-08-07: и имя, и дата в подвале переведены на
   --c-ink (.footer__names/.footer__date, ниже по файлу) — самую контрастную
   роль палитры, и обе строки одного цвета. Замер снимком по факту (тень
   волны под подписью, как описано выше): --c-ink здесь держит ~2,9-3,9:1 —
   лучше, чем было у .muted (~1,9:1) или --c-accent-deep (~1,7-2,2:1), но
   всё ещё ниже 4,5:1 — тот же принятый размен, что и у .page-bg::after
   выше, не новый недосмотр. */
.page-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background-color: color-mix(in srgb, var(--c-bg) 40%, transparent);
}

/* Смещение — порядка 1/10 полной прокрутки страницы (решение заказчиков:
   «в 100 раз медленнее» на практике неподвижность при высоте страницы в
   несколько тысяч px). Величина подобрана по факту на живой странице, не
   вычислена из описания: замер headless-браузером (2026-08-07) дал полную
   прокрутку ~4150-4300 px (390x844 и 1280x900, до T088/T089/T087 — при их
   реализации высота страницы вырастет, пересверить). 420px — около 1/10 от
   обоих замеров. */
:root {
  --bg-shift: 420px;
}

@keyframes page-bg-parallax {
  from {
    transform: translateY(0);
  }

  to {
    transform: translateY(calc(-1 * var(--bg-shift)));
  }
}

@supports (animation-timeline: scroll()) {
  .page-bg {
    animation: page-bg-parallax linear;
    animation-timeline: scroll(root);
    animation-fill-mode: both;
  }
}

@media (prefers-reduced-motion: reduce) {
  .page-bg {
    animation-name: none;
    transform: none;
  }
}

img,
svg,
picture,
iframe {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   Типографика
   -------------------------------------------------------------------------- */

h1,
h2,
h3 {
  font-family: var(--font-display);
  /* Lora поставляется только в 400 и 500 — начертания 300, каким рисовались
     заголовки у Cormorant, у неё нет. Берём 400: следующее по насыщенности
     после отсутствующего лёгкого, не «жирнее задуманного» 500. */
  font-weight: 400;
  line-height: 1.15;
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-hero);
  letter-spacing: 0.06em;
}

h2 {
  font-size: var(--fs-h2);
  letter-spacing: 0.04em;
}

h3 {
  font-size: var(--fs-h3);
  letter-spacing: 0.02em;
}


p {
  text-wrap: pretty;
}

a {
  color: var(--c-accent-deep);
  text-underline-offset: 0.2em;
}

/* Рукописный акцент: подписи, имена, мелкие подзаголовки.
   `text-transform: none` здесь не косметика, а страховка: в Pompadur нет заглавных
   Ъ, Ы, Ь, и капслок от родителя (например от `.cover__names`) съел бы букву,
   ничего не сообщив. Правило гасит наследование в одном месте. */
.accent {
  font-family: var(--font-script);
  font-weight: 400;
  text-transform: none;
  color: var(--c-accent-deep);
}

.muted {
  color: var(--c-muted);
}

/* Мелкие служебные подписи (согласие на обработку данных, заметка под
   картой) — чёткость важнее эстетики, тот же принцип, что и у кнопок/полей
   (см. --font-ui). Дата в подвале (.footer__date) этот класс больше не
   несёт — правка пользователя 2026-08-07 подтянула её оформление к
   .cover__date (Lora, капслок, разрядка), а не к служебным подписям. */
.small {
  font-family: var(--font-ui);
  font-size: var(--fs-small);
}

/* --------------------------------------------------------------------------
   Раскладка
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-md);
}

.section {
  padding-block: var(--space-xl);
}

/* Зеркало — блок вместо розовых карточек-заметок с уголками. Голубое стекло
   в золочёной раме, сверху и снизу — растровый вензель. Вензели идут обычными
   <img> в потоке (не абсолютным позиционированием): у картинки своя пропорция,
   и в потоке она сама держит место, без расчёта отступов и риска обрезки. */
.mirror {
  display: grid;
  /* Тот же --measure, под который считалась ширина нарезки вензеля (см.
     комментарий к элементу 47 в tools/gold_extract.py: заказ 1580px — это
     тройной запас именно от 34rem максимального показа). Без потолка
     .mirror растягивается на всю ширину .container (60rem) — вензели
     раздуваются, а .chat__ask, ограниченный процентами, ничего не
     ограничивает от такой ширины. */
  max-width: var(--measure);
  margin-inline: auto;
}

.mirror__crest {
  width: 100%;
  height: auto;
  display: block;
  /* T106/T122: тиснение — общий рецепт --m-emboss (MATERIAL), см. его комментарий. */
  filter: var(--m-emboss);
}

/* Вензель нарисован так, что его нижняя (у верхнего) / верхняя (у нижнего)
   кромка — та самая линия, на которой стоит орнамент. Орнамент должен сесть
   НА линию рамы стекла, а не повиснуть над ней зазором — отсюда нахлёст. */
.mirror__crest:first-child {
  /* У верхнего вензеля (crest-top.webp) линия-подставка не совпадает с
     нижней кромкой картинки: ниже линии остаётся ещё 62 px завитков и
     лепестков (из 1580 px ширины кадра) — печатается tools/gold_extract.py
     при нарезке элемента 47. 62 / 1580 = 3,92%. Процент, а не px: margin
     по вертикали считается от ширины содержащего блока, а вензель всегда
     width: 100%, поэтому нахлёст сам масштабируется с зеркалом. Без него
     линия вензеля виснет НАД рамой стекла второй параллельной чертой. */
  --crest-top-overlap: -3.92%;
  margin-bottom: var(--crest-top-overlap);
}

.mirror__crest:last-child {
  margin-top: -1px;
}

.mirror__glass {
  position: relative;
  /* Не overflow: hidden — при нём Chromium (проверено, версия 151) перестаёт
     обновлять animation-timeline: view() у любого потомка (в т.ч. у ::after
     с живым бликом ниже): прогресс замирает на значении первого кадра и
     больше не следует за прокруткой, хотя сам скролл проходит и getBounding-
     ClientRect меняется корректно. Воспроизведено в изолированном стенде:
     стоит добавить overflow: hidden на предка — и view()-таймлайн потомка
     замирает; стоит заменить на clip-path — снова едет. clip-path обрезает
     по тому же контуру (с тем же скруглением, что border-radius ниже), но
     не создаёт этот баг. */
  clip-path: inset(0 round 1.5rem);
  padding: var(--space-lg) var(--space-md);
  background-color: var(--m-glass);
  /* Блик — диагональная светлая полоса поверх заливки: без неё голубой фон
     читается плашкой, а не стеклом. Это статичный запасной вариант — там, где
     браузер понимает animation-timeline: view() (правило ниже), полоса
     вместо этого едет от прокрутки, а этот background-image просто не
     используется.

     2026-08-06, заказчики: «блики видны, но очень слабо» (важно — у них
     отключены анимации в системе, они видят именно этот случай, а не
     бегущую полосу, см. ту же правку у .mirror__glass::after ниже: там же
     причина, почему поднимать нужно ОБЕ версии). Пик полосы был
     color-mix(--m-glass 40%, --m-sheen) — почти не отличался от заливки
     (245 из 255 по светимости, разница с фоном стекла всего ~19 по яркости
     из физически доступных на этом материале ~29 — 40%-примесь стекла топила
     блик в самой заливке). Поднято до 10% примеси стекла — разница выросла
     до ~26 (+33%), это НЕ равно белому наотмашь (чистый --m-sheen без
     примеси давал 255, полностью убирая тёплый тон стекла из блика, —
     проверено, выглядит как настоящая вспышка, а не отсвет, поэтому
     отклонено). Больше 255 не бывает физически (потолок sRGB) — на этом
     материале (светлое стекло, светимость заливки ~226 из 255) разница
     не может быть намного больше: это неизбежный физический потолок, не
     недоработка. Ширина полосы (стопы 30/48/66%) не менялась — усиление
     только через непрозрачность/яркость, широкая полоса превратила бы блик
     в засветку всего стекла. */
  background-image: linear-gradient(
    115deg,
    transparent 30%,
    color-mix(in srgb, var(--m-glass) 10%, var(--m-sheen)) 48%,
    transparent 66%
  );
  border: 1px solid var(--m-gold);
  /* Скругление мягче общего --radius: 2px — зеркало это стекло в раме,
     острый угол ему противоречит. */
  border-radius: 1.5rem;
}

@media (max-width: 30em) {
  .mirror__glass {
    padding-inline: var(--space-sm);
  }
}

/* Живой блик: там, где браузер понимает scroll-driven анимации, полоса едет
   по стеклу, пока зеркало проходит через вьюпорт — привязана к прокрутке
   через animation-timeline, без обработчика scroll на каждый кадр. Полоса
   вынесена в ::after (а не оживляет background-image самого .mirror__glass)
   ради z-index: -1 — слой должен красться под текстом реплики, но над
   заливкой, а у самой .mirror__glass под неё уже занят background-image.

   Раньше слой был шире и выше стекла (inset: -20% -60%) и ехал через
   translateX — так и было исходно задумано, но это ломало геометрию
   прокрутки: clip-path обрезает такой слой только визуально, а его
   layout-box шире родителя (.mirror__glass не overflow: hidden, см.
   комментарий выше) всё равно расширял scrollWidth страницы на десятки
   пикселей на каждом зеркале. Проверено: ::after заперт в границах стекла
   (inset: 0), а едет не сам слой, а фон внутри него — background-size шире
   контейнера, background-position сдвигается по шкале прокрутки.
   Диагональная полоса под углом при таком сдвиге едет параллельно самой
   себе, без искажения (сдвиг position не масштабирует и не скашивает
   картинку) — прежнее опасение было ошибочным. */
@supports (animation-timeline: view()) {
  .mirror__glass {
    background-image: none;
    /* position: relative само по себе НЕ создаёт контекст наложения — для
       z-index: -1 у ::after ниже нужен явный контекст на самом
       .mirror__glass (isolation: isolate), иначе потомок с отрицательным
       z-index уходит в контекст ближайшего предка, создающего его, и
       красится под заливкой .mirror__glass, а не над ней. isolation не
       трогает раскладку/геометрию и не делает элемент содержащим блоком
       для fixed-потомков — в отличие от transform/filter. */
    isolation: isolate;
  }

  .mirror__glass::after {
    content: '';
    position: absolute;
    /* Строго в границах стекла — никакого вклада в scrollWidth родителя. */
    inset: 0;
    background-repeat: no-repeat;
    /* Втрое шире и в полтора раза выше контейнера — запас, чтобы полосе
       было куда ехать (background-position) и чтобы её диагональные концы
       не обрезались по высоте у самых краёв прохода. */
    background-size: 300% 140%;
    /* Стопы сужены обратно пропорционально растяжению background-size: при
       300%-ширине полоса шириной 30% ВИДИМОЙ ширины стекла должна занимать
       10% длины градиента (30% / 300% × 100). Раньше стопы (30%…66%, разброс
       36 пунктов) были унаследованы от нерастянутого фона — на 300%-фоне это
       превращалось в мягкую заливку почти во всю ширину контейнера
       (36 пунктов × 3 = 108% контейнера), а не в узкую полосу.

       Пик градиента (color-mix) поднят той же правкой 2026-08-06, что и у
       статичного запасного варианта выше (.mirror__glass background-image,
       см. комментарий там про числа и физический потолок) — оба места
       делят одну и ту же переменную яркости не случайно: гость с
       prefers-reduced-motion видит именно ЭТУ полосу, замороженную
       background-position ниже, а не статичный запасной вариант — если
       поднять яркость только там, заказчики изменения вообще не увидят. */
    background-image: linear-gradient(
      115deg,
      transparent 43%,
      color-mix(in srgb, var(--m-glass) 10%, var(--m-sheen)) 48%,
      transparent 53%
    );
    /* Красится поверх заливки .mirror__glass, но под её содержимым: за счёт
       isolation: isolate выше .mirror__glass сама создаёт контекст
       наложения, и z-index: -1 остаётся внутри него — между фоном
       родителя и его контентом. */
    z-index: -1;
    pointer-events: none;
    animation: mirror-sheen linear;
    animation-timeline: view();
  }

  /* Гость отключил анимации в системе. Блик не убираем и не «замораживаем» на
     случайном кадре — ставим полосу намеренно: настройка просит убрать
     движение, а не превратить стекло в плоскую заливку.

     52% по горизонтали — полоса чуть левее середины стекла. Шкала едет справа
     налево (см. @keyframes ниже: 10% — полоса за правым краем, 90% — за
     левым), и связь между процентом и местом полосы неочевидна: фон втрое
     шире стекла, поэтому сдвиг равен -2×ширины×процент. Менять число на глаз
     бесполезно — считать надо от этого.

     Правило стоит ЗДЕСЬ, а не в общем блоке prefers-reduced-motion выше по
     файлу: специфичность у обоих селекторов одинаковая, и победил бы тот,
     что ниже по исходнику. */
  @media (prefers-reduced-motion: reduce) {
    .mirror__glass::after {
      animation-name: none;
      background-position: 52% 50%;
    }
  }
}

@keyframes mirror-sheen {
  from {
    /* 50% по вертикали — центрирует запас высоты (140%) симметрично сверху
       и снизу: (100% - 140%) * 50% = -20%, то есть кадр выступает на 20%
       за верх и низ стекла, как и было при inset: -20% -60% раньше. */
    background-position: 10% 50%;
  }

  to {
    background-position: 90% 50%;
  }
}

/* --------------------------------------------------------------------------
   Оживление зеркал — появление при прокрутке и «печать» перед контентом.
   Класс mirror--reveal ставит ТОЛЬКО site/js/mirror.js (и не ставит вовсе
   при prefers-reduced-motion) — без JS или с уважаемой настройкой гость
   видит зеркала сразу, начальное скрытие в разметке не зашито.
   -------------------------------------------------------------------------- */

.mirror--reveal {
  opacity: 0;
  transform: translateY(1.5rem);
  transition:
    opacity 0.6s ease-out,
    transform 0.6s ease-out;
}

.mirror--reveal.mirror--visible {
  opacity: 1;
  transform: none;
}

/* Накладка «печатает…»: создаёт и удаляет её сам mirror.js, в разметке нет
   и следа — гость без JS и скринридер видят содержимое зеркала сразу. */
.mirror__typing {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  background-color: var(--m-glass);
}

.mirror__typing-dot {
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background-color: var(--m-gold);
  opacity: 0.25;
  animation: mirror-typing-blink 0.6s ease-in-out infinite;
}

.mirror__typing-dot:nth-child(2) {
  animation-delay: 0.15s;
}

.mirror__typing-dot:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes mirror-typing-blink {
  0%,
  100% {
    opacity: 0.25;
  }

  50% {
    opacity: 1;
  }
}

/* --c-muted на голубом стекле — 4,04:1, мало для мелкого текста. Роль не
   переопределяем (--c-muted нужен таким на тёплом фоне страницы), а темним
   локально через color-mix — это даёт 5,25:1 без нового цветового литерала. */
.mirror__glass .muted {
  color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
}

/* --------------------------------------------------------------------------
   Служебное
   -------------------------------------------------------------------------- */

/* Видно только скринридеру. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Поле-приманка (FR-017): человек его не видит и не заполняет, бот — заполняет.
   Спрятано так, чтобы не выпадать из потока подозрительно очевидным display: none,
   и убрано из обхода клавиатурой и из чтения скринридером. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--c-accent-deep);
  outline-offset: 3px;
}

.decor {
  margin-inline: auto;
}

.decor--divider {
  display: block;
  width: min(18rem, 60%);
  height: auto;
  /* Центрирование берётся из `.decor` (margin-inline: auto). Свой `margin: 0 auto`
     здесь обнулял бы вертикальные поля и молча гасил `margin-bottom` подвального
     вензеля — оба правила равной специфичности, победа зависела бы от порядка строк. */
  /* T106/T122: тиснение — общий рецепт --m-emboss (MATERIAL), см. его комментарий. */
  filter: var(--m-emboss);
}

/* --------------------------------------------------------------------------
   Кнопки
   -------------------------------------------------------------------------- */

.button {
  display: inline-block;
  /* Горизонтальные поля ужаты с 2.2em: кнопки читались как «слишком большие»
     (замечание пользователя 2026-08-12). Вертикальные НЕ трогаем — они держат
     высоту тач-цели, и её уменьшение выбило бы попадание пальцем. */
  padding: 0.85em 1.5em;
  border: 1px solid transparent;
  /* Не --radius (2px): у страницы есть собственный язык скругления — пузыри
     реплик скруглены на 1.1em, и кнопки с прямыми углами из него выпадали.
     Тот же 1.1em, а не «пилюля»: на приглашении с рукописными гарнитурами
     полностью круглый торец читается как элемент современного интерфейса. */
  border-radius: 1.1em;
  /* Кнопка — интерфейс, не проза: чёткость важнее эстетики (см. --font-ui). */
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}

.button--primary {
  background-color: var(--c-accent-deep);
  border-color: var(--c-accent-deep);
  color: var(--c-bg);
}

.button--primary:hover {
  background-color: var(--c-ink);
  border-color: var(--c-ink);
}

.button--ghost {
  background-color: transparent;
  border-color: var(--c-accent);
  color: var(--c-accent-deep);
}

.button--ghost:hover {
  background-color: var(--c-accent);
  color: var(--c-bg);
}

/* --------------------------------------------------------------------------
   1. Обложка
   -------------------------------------------------------------------------- */

.cover {
  min-height: 100svh;
  display: grid;
  /* Явная колонка, а не auto: auto-трек меряется по содержимому, и длинное
     неразрывное слово растянуло бы его шире вьюпорта. minmax(0, 1fr)
     фиксирует ширину по контейнеру (FR-008). */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  padding-block: var(--space-lg);
}

/* Специфичность 0,2,0 намеренно: правило перебивает `padding-inline` из `.container`,
   с которым делит элемент. При равной специфичности победа зависела бы от порядка
   строк в файле — перестановка блоков молча вернула бы широкие поля. */
.cover .cover__inner {
  padding-inline: var(--space-sm);
}

.cover__inner {
  text-align: center;
  /* У grid-элемента min-width по умолчанию равен min-content: длинное слово
     распёрло бы его шире трека. */
  min-width: 0;
}

/* Венок обложки. Сетка в одну ячейку: венок и имена лежат друг на друге и
   масштабируются вместе, поэтому имена не выезжают из просвета ни на какой ширине.
   Венок идёт целиком, без обрезки: композиция задумана кольцом, отрезанный край
   оставил бы рубленые листья. */
.cover__frame {
  display: grid;
  place-items: center;
  /* От ширины КОНТЕЙНЕРА, а не вьюпорта: контейнер уже съел свои поля, и 92vw
     поверх них вылезали за экран — на 320 px венок обрезало скрытым переполнением
     как раз по краю листьев, ради целостности которых он и берётся целиком. */
  width: min(100%, 34rem);
  margin-inline: auto;
  /* Кегль внутри считаем от ширины венка, а не от вьюпорта: на десктопе венок
     упирается в 34rem и расти перестаёт, а vw продолжал бы — имена вылезли бы
     за просвет на широком экране. */
  container-type: inline-size;
}

.cover__frame > * {
  grid-area: 1 / 1;
}

.cover__frame img {
  width: 100%;
  height: auto;
  /* Тиснение — общий рецепт --m-emboss (MATERIAL), там же весь разбор:
     почему ореол, а не обводка; почему свет идёт сверху слева; почему тон
     живёт в тени, а не в блике; и почему порядок слоёв менять нельзя. */
  filter: var(--m-emboss);
}

.cover__inside {
  /* Просвет внутри венка — около 56 % его ширины. */
  width: 56%;
  text-align: center;
}

.cover__names {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1em;
  /* Имена набраны рукописным Pompadur (см. .cover__name) — гарнитура рассчитана
     на строчные, капслока здесь не будет: заказчики подписывают приглашение
     именно так (assets/source/refs/Пример приглашения + шрифт.jpg). */
  /* Запасной кегль от вьюпорта — на случай, если единицы контейнера не поддержаны.
     Имена — сам заголовок h1, но кегль привязан к ширине венка (cqi ниже), а не к
     --fs-hero, поэтому в общий пересчёт роли --fs-hero не попадает автоматически.
     Множитель 0,8 (см. --fs-body), который здесь раньше компенсировал смену
     Cormorant→Lora, снят по правке заказчика 2026-08-07: имена и дата на макете
     мельче просвета венка, крупный кегль здесь желателен, а не риск. */
  font-size: clamp(0.95rem, 4vw, 1.8rem);
}

.cover__date {
  margin-top: 0.9em;
  /* Множитель 0,8 (как у .cover__names выше) снят по той же правке 2026-08-07. */
  font-size: clamp(0.65rem, 2vw, 0.95rem);
  /* T105: настоящее начертание вместо обычного 400 — файл Lora 500 подключён
     выше (см. @font-face, style.css:34), это не рукописный Pompadur, капслок
     ему не мешает. */
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* «19 СЕНТЯБРЯ 2026, СУББОТА» в узком просвете рвётся на две строки —
     balance выравнивает их по длине, иначе вторая заметно длиннее первой. */
  text-wrap: balance;
  /* Роль --c-muted (самый низкоконтрастный цвет палитры) на осветлённом
     фоне-рассвете (матовый слой снижен до 40%, см. .page-bg::after) гасила
     дату почти до невидимости — правка заказчика 2026-08-07. --c-ink — тот
     же цвет, что у имён выше (наследуется от body), и самый контрастный
     в палитре. */
  color: var(--c-ink);
  /* T097: дату переносить нельзя, см. комментарий у .cover__name. */
  hyphens: none;
}

@supports (width: 1cqi) {
  .cover__names {
    font-size: 6.4cqi;
  }

  .cover__date {
    /* Дата стоит ниже середины венка, а там просвет уже сужается — кегль меньше,
       чем позволяла бы ширина имён. T105: 2.3cqi (~12px при венке 544px) рвал
       текст на мелкие пятна вместе с разрядкой 0.22em — поднято до 3cqi. */
    /* max(), а не голый 3cqi: у cqi-пути нет нижнего предела, а у запасного
       clamp выше он есть. Ниже ~375px они расходились в неправильную сторону —
       на 320px 3cqi давал 8.8px, то есть браузер С поддержкой контейнерных
       единиц показывал дату мельче, чем без неё.
       Пол именно 0.9rem, а не 0.65rem: корневой кегль страницы ОТЗЫВЧИВЫЙ и на
       320px равен 14.08px, поэтому 0.65rem там = 9.15px — прибавка +0.4px,
       которая жалобу на читаемость не лечит. Замер 2026-08-12: коробка даты на
       320px — 163.4px, при 0.9rem (12.67px) самая длинная строка 120.1px,
       запас 43px, две строки. Место есть. */
    font-size: max(0.9rem, 3cqi);
  }
}

.cover__name {
  display: block;
  /* Рукописный Pompadur — как подпись под приглашением заказчиков
     (assets/source/refs/Пример приглашения + шрифт.jpg). Гарнитура рассчитана на
     строчные: text-transform:none страхует от капслока родителя, у Pompadur нет
     заглавных Ъ Ы Ь, и буква пропала бы молча (покрытие для «Владимир»/«Анастасия»
     проверено — обе буквы В и А в гарнитуре есть). */
  font-family: var(--font-script);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  /* T091 (правка заказчиков 2026-08-11, «сделать чётче/жирнее»): у гарнитуры
     только начертание 400 — font-weight:700 дал бы синтетическую
     псевдожирность, которая смазывает тонкие соединения рукописных букв.
     Утолщаем обводкой того же цвета, что заливка (currentColor = --c-ink,
     наследуется от body): paint-order рисует обводку ПОД заливкой, иначе она
     съедала бы внутренние просветы букв. Толщина подобрана снимком — имена
     стоят в узком просвете растрового венка. */
  paint-order: stroke fill;
  -webkit-text-stroke: 0.4px currentColor;
  /* Тень — под вопросом заказчиков («может, добавить?»). Оставлена
     закомментированной: пользователю показаны снимки обоих вариантов (см.
     отчёт задачи T091), решение ещё не принято. */
  /* text-shadow: 0 0.05em 0.1em rgba(51, 48, 46, 0.35); */
  /* T097: имя переносить нельзя («Владими-р» хуже отсутствия переноса) —
     явный запрет, не завязанный на текущее отсутствие hyphens:auto у
     предков: правило переживёт будущую правку разметки без молчаливой
     регрессии. */
  hyphens: none;
}

/* Амперсанд рядом с рукописными именами (.cover__names на обложке,
   .footer__names в подвале — оба набраны Pompadur, .accent/.cover__name) —
   один общий класс, не два одинаковых правила под разные секции (T090 +
   правка 10, 2026-08-07: подвал подтянут к обложке). У Pompadur (91 глиф)
   амперсанда НЕТ — если оставить его унаследованным, браузер молча
   подставит амперсанд из fallback-стека (cursive), а он у каждого гостя
   свой. Поэтому амперсанд явно набран курсивом Lora (var(--font-text)), а
   не унаследован от родителя. Кегль подбирался снимком под кегль имён в
   КАЖДОМ месте своим модификатором ниже — общего числового кегля тут нет. */
.conj {
  font-family: var(--font-text);
  font-style: italic;
  text-transform: lowercase;
  letter-spacing: 0;
  line-height: 0.85;
}

/* Кегль 1.5em подбирался под мелкий очковый рост Pompadur на обложке — на
   Lora курсивом при том же кегле амперсанд выглядел разбухшим, снижен до
   1.15em (подобрано снимком, не расчётом). */
.cover__conj {
  font-size: 1.15em;
}

/* Подвал крупнее обложки (footer__names — 1.3× --fs-h3, см. ниже по файлу),
   но амперсанд курсивом Lora на этом кегле уже не «разбухает» — снижение
   не нужно, оставлен родной кегль текста (подобрано снимком). */
.footer__conj {
  font-size: 1em;
}

/* --------------------------------------------------------------------------
   2. Отсчёт и факты
   -------------------------------------------------------------------------- */

/* Компактная строка внутри пузыря реплики — раньше были четыре плашки со
   своей заливкой и рамкой (плашка в плашке поверх уже залитого зеркала).
   Теперь просто числа в ряд, разделённые тонкой чертой, без своего фона. */
.countdown {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-sm);
}

.countdown__cell {
  text-align: center;
}

/* Раньше между ячейками стояла черта (`+`-селектор, border-left на всех,
   кроме первой). На переносе строки (.countdown — flex-wrap: wrap) это
   ломалось: первая ячейка второго ряда — не первый child в разметке, черта
   доставалась и ей, повисая перед «минутами»/«секундами» ни от чего (видно
   на 390px). Гарантированно уместить 4 ячейки в ряд даже на 320px (см. ниже
   — на нём секунды скрыты) можно, но черты для минималистичного вида,
   который просили заказчики, всё равно лишние — ритм держит воздух между
   ячейками (gap), которому на переносе нечего ломать. */
.countdown__value {
  display: block;
  /* Антиква --font-display (раньше Cormorant Garamond, теперь Lora) рисует
     цифры не маюскульными — риск нечёткости в кегле отсчёта. Поэтому у ЧИСЕЛ
     отсчёта гарнитура переключена на --font-ui (Inter, маюскульные цифры —
     чёткость важнее эстетики, тот же принцип, что у кнопок и полей анкеты).
     Подписи (.countdown__label) — мелкая служебная подпись, тот же принцип.
     tabular-nums оставлен: без него цифры прыгают по ширине на каждый тик. */
  font-family: var(--font-ui);
  font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.25rem);
  line-height: 1;
  color: var(--c-accent-deep);
  font-variant-numeric: tabular-nums;
}

.countdown__label {
  display: block;
  margin-top: 0.35em;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  /* Внутри .mirror__glass — тот же затемнённый muted, что и у .avatar--guest
     (см. .mirror__glass .muted): подписи мелкие, им нужен запас контраста. */
  color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
}

.countdown__note {
  font-size: var(--fs-h3);
  text-align: center;
}

/* Календарь месяца торжества (FR-004a, T059) — сменил обратный отсчёт выше
   (.countdown* сохранён неиспользуемым, js/countdown.js осиротел намеренно).
   Сетка строится в js/calendar.js из EVENT.startsAt, здесь только оформление
   готовой <table>. Правка «календарь и адреса внутрь сообщения»: раньше блок
   лежал отдельной плашкой на стекле шире пузыря переписки, margin-top держал
   отступ от предыдущего блока. Теперь .calendar — содержимое ВНУТРИ пузыря
   .chat__ask--calendar, свой отступ и фон пузырю не нужны, отступ снят. */
.calendar {
  position: relative;
}

.calendar__grid {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

.calendar__caption {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  color: var(--c-accent-deep);
  text-align: center;
  margin-bottom: var(--space-xs);
  caption-side: top;
}

.calendar__grid th {
  /* Подписи дней недели — служебный интерфейс, как время программы дня
     и адреса (см. .facts__value/.timeline__time): чёткость важнее
     эстетики антиквы. */
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-align: center;
  padding-block: 0.35em;
  color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
}

.calendar__cell {
  text-align: center;
  padding: 0.35em 0;
  vertical-align: middle;
}

.calendar__cell--empty {
  /* Клетка вне месяца — пусто, без числа и без декора. */
}

.calendar__day {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-variant-numeric: tabular-nums;
  color: var(--c-ink);
}

/* Правка заказчиков (лист рисованных сердец 8310.eps, автор rawpixel.com):
   прямоугольная бежевая подложка клетки 19-го снята — выделение теперь несёт
   контур сердца №1 листа, заливка-плашка в скрапбучном стиле смотрелась
   чужеродно. Позиционный контекст для декоративного сердца — оно рисуется
   абсолютно и вправе выходить за границы клетки (клетка ужимается вплоть до
   ~20px на 320px-экране, сердцу нужно больше места, чем есть в ячейке). */
.calendar__cell--event {
  position: relative;
}

.calendar__cell--event .calendar__day {
  position: relative;
  /* Уточнение заказчиков 2026-08-06: число, обведённое сердцем, — тёмным
     (--c-ink, та же роль, что у обычных чисел календаря), сердце теперь
     матовым красным (--c-heart, см. PALETTE выше): два разных цвета
     читаются раздельно, даже когда контур сердца задевает край цифры.
     Полужирность снята тем же уточнением заказчиков (2026-08-06, третья
     правка) — выделение несёт сердце, дублировать его насыщенностью
     штриха цифры не нужно; начертание числа 19 — то же, что у остальных
     чисел месяца. */
  color: var(--c-ink);
}

/* Оба ассета листа 8310.eps — ЗАЛИТЫЕ фигуры (fill-rule: nonzero), без
   обводки: у сердца №1 «контурность» дана самой геометрией (внешний и
   внутренний подпути кольцом гасят друг друга при заливке), а не толщиной
   линии — stroke тут не рисует ничего. */
.icon-heart {
  fill: currentColor;
  fill-rule: nonzero;
}

/* Сердце №2 листа 222560-P1MBMS-260.eps «Doodle heart collection» — обводит
   число 19 (клетка .calendar__cell--event). Пропорции контура: viewBox
   "0 0 93.102 85.511" → 93.102×85.511.

   Правка заказчиков («сердце перекрывает 19»): сердце №1 листа 8310.eps сюда
   не годилось по геометрии — нарисовано сдвоенной линией, центр viewBox лежал
   ПРЯМО НА штрихе, самый большой чистый просвет внутри — 34,2%×17,1% от
   габарита. Замена размером (множитель 5, старое смещение -74%/-40.6%) делала
   хуже: линия закрывала 33% площади глифов «19» и задевала ещё четыре числа
   сетки — 18 (41%), 26 (25%), 25 (22%), 12 (15%), а само сердце выходило
   вдвое шире клетки календаря.

   Сердце №2 нарисовано эскизно (один прерывистый контур, не кольцо-обводка):
   у него нет единого «чистого просвета» — выемка между лопастями уходит
   глубоко внутрь фигуры и делит середину надвое, так что ориентир вида
   «центр просвета X%/Y%» для него в принципе не работает (ранняя оценка
   45,5%/41,4% была получена как самый большой прямоугольник без чернил и
   вводила в заблуждение — на практике в этой точке контур задевает саму
   «19»). Реальная посадка подобрана прямым перебором на живой странице:
   путь отрисован на canvas в экранном размере/положении (Path2D, fill
   nonzero), рядом растеризованы глифы чисел сетки тем же шрифтом от
   вычисленной базовой линии, перекрытие посчитано попиксельно. Для этого
   эскиза чистой одновременно для «19» и для всех соседей конфигурации нет —
   решение заказчиков: 4% лёгкого касания края «19» (по факту незаметно на
   глаз) вместо полного перекрытия соседних чисел. */
.calendar__heart--outline {
  display: block;
  position: absolute;
  /* Ширина в em от --fs-ui (кегль самого числа), не в rem: --fs-ui растёт от
     320 к 1280 быстрее, чем корневой rem (плавная база html, см. выше) — на
     фиксированных rem сердце на широком экране отставало ростом от цифр и
     обрезало «1» у «19» (замер T059-правки). em привязывает рост сердца
     к росту самого числа, которое оно обводит.

     Множитель 3,4 — решение заказчиков после перебора на живой странице
     (Chromium/CDP, попиксельное сравнение залитого пути сердца и
     растеризованных глифов чисел сетки от вычисленной базовой линии).
     У этого эскиза нет размера/позиции, при которой одновременно чиста и
     «19», и все соседние числа (см. комментарий выше про глубокую выемку
     между лопастями). Перебор дал: множитель 3,8–4,2 полностью очищает
     «19», но линия задевает «12» (0,7–9,0% площади его глифов) и «18»
     (0,7–0,9%); множитель 3,4 с этим же смещением X (-58%) касается только
     самой «19» (около 4% площади её глифов на 1280px — лёгкое касание края,
     не перекрытие) и не задевает ни одного соседа. Заказчики выбрали второй
     вариант: небольшое касание целевого числа приемлемее, чем перечёркнутые
     соседние дни. */
  width: calc(var(--fs-ui) * 3.4);
  /* Общий сброс `svg { max-width: 100% }` иначе клампит сердце шириной
     клетки (её containing block, ~20px на 320px-экране) — деко́р должен
     вправе выйти за границы клетки (см. комментарий у .calendar__cell--event
     выше), поэтому клампу здесь не место. */
  max-width: none;
  aspect-ratio: 93.102 / 85.511;
  left: 50%;
  top: 50%;
  /* Сердце центрируется на числе: -50%/-50% ставит середину контура в середину
     клетки. Прежние -58%/-68% поднимали его на 9px выше числа, и верх контура
     заходил на «12» строкой выше — замечание заказчиков 2026-08-07.

     ВАЖНО про способ проверки. Подбирать эти координаты попиксельным
     пересечением «путь сердца × глифы числа» НЕ надо: такая проверка уже дважды
     дала ложный результат (утверждала перекрытие «19» там, где на снимке экрана
     цифра чистая). Причина — вычисление вертикали глифов от базовой линии
     расходится с реальным рендером. Проверять снимком блока, а не арифметикой. */
  transform: translate(-50%, -50%);
  color: var(--c-heart);
  pointer-events: none;
  /* Уточнение заказчиков 2026-08-06: сердце рисуется ПОВЕРХ числа (раньше
     было наоборот, число лежало выше) — стек не меняем этой правкой, только
     геометрию (width/transform выше): цифра теперь и без стекового порядка
     не перекрыта — целиком в просвете клина.

     Цвет сердца — матовый красный (--c-heart), не золото: правка заказчиков
     2026-08-06 (см. подробный расчёт цвета — комментарий у роли --c-heart в
     PALETTE выше). */
  z-index: 1;
}

/* T102 (2026-08-12): прежняя карточка-список .facts/.facts__item/
   .facts__label (подпись «Регистрация»/«Банкет» + значение в двух <li>)
   удалена — оба адреса теперь звучат репликами от пары внутри .chat__ask,
   текстом, а не карточкой. Вторая итерация (замечание пользователя того же
   дня) убрала и обёртку .facts__value-row: она была display: inline-flex,
   то есть атомарным inline-блоком, — текст обтекал пару «адрес + значок»
   целиком, значок читался как вставка «в поле диалога», а точка после адреса
   срывалась на новую строку. Остался только .facts__value (сам текст адреса),
   значок стоит в строке отдельным inline-элементом (см. index.html, #date).

   T111 (осмотр 2026-08-12): раньше здесь стояли font-family/font-size
   var(--font-ui) — расчёт был на то, что критичная информация (адрес)
   выигрывает от чёткого служебного шрифта. На практике внутри пузыря
   .chat__ask (гарнитура Lora) это читалось разнобоем гарнитур посреди
   одного предложения — ошибкой вёрстки, а не осознанным акцентом. Адрес
   теперь набран той же Lora, что и вся реплика: он и так выделен тем, что
   стоит после тире и перед именованной кнопкой-значком, отдельная гарнитура
   для этого не нужна. */
.facts__value {
  /* T097: адрес переносить нельзя («Екатерин-бург» хуже, чем отсутствие
     переноса вовсе) — гасит hyphens: auto, унаследованный от .chat__ask
     (T102: раньше наследовалось от снятого .facts, источник сменился, само
     правило нужно по-прежнему). */
  hyphens: none;
}

/* Кнопка-глобус — маленькая, круглая, тем же приёмом currentColor, что и
   .chat__composer-send--icon (D12), но контурная (ghost), а не залитая:
   рядом с адресом залитый кружок спорил бы за внимание с текстом. Один
   значок на строку — открывает окно карты с уже проложенным маршрутом
   именно до этого адреса (page.js#setupMapDialog).

   Правка T094 (жалоба заказчиков «плохо видны»): кружок и глиф увеличены
   (снимок строки адреса на 320/390/768/1280px — с прежними 1.75rem/0.95rem
   значок терялся рядом с текстом адреса, с этими числами читается сразу,
   но ещё не спорит с адресом за внимание). Видимый кружок сознательно
   меньше 44px тач-цели — тач-цель добита невидимым hit-area (::before
   ниже), а не раздутым видимым кругом, который на D12 признали слишком
   заметным для места рядом с текстом. */
.facts__map-btn {
  position: relative;
  display: inline-flex;
  /* Значок стоит ПРЯМО В СТРОКЕ текста реплики (T102, вторая итерация), а не в
     отдельной обёртке-flex, как раньше, — поэтому его надо посадить на базовую
     линию по-человечески: без vertical-align inline-flex выравнивается по
     baseline своего содержимого и кружок «проваливается» ниже строки. */
  vertical-align: middle;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--c-line);
  border-radius: 50%;
  background: none;
  color: var(--c-accent-deep);
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
}

/* Невидимая тач-цель 44×44 CSS px (порог тач-доступности) поверх видимого
   кружка меньшего размера — тот же приём с абсолютными px, а не rem, что и
   .map-dialog__close (см. её комментарий про плавающий html{font-size}).
   Кружок остаётся визуально скромным, а палец промахивается реже. */
.facts__map-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.facts__map-btn:hover {
  background-color: var(--c-accent-deep);
  color: var(--c-bg);
}

/* T101 (переделка 2026-08-12): непрерывно вращающийся золотой ободок —
   замена отвергнутому пользователем варианту с бегущей дугой (короткая
   яркая дуга на тусклом кольце — это в точности язык спиннера загрузки,
   от которой уходили ещё в T094). Здесь ободок залит золотом ЦЕЛИКОМ, без
   тёмного фона и без чёткого начала/конца дуги — переливается мягкий
   перепад яркости, а не бежит светлое пятно по тёмному кольцу.

   ::after (не ::before — та невидимая тач-цель 44px выше, трогать нельзя),
   сама кнопка border-box (см. * { box-sizing: border-box } в начале
   файла), поэтому inset: 0 у ::after кладёт его точно в границы кружка со
   стороной 2.25rem, без отдельного расчёта смещения под border: 1px.

   Ring-маска не менялась: radial-gradient с ключевым словом circle
   closest-side — ЕГО радиус равен половине СТОРОНЫ квадратного блока (а не
   половине диагонали, как у дефолтного farthest-corner на квадрате),
   поэтому 100% стопа совпадает точно с окружностью кружка (border-radius:
   50% обрезает квадрат по тому же радиусу).

   Конус-градиент — два плавных пика яркости на противоположных сторонах
   кольца (0%/50%/100% — светлее, 25%/75% — сам --c-accent без подмеса),
   БЕЗ прозрачных или тёмных стопов: перепад мягкий по всей окружности,
   резкой границы светлого/тёмного нет нигде — то, что превращало прошлый
   вариант в дугу-спиннер. Яркость даёт color-mix с white, не сдвиг тона:
   единственный цвет страницы остаётся золото (--c-accent), меняется
   только его светлота. Вращает не сам конус (нужен @property для
   плавной интерполяции angle), а transform: rotate() всего ::after —
   анимируемо нативно везде без регистрации кастомных свойств. */
.facts__map-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(
    from 0deg,
    color-mix(in srgb, var(--c-accent) 60%, white) 0%,
    var(--c-accent) 25%,
    color-mix(in srgb, var(--c-accent) 60%, white) 50%,
    var(--c-accent) 75%,
    color-mix(in srgb, var(--c-accent) 60%, white) 100%
  );
  -webkit-mask-image: radial-gradient(
    circle closest-side,
    transparent 0,
    transparent calc(100% - 4px),
    #000 calc(100% - 4px),
    #000 100%
  );
  mask-image: radial-gradient(
    circle closest-side,
    transparent 0,
    transparent calc(100% - 4px),
    #000 calc(100% - 4px),
    #000 100%
  );
  /* 8s, НЕ 7s (период globe-spin ниже) — периоды сознательно разведены, и
     разводить их по фазе не требуется: пространственно эффекты не
     пересекаются. Кольцо живёт на ::after и маской обрезано до внешних
     4px кружка; глиф globe-spin — отдельный SVG-элемент 1.3rem внутри
     кружка 2.25rem, полностью внутри немаскированной прозрачной середины
     кольца. Разные элементы, разная область экрана — общий кадр с обоими
     движениями не даёт ряби, потому что накладываться геометрически
     нечему. linear (не ease-in-out, как у globe-spin) — вращение ободка
     ровное и медленное, без ускорений, которые читались бы как рывок. */
  animation: map-btn-shimmer 8s linear infinite;
}

/* При наведении рамку заливает :hover выше — вращающийся ободок на золотом
   кружке был бы шумом поверх заливки, а не акцентом. animation: none (а не
   только opacity: 0) отменяет саму анимацию, а не замораживает её на
   текущем кадре. */
.facts__map-btn:hover::after {
  animation: none;
  opacity: 0;
}

@keyframes map-btn-shimmer {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.icon-globe {
  width: 1.3rem;
  height: 1.3rem;
  fill: currentColor;
  /* Центр вращения — визуальный центр самого глифа, а не левый верхний
     угол SVG-вьюпорта (дефолт CSS Transforms для SVG без transform-box). */
  transform-box: fill-box;
  transform-origin: center;
  /* Вращение — периодическое, не непрерывное (T094): постоянно крутящийся
     значок на свадебном приглашении читается как индикатор загрузки, а не
     как декоративная деталь. Пауза зашита ВНУТРИ keyframes (глиф стоит на
     месте от 8% до 100% шкалы), а не через animation-delay — delay просто
     сдвинул бы старт первого прохода, а не создал бы паузу МЕЖДУ проходами
     при animation-iteration-count: infinite. Финальный кадр — те же 360deg
     (визуально неотличимо от 0deg), поэтому виток в начало цикла не даёт
     видимого скачка глифа назад. Кнопка (родитель) в анимации не участвует
     — крутится только сам глиф внутри неподвижной цели нажатия. */
  animation: globe-spin 7s ease-in-out infinite;
}

@keyframes globe-spin {
  0% {
    transform: rotate(0deg);
  }
  10% {
    transform: rotate(360deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* Гость отключил анимации в системе. Общий блок @media
   (prefers-reduced-motion: reduce) выше по файлу уже сжимает
   animation-duration до 0.01ms — этого достаточно, чтобы вращение стало
   незаметным, но не гарантирует «полностью выключено»: на этой фиче на
   reduced-motion уже горели (T091/mirror-sheen), поэтому здесь — явный
   animation: none, а не расчёт на общее правило. */
@media (prefers-reduced-motion: reduce) {
  .icon-globe {
    animation: none;
  }

  /* T101: как и у .icon-globe выше — явный animation: none, а не расчёт на
     сжатие animation-duration общим правилом (на reduced-motion в этой
     фиче уже горели дважды, см. комментарий у .icon-globe). opacity: 0
     дублирует финальный кадр keyframes на случай, если анимация всё же
     где-то не отменится. */
  .facts__map-btn::after {
    animation: none;
    opacity: 0;
  }
}

/* --------------------------------------------------------------------------
   4. Программа дня
   -------------------------------------------------------------------------- */

/* Правка T060/T049 (программа дня — внутрь пузыря переписки): раньше
   .timeline лежал отдельной плашкой на стекле, рельс из вертикальной линии
   (--c-line) и обведённых кружков размечал шаги вдоль неё. Внутри пузыря
   .chat__ask своя рамка/фон уже есть (см. .chat__ask--calendar выше),
   рельс не нужен вовсе — пункты разделяет тонкая граница между строками,
   тем же --c-line, что раньше был у линии. */
.timeline {
  list-style: none;
  padding: 0;
}

.timeline__item {
  padding-block: var(--space-xs);
}

.timeline__item + .timeline__item {
  border-top: 1px solid var(--c-line);
}

.timeline__time {
  display: block;
  /* Время программы дня — критичная информация, чёткость важнее эстетики
     (см. --font-ui), в отличие от остального декоративного текста блока. */
  font-family: var(--font-ui);
  font-size: var(--fs-h3);
  color: var(--c-accent-deep);
}

.timeline__title {
  display: block;
  color: var(--c-ink);
}

/* --------------------------------------------------------------------------
   5. Карта (T061, решения пользователя 2026-08-07):
   — отдельный блок «Место проведения» убран — оба адреса названы строками
     выше в блоке даты (.venue/.venue__name/.venue__address сняты вместе с
     разметкой);
   — инлайн-раскрытие карты в пузыре отклонено по факту осмотра («карта
     зажата шириной реплики») — карта переехала во всплывающее окно
     .map-dialog, тот же нативный <dialog>, что у .rsvp-success;
   — вместо двух текстовых кнопок («Показать карту» + «Построить маршрут») —
     по одному значку-глобусу у каждой строки адреса, см. .facts__map-btn.
   -------------------------------------------------------------------------- */

.map {
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--c-bg-soft);
}

.map__frame {
  width: 100%;
  height: 100%;
  border: 0;
  /* Виджет Яндекс.Карт перехватывает вертикальный свайп под панорамирование
     карты — на телефоне это глушит прокрутку страницы прямо над пузырём.
     pan-y оставляет браузеру только вертикальный жест страницы, горизонтали
     и жесты масштабирования карты (пинч, drag вбок) остаются у виджета. */
  touch-action: pan-y;
}

@media (min-width: 40em) {
  .map {
    aspect-ratio: 16 / 9;
  }
}

/* Окно карты — тот же приём, что у .rsvp-success (width: min(…, 90vw), своя
   ::backdrop через opacity, а не color-mix — см. её комментарий). Шире
   .rsvp-success: карте нужна ширина, чтобы маршрут читался, а не только
   текст. */
.map-dialog {
  max-width: 40rem;
  width: min(40rem, 90vw);
  margin: auto;
  padding: var(--space-md);
  border: none;
  border-radius: var(--radius);
  background-color: var(--c-bg);
}

.map-dialog::backdrop {
  background-color: var(--c-ink);
  opacity: 0.55;
}

.map-dialog__close {
  /* Жалоба заказчика: кнопка «закрыть» визуально доминировала над картой —
     базовый .button рассчитан на кнопки-призывы во весь экран, а тут это
     единственная вспомогательная кнопка внутри модалки. Уменьшаем шрифт и
     паддинги, но не hit-area: min-width/height взяты в px, НЕ в rem — у
     сайта плавающий html{font-size} (клампится вниз до ~88% от 16px, см.
     html выше), и 2.75rem там дал бы ~39px вместо нужных 44 — ниже порога
     тач-цели. 44px — абсолютный порог, привязка к rem его бы подрывала. */
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin: var(--space-sm) auto 0;
  padding: 0.4em 1.4em;
  font-size: 0.8rem;
}

/* --------------------------------------------------------------------------
   6. Опросник
   -------------------------------------------------------------------------- */

.chat {
  /* Уже .mirror__glass: .mirror ограничен var(--measure) = 34rem, минус
     паддинги стекла (--space-md * 2) внутренняя ширина ~30,5rem — этот
     max-width уже ничего не режет сам по себе. Оставлен как явная граница
     на случай, если .chat однажды окажется вне зеркала. */
  max-width: 32rem;
  margin-inline: auto;
  display: grid;
  gap: var(--space-md);
}

/* .chat__turn — «домик» реплики: пока гость до неё не дошёл или уже ответил,
   лежит здесь скрытым (см. chat-form.js). Всегда ровно два прямых потомка —
   .chat__ask (вопрос, переезжает в ленту навсегда) и .chat__control (поле,
   ходит туда-сюда между домиком и строкой ввода). */
.chat__turn {
  display: grid;
  gap: var(--space-xs);
}

/* .chat__control для трёх реплик — <fieldset> (переключатели/чекбоксы/счётчики),
   для остальных — сам <input>: сбрасываем рамку fieldset только там, где она
   есть, иначе это же правило перебило бы padding у .field__input (та же
   специфичность класса, порядок в файле решил бы исход). */
fieldset.chat__control {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0; /* fieldset иначе не сжимается на узких экранах */
}

/* Реплика пары — пузырь, прижатый влево, с «хвостиком» снизу-слева.
   Аватарка пары рисуется псевдоэлементом, а не отдельной <img>: у части
   реплик .chat__ask — это <label>, у части — обычный <p> (см. index.html,
   связь с группой полей там держит aria-labelledby, а не соседство в DOM),
   обернуть их в <div> под img+текст было бы лишней вложенностью.
   --avatar-size задаёт размер и слева от пузыря резервируется margin-left
   той же величины: max-width уменьшен на неё же, чтобы правая граница
   пузыря осталась на прежних 85% — аватарка с пузырём вместе занимают ровно
   столько же места, сколько раньше занимал один пузырь, без переполнения на
   узких экранах. */
.chat__ask {
  position: relative;
  --avatar-size: 3rem;
  display: block; /* сбрасывает float у <legend>, которая иначе капризна */
  width: 100%;
  float: none;
  margin-right: auto;
  margin-left: calc(var(--avatar-size) + var(--space-xs));
  /* Не 100 %: на всю ширину пузыри выравниваются по обоим краям, и переписка
     перестаёт читаться — по кому реплика, видно только по сдвигу. 92 %
     оставляют заметный уступ справа при любой ширине экрана, но не режут
     пузырь до нечитаемой узости (было перевёрнуто: 85 % на широком экране
     и 92 % только на телефоне — правка 2026-08-03, T065). */
  max-width: calc(92% - var(--avatar-size) - var(--space-xs));
  /* Правка заказчиков (2026-08-06): абсолютный потолок сообщения — тот же
     --measure, что держит длину строки колонки переписки (34rem по
     исходному расчёту роли; текущее значение переменной — решение T075,
     здесь оно не переопределяется, только используется). На узких экранах
     решает процентный max-width выше, --measure подключается только на
     широких, где 92% сами по себе дали бы пузырь шире комфортной строки. */
  max-width: min(calc(92% - var(--avatar-size) - var(--space-xs)), var(--measure));
  padding: 0.75em 0.9em;
  background-color: var(--c-bg);
  border: 1px solid var(--c-line);
  border-radius: 1.1em 1.1em 1.1em 0.2em;
  color: var(--c-ink);
  font-size: var(--fs-body);
  font-weight: 400;
  /* Жалоба заказчика: длинная строка без пробелов (гость может ввести 300
     символов подряд) раздвигает scrollWidth пузыря шире его max-width и
     вылезает за скруглённый фон — anywhere режет слово посреди, а не только
     на границе слова (обычный break-word её бы не тронул). */
  overflow-wrap: anywhere;
  /* T097: пузырь — узкая колонка, hyphens: auto решает ДРУГУЮ задачу, чем
     overflow-wrap выше — обычные слова с переносом там, где раньше слово
     целиком падало на новую строку или лепилось тире. Оба правила нужны
     вместе, ни одно не отменяет другое (overflow-wrap срабатывает только
     когда одному «слову» самому по себе тесно). */
  hyphens: auto;
}

.chat__ask::before {
  content: '';
  position: absolute;
  left: calc(-1 * (var(--avatar-size) + var(--space-xs)));
  bottom: 0;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  border: 1px solid var(--m-gold);
  /* Стикер couple-sticker.webp — с прозрачным фоном (PNG/WebP alpha), а не
     сплошной кружок: при матовом слое 40% (было 88%, см. .page-bg::after)
     сквозь него вместо ровной подложки стало видно фотографию — кружок
     читался дырой в странице. Заливка — тот же --c-bg, что у пузыря
     реплики рядом (.chat__ask), поэтому аватарка визуально сидит на той
     же «бумаге», что и текст. Правка заказчика 2026-08-07. */
  background-color: var(--c-bg);
  /* Кадр голов в готовом стикере (couple-sticker.webp, 512×512) — квадрат
     152×152 px с началом в точке (200, 8). Масштаб и смещение выражены через
     отношение размера аватарки S к размеру кадра — коэффициенты читаются
     прямо из чисел кадра, без вручную посчитанных процентов. */
  background-image: url('../img/couple-sticker.webp');
  background-repeat: no-repeat;
  background-size: calc(var(--avatar-size) * 512 / 152) auto;
  background-position: calc(var(--avatar-size) * -200 / 152) calc(var(--avatar-size) * -8 / 152);
  /* T081: тот же couple-sticker.webp нарезан из старого цветного снимка и
     остаётся единственным цветным пятном рядом с чёрно-белой съёмкой —
     обесцвечено фильтром здесь, а не перевыпуском файла: тот же файл ещё
     раз используется как есть (рисованный стикер, не фото) в
     .rsvp-success__sticker на экране благодарности, и там цвет остаётся —
     перевыпуск затронул бы оба места сразу. */
  filter: grayscale(1);
}

/* Аватарка — только у последней реплики серии (в мессенджере она у
   последнего сообщения, не у первого). Раньше это было CSS-правило
   `.chat__ask--grouped:has(+ .chat__ask--grouped)`, но оно видит только
   прямых соседей в одном родителе — в анкете (#rsvp, chat-form.js) реплики
   раскрываются по шагам и на разных стадиях лежат в разных родителях
   (.form.chat/.chat__log/.chat__turn), а «последняя реплика серии» вообще
   движется по ходу диалога. Правка пользователя 2026-08-07 («в последнем
   экране две иконки, когда должна одна») — это была именно та дыра: две
   несгруппированные реплики анкеты подряд несли каждая свою аватарку.
   Единый механизм на всю страницу — JS (chat-avatars.js), см. там же:
   второй механизм рядом не заводим, старое CSS-правило снято целиком. */
.chat__ask:not(.chat__ask--tail)::before {
  content: none;
}

/* Серия реплик от пары стоит теснее, чем реплики разных серий (обычный gap
   между .chat__ask в .chat — var(--space-md)), но граница между соседними
   сообщениями всё равно обязана быть видна — прошлый заход ужимал зазор
   ОТРИЦАТЕЛЬНЫМ margin-top, три реплики серии слипались в один пузырь на
   весь блок (правка заказчиков 2026-08-06: «это читается как одно
   гигантское сообщение»). --space-xs — тот же зазор, что между вопросом и
   полем внутри одной реплики (.chat__turn), заметно меньше обычной паузы
   между сериями, но не отрицательный. */
.chat__ask--grouped {
  margin-top: var(--space-xs);
}

/* Календарь-вложение внутри пузыря: своя рамка/фон блока календаря ниже
   (.calendar) больше не нужна — пузырь .chat__ask уже даёт фон/скругление,
   а .calendar остаётся только для внутренних отступов/позиционирования
   уголка сердца. */
.chat__ask--calendar {
  padding-block: 0.6em;
}

/* Несколько строк внутри одного пузыря (правка заказчиков 2026-08-06 раздала
   бывший монолог знакомства по отдельным репликам, но одна из них — «Но вы и
   сами знаете» + «Будем рады видеть вас» — осталась двумя фразами в одном
   пузыре): без этого правила абзацы внутри .chat__ask схлопнулись бы
   глобальным сбросом `* { margin: 0 }` в сплошной текст. Прежний модификатор
   .chat__ask--lines снят вместе со старым монолитным пузырём — писать
   разбиение внутри пузыря понадобилось только тут, правило переведено на
   голый .chat__ask. */
.chat__ask p + p {
  margin-top: 0.6em;
}

/* Подпись пары (T058, была отдельным .greeting__sign) — теперь своя реплика
   в конце серии знакомства, крупнее обычной фразы. */
.hello__sign {
  font-size: var(--fs-h3);
}

/* Эхо-пузырь ответа гостя в ленте — пузырь, прижатый вправо, зеркальный
   хвостик снизу-справа, ровно один на реплику (строит chat-form.js после
   ответа). Заливку несёт .chat__echo — иначе выйдет пузырь в пузыре.
   Аватарка гостя — последний ребёнок, текст растягивается и делит строку
   с ней. */
.chat__reply {
  display: flex;
  align-items: flex-end;
  gap: var(--space-xs);
  margin-left: auto;
  max-width: 85%;
  width: 100%;
}

.chat__reply > :not(.avatar) {
  flex: 1;
  min-width: 0;
}

.chat__echo {
  padding: 0.75em 0.9em;
  background-color: var(--c-bg);
  border: 1px solid var(--c-line);
  /* Зеркало хвостика .chat__ask — эхо-пузырь читается как исходящее сообщение. */
  border-radius: 1.1em 1.1em 0.2em 1.1em;
  color: var(--c-ink);
  /* Тот же перенос длинного «слова», что у .chat__ask выше — .chat__echo
     дополнительно flex-элемент внутри .chat__reply (flex: 1, min-width: 0
     уже стоит на нём), но одного min-width без overflow-wrap не хватает:
     контент внутри самого пузыря всё равно раздувает scrollWidth. */
  overflow-wrap: anywhere;
  /* T097: то же hyphens: auto, что у .chat__ask — эхо-пузырь такой же узкой
     ширины. */
  hyphens: auto;
}

/* Круглая аватарка — общий размер для аватарки пары (::before выше) и
   аватарки гостя (.avatar--guest ниже), синхронизирован через ту же
   величину 3rem, но как самостоятельная переменная: аватарка гостя не
   вложена внутрь .chat__ask и его --avatar-size ей не наследуется. */
.avatar {
  --avatar-size: 3rem;
  width: var(--avatar-size);
  height: var(--avatar-size);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: 50%;
}

.avatar--guest {
  border: 1px solid var(--m-gold);
  /* Тот же провал, что у .chat__ask::before выше (аватарка пары): силуэт
     лежит на прозрачном кружке, и при матовом слое 40% сквозь него
     читалась фотография вместо ровной подложки. */
  background-color: var(--c-bg);
  overflow: hidden; /* силуэт срезан по кругу исходника, но кромку страхуем */
}

/* T112: раньше внутри кружка стояло слово «гость» кеглем 0.55rem. На 320 px
   аватарка сжата до 2.2rem ≈ 31 px, а слово при читаемых 11-12 px требует
   ≈ 34 px — шире самого кружка; поднять --avatar-size нельзя, она общая с
   пузырями реплик (см. брейкпоинт ниже). Решение пользователя 2026-08-13 —
   безликий силуэт вместо надписи. Читается на любом размере: смысл несёт
   форма, а не кегль. Доступности не теряем — .avatar--guest и раньше был
   aria-hidden, гостя в ленте называет сам текст эхо-пузыря.

   Силуэт подключён МАСКОЙ, а не готовой картинкой: файл несёт только альфу
   (tools/guest_avatar_extract.py), цвет задаётся здесь — тот же затемнённый
   muted, каким была надпись, и он останется в палитре при любой её правке. */
.avatar--guest::before {
  content: '';
  width: 100%;
  height: 100%;
  background-color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
  mask-image: url('../img/decor/avatar-guest.webp');
  mask-size: contain;
  mask-position: center;
  mask-repeat: no-repeat;
}

.field__input {
  width: 100%;
  padding: 0.75em 0.9em;
  /* Поле ввода — чёткость важнее эстетики (см. --font-ui). Кегль — явные
     16px, не переменная роли: ниже Safari на iPhone при фокусе на поле
     самопроизвольно увеличивает всю страницу зумом. max() — на случай,
     если 1rem когда-нибудь окажется больше 16px (крупная настройка
     шрифта в браузере гостя), поле не должно стать МЕНЬШЕ читаемого. */
  font-family: var(--font-ui);
  font-size: max(1rem, 16px);
  background-color: var(--c-bg);
  /* --c-line на голубом стекле (--m-glass) — 1,1:1, рамка почти не видна
     (просьба заказчиков 2026-08-03: поле должно читаться рамкой, как в
     мессенджере). Роль не переопределяем — тот же приём, что у .muted
     чуть выше: темним локально через color-mix, это даёт 3,57:1 без
     нового цветового литерала. */
  border: 1px solid color-mix(in srgb, var(--c-line) 40%, var(--c-ink));
  /* Зеркало хвостика .chat__ask — поле читается как исходящий пузырь. */
  border-radius: 1.1em 1.1em 0.2em 1.1em;
  transition: border-color 0.2s;
}

.field__input:focus {
  border-color: var(--c-accent);
}

/* T110: поле переписки-зеркала после первой отправки — read-only (не
   `disabled`), чтобы гость всё ещё мог дойти до него табом и скринридер
   зачитал и само поле, и explaining aria-label в mirror-reply.js: молчаливый
   серый инпут ничего не объясняет, а `disabled` вдобавок выкидывает элемент
   из таб-порядка. Курсор и приглушённый фон — тот же сигнал «не трогай»,
   что и на выключенной кнопке .chat__composer-send--icon:disabled ниже. */
.chat__control--locked {
  cursor: not-allowed;
  background-color: color-mix(in srgb, var(--c-bg) 85%, var(--c-line));
  color: var(--c-muted);
}

/* Обёртка вокруг поля с maxlength (index.html) — несёт класс .chat__control
   вместо самого <input>/<textarea>: chat-form.js переносит между репликой и
   строкой ввода именно .chat__control целиком (appendChild), и если счётчик
   символов (char-counter.js) сидит внутри неё соседом поля, он переезжает
   вместе с полем, а не остаётся сиротой в опустевшей реплике. position:
   relative — точка отсчёта для .field__counter ниже. */
.field__wrap {
  position: relative;
}

/* Тот же приём для узкой строки «Написать нам» под зеркалами
   (mirror-reply.js) — там своей обёртки нет, поле лежит прямо в
   .chat__composer-control, и эта же строка ввода никогда не переезжает
   (в отличие от анкеты, у зеркал одно-единственное статичное поле), так что
   отдельная обёртка не нужна — сам слот и служит точкой отсчёта. */
.chat__composer-control {
  position: relative;
}

/* Счётчик «10/100» — декоративная подсказка (aria-hidden в char-counter.js:
   реальный лимит уже несёт maxlength, зачитывать число на каждый символ
   скринридеру ни к чему). pointer-events: none — не должен подставляться
   под клик по полю или мешать выделению текста. */
.field__counter {
  position: absolute;
  top: 0.5em;
  right: 0.8em;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
  pointer-events: none;
  user-select: none;
}

/* Запас справа под сам счётчик — иначе текст в поле при заполнении верхней
   строки залезал бы под цифры. Выбирается по
   соседству со счётчиком (:has), а не заведён на все .field__input подряд —
   у полей без maxlength (например, ещё не встречавшихся в разметке) запаса
   не будет, потому что и цифр там не появится. */
.field__input:has(+ .field__counter) {
  padding-right: 3.6em;
}

/* Счётчики «сколько вас будет»: две узкие колонки, а не поля во всю ширину —
   двузначное число в поле на 32rem читается как ошибка вёрстки. */
.counters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--space-sm);
}

.counter__label {
  display: block;
  margin-bottom: var(--space-xs);
  /* Подпись поля анкеты — чёткость важнее эстетики (см. --font-ui). */
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  /* Внутри .mirror__glass — тот же затемнённый muted, что и у .avatar--guest
     (см. .mirror__glass .muted): подпись мелкая, ей нужен запас контраста. */
  color: color-mix(in srgb, var(--c-muted) 70%, var(--c-ink));
}

.counter__input {
  /* Стрелки спиннера в Firefox сжимают поле и по-разному выглядят в браузерах;
     ввод с клавиатуры и inputmode="numeric" на телефоне работают без них. */
  appearance: textfield;
}

.counter__input::-webkit-outer-spin-button,
.counter__input::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}

.choices {
  display: grid;
  gap: var(--space-xs);
}

/* Реплика «имена спутников» (T096) — до девяти полей «Гость N» подряд,
   не сеткой в несколько колонок как .choices--grid: узкий экран (320 px)
   не даёт двум полям с подписью встать рядом без переноса. */
.chat__control--stack {
  display: grid;
  gap: var(--space-sm);
}

.choices--grid {
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}

/* T121: поле «какой напиток» лежит в том же fieldset, что и .choices--grid
   выше, но fieldset — обычный block-элемент без gap (в отличие от
   .chat__turn/.choices), поэтому отступ сверху задаём явно. */
[data-drink-other-field] {
  margin-top: var(--space-sm);
}

.choice {
  display: flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.65em 0.9em;
  background-color: var(--c-bg);
  border: 1px solid var(--c-line);
  /* Зеркало хвостика .chat__ask — вариант ответа читается как исходящий пузырь. */
  border-radius: 1.1em 1.1em 0.2em 1.1em;
  cursor: pointer;
  /* У flex-элемента min-width по умолчанию — auto, то есть «не уже своего
     содержимого» — грид-ячейка (auto-fit, minmax(9rem, 1fr)) может выделить
     чипу меньше места, чем нужно тексту, и «Шампанское» вылезало за рамку
     стекла, а не переносилось. min-width: 0 разрешает чипу сжаться до ширины
     ячейки — дальше перенос строки у текста берёт на себя обычный word-wrap. */
  min-width: 0;
}

.choice span {
  /* Тот же приём, что и у .chat__ask (см. её overflow-wrap: anywhere выше) —
     «Шампанское» само по себе одно слово без пробелов, обычный перенос по
     границе слова его не спасает: без anywhere слово вылезало бы из чипа,
     даже когда сама чипа уже готова сжаться (min-width: 0 выше). */
  overflow-wrap: anywhere;
  /* T097: та же пара правил, что у .chat__ask/.chat__echo — chip тоже узкая
     колонка, и обычные многосложные названия («Безалкогольные») выигрывают
     от переноса раньше, чем сработает anywhere. */
  hyphens: auto;
}

.choice:has(:checked) {
  border-color: var(--c-accent);
  background-color: color-mix(in srgb, var(--c-accent) 12%, var(--c-bg));
}

.choice input {
  accent-color: var(--c-accent-deep);
  /* Цель нажатия не меньше 20 px — на телефоне в чекбокс надо попадать */
  width: 1.25em;
  height: 1.25em;
  flex-shrink: 0;
}

/* Лента переписки — вопросы пары и эхо-пузыри гостя друг под другом. Своя
   величина отступа чуть плотнее, чем gap самой .chat (var(--space-md)) —
   реплики одного разговора должны читаться теснее, чем домики-заглушки
   вокруг них. */
.chat__log {
  display: grid;
  gap: var(--space-sm);
}

/* Строка ввода: сюда chat-form.js переносит .chat__control текущего вопроса.
   Верхняя черта отделяет её от ленты — читается как граница поля ввода
   мессенджера, а не просто «ещё одна реплика». */
.chat__composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-xs);
  padding-top: var(--space-xs);
  border-top: 1px solid var(--c-line);
}

.chat__composer-control {
  flex: 1;
  min-width: 0;
}

.chat__composer-send {
  flex-shrink: 0;
}

/* T066: поле ответа под зеркалами без анкеты (#hello/#date/#schedule,
   mirror-reply.js) — тот же .chat__composer, что и в анкете, но кнопка
   отправки здесь значок, а не текст «Ответить» (правка заказчиков
   2026-08-07: «нынешняя кнопка велика»). Круглая кнопка с иконкой
   currentColor — тот же приём, что у .icon-heart в calendar.js (D12). Тот
   же класс переиспользуют кнопка «Ответить» composer'а анкеты
   (chat-form.js) и настоящая кнопка отправки анкеты (.form__submit,
   index.html) — правка заказчиков 2026-08-07 распространилась на все три
   кнопки-«самолётика» на странице. */
.chat__composer-send--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background-color: var(--c-accent-deep);
  color: var(--c-bg);
  cursor: pointer;
  transition: background-color 0.2s;
}

.chat__composer-send--icon:hover {
  background-color: var(--c-ink);
}

.chat__composer-send--icon:disabled {
  opacity: 0.5;
  cursor: default;
}

.icon-send {
  width: 1.1rem;
  height: 1.1rem;
  fill: currentColor;
}

/* Обёртка ленты ответа + composer под зеркалом (T066, mirror-reply.js):
   .mirror__glass сам не grid и не даёт gap, поэтому лента и строка ввода
   едут в собственном grid-контейнере — та же величина отступа, что и между
   репликами в .chat анкеты, плюс отступ сверху от уже готовых реплик из
   разметки. */
.chat__reply-wrap {
  display: grid;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

/* Точки «печатает» между ответом и следующим вопросом — тот же .chat__ask
   пузырь пары, что и у обычного вопроса (тот же хвостик и аватарка), внутри
   которого мигают точки. Сами точки — .mirror__typing-dot, тот же класс
   и анимация, что у зеркал (mirror.js): не плодим второй вариант. */
.chat__typing {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-height: 0.5em;
}

/* T116: список гостей внутри одного эхо-пузыря реплики "guests" — строка на
   спутника, крестик убирает конкретного без режима редактирования и без
   возврата к «Изменить анкету». Список — block-элемент внутри .chat__echo
   (та же заливка/хвостик, что у обычного эхо-текста, чтобы не заводить
   второй вид пузыря), .chat__echo--list снимает паддинг у самого пузыря —
   отступы переезжают на строки списка, иначе крестик оказался бы слишком
   близко к правому краю пузыря для 44px тач-цели. */
.chat__echo--list {
  padding: 0.4em 0;
}

.chat__guest-list {
  display: grid;
  gap: 1px;
  /* Список — не нумерованный, роль каждой строки очевидна из текста, а
     маркер только сузил бы и без того тесную строку на 320px. */
  list-style: none;
}

.chat__guest-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  /* min-height, не просто padding: крестик несёт невидимую тач-цель 44px
     (::before ниже) — если строка ниже 44px, зоны нажатия соседних строк
     на 320px перекрываются и палец промахивается мимо нужного гостя. */
  min-height: 44px;
  padding: 0.2em 0.9em;
  overflow-wrap: anywhere;
  hyphens: auto;
}

.chat__guest-row--empty {
  color: var(--c-muted);
}

/* Крестик «убрать гостя» — видимый кружок меньше тач-цели (тот же приём,
   что у .facts__map-btn::before: 44px добирается невидимым hit-area, а не
   раздутым видимым значком, который на плотном списке строк наезжал бы на
   соседние). Соседние строки на 320px стоят вплотную (T116, критично) —
   без ::before крестики соседних строк перекрывали бы зону нажатия друг
   друга. */
.chat__guest-remove {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 1px solid var(--c-line);
  border-radius: 50%;
  background: none;
  color: var(--c-accent-deep);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
}

.chat__guest-remove::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.chat__guest-remove:hover {
  background-color: var(--c-accent-deep);
  color: var(--c-bg);
}

.form__consent {
  margin-top: calc(var(--space-xs) * -1);
}

.form__submit {
  /* Кнопка — как исходящее сообщение гостя, поэтому у правого края. Размер
     и форма — значок-самолётик (.chat__composer-send--icon, правка
     заказчиков 2026-08-07: «нынешняя кнопка велика»), тут только позиция. */
  justify-self: end;
}

/* Реплика только что раскрылась — короткий сдвиг снизу и проявление.
   Общее правило @media (prefers-reduced-motion: reduce) выше по файлу уже
   глушит все animation-duration через `*` — здесь не нужно дублировать. */
.chat__turn--fresh {
  animation: chat-turn-in 0.35s ease-out;
}

@keyframes chat-turn-in {
  from {
    opacity: 0;
    transform: translateY(0.6em);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 30em) {
  /* max-width теперь 92% на любой ширине (см. базовое правило .chat__ask
     выше) — здесь остаётся только своя аватарка, под которую пересчитывается
     тот же calc(). */
  .chat__ask {
    --avatar-size: 2.2rem; /* иначе поле ввода уже кнопок под ним */
  }

  .chat__reply {
    max-width: 92%;
  }

  .avatar {
    --avatar-size: 2.2rem;
  }
}

/* Всплывающее окно благодарности после отправки анкеты. Открывает серверная
   часть вызовом showModal() (см. комментарий в HTML). */
.rsvp-success {
  max-width: 30rem;
  width: min(30rem, 90vw);
  margin: auto;
  padding: var(--space-lg) var(--space-md);
  border: none;
  border-radius: var(--radius);
  background-color: var(--c-bg);
  text-align: center;
}

.rsvp-success::backdrop {
  /* Через opacity, а не color-mix: тот моложе и на старом браузере молча даёт
     прозрачную подложку — окно повисает над страницей без затемнения. Здесь же
     цвет остаётся ролью палитры, без литерала. */
  background-color: var(--c-ink);
  opacity: 0.55;
}

.rsvp-success__sticker {
  /* Исходник 512x512 — растягивать выше нельзя, размоется. */
  width: min(512px, 100%);
  margin-inline: auto;
}

/* Окно подтверждения перед отправкой анкеты (T087). Та же ширина, что у
   .rsvp-success — по смыслу это соседнее окно той же переписки. Высота —
   не как у остальных диалогов: сводка ответов растёт вместе со списком
   спутников (до девяти полей), поэтому здесь max-height + overflow-y,
   иначе на низких экранах окно вылезло бы за верх/низ вьюпорта — гость
   листал бы страницу под затемнением, а не содержимое диалога. */
.rsvp-confirm {
  max-width: 30rem;
  width: min(30rem, 90vw);
  max-height: min(34rem, 85vh);
  overflow-y: auto;
  margin: auto;
  padding: var(--space-lg) var(--space-md);
  border: none;
  border-radius: var(--radius);
  background-color: var(--c-bg);
  text-align: center;
}

.rsvp-confirm::backdrop {
  /* Тот же приём через opacity, что у .rsvp-success::backdrop выше. */
  background-color: var(--c-ink);
  opacity: 0.55;
}

.rsvp-confirm__title {
  font-size: var(--fs-h3);
  margin-bottom: var(--space-sm);
}

/* Заголовок несёт tabindex="-1" + autofocus (настоящий <dialog>, фокус нужен
   для озвучивания при showModal()) — рамка на нём лишняя, гость её не
   выбирал табом. */
.rsvp-confirm__title:focus {
  outline: none;
}

.rsvp-confirm__summary {
  display: grid;
  gap: var(--space-xs);
  margin: 0 0 var(--space-md);
  padding: 0;
  list-style: none;
  text-align: left;
}

.rsvp-confirm__item {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-xs);
  border-bottom: 1px solid var(--c-line);
}

.rsvp-confirm__value {
  text-align: right;
  /* Имена спутников/номер телефона — им случается быть длиннее подписи,
     перенос строкой лучше горизонтального сжатия соседней колонки. */
  overflow-wrap: anywhere;
}

.rsvp-confirm__actions {
  display: grid;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

/* T087: пятый снимок (couple-embrace) — в окне подтверждения перед
   отправкой, не в окне благодарности (там остался только стикер).
   T114: ширина увеличена с 12rem до 14rem — простому кадру (см.
   .rsvp-confirm__photo-frame ниже) не нужен запас под булавку, зато сам
   снимок читается лучше крупнее; диалог по-прежнему узкий (.rsvp-confirm),
   поэтому до полноразмерного .hello__photo/.rsvp__photo всё равно далеко. */
.rsvp-confirm__photo {
  max-width: 14rem;
  margin-inline: auto;
  margin-bottom: var(--space-md);
}

/* T114: простой кадр вместо .scrapbook-photo — см. комментарий у разметки
   (index.html, .rsvp-confirm__photo). aspect-ratio уже, чем у исходника
   (823×960), намеренно: обрезка по высоте убирает нижнюю часть кадра
   (объятие ниже плеч) и оставляет в видимой области лица — то, что гостю
   и нужно разглядеть в маленьком окне. object-position: top — та же логика,
   что была у .scrapbook-photo__frame img. Рамка и тень — вместо рваной
   бумаги: тонкая линия (--c-line) держит контур на кремовом фоне диалога,
   мягкая тень отделяет фото от текста под ним, но не темнит сам снимок
   (тень не наложена на img, а стоит отдельным box-shadow контейнера). */
.rsvp-confirm__photo-frame {
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  box-shadow: 0 0.4rem 0.9rem color-mix(in srgb, var(--c-ink) 20%, transparent);
}

.rsvp-confirm__photo-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}

/* Фото пары во всех четырёх блоках-переписках (T058/T083/T088): знакомство,
   дата, программа дня, анкета — все используют один и тот же компонент
   .scrapbook-photo (см. ниже), сюда вынесена только общая ширина ячейки,
   отдельная от блока, пока колонки стоят одна под другой. Пятый снимок
   (couple-embrace) — в окне подтверждения анкеты, см. .rsvp-confirm__photo
   выше (T087). */
.hello__photo,
.date__photo,
.schedule__photo,
.rsvp__photo {
  max-width: 24rem;
  margin-inline: auto;
}

/* На двухколоночной раскладке (>=60em, см. .hello/.rsvp__body и парные
   .date__body/.schedule__body ниже) ячейка фото — уже полноценная колонка
   грида (1fr, остаток после --measure), и потолок 24rem — наследие
   одноколоночной раскладки — становится лишним ограничением: без снятия
   фото не смогло бы вырасти при расширении --container (T-задача
   «фотография крупнее», 2026-08-06). На узких экранах (одна колонка) 24rem
   остаётся — там он не даёт фото раздуться на всю ширину контейнера. */
@media (min-width: 60em) {
  .hello__photo,
  .date__photo,
  .schedule__photo,
  .rsvp__photo {
    max-width: none;
    /* Заказчики 2026-08-06: «я бы опустил фотку чуть ниже» — на широком
       экране снимок стоял вровень с верхом пузыря переписки, отступ сдвигает
       его вниз, чтобы композиция не начиналась одной линией с текстом.
       Только здесь (>=60em, там же, где фото становится полноценной
       колонкой грида): на узкой раскладке снимок и так стоит НАД перепиской
       (одна колонка), сдвигать там нечего. */
    margin-top: var(--space-md);
  }
}

/* --------------------------------------------------------------------------
   Скрапбук-фото (T078) — переиспользуемый компонент снимка с рваным бумажным
   полем под ним, булавкой у верхнего края (T080, изначально была лента — см.
   ниже) и статичным наклоном. Заказчики отказались от книжного разворота
   (T076) и от скруглений (T048) в пользу «фото приклеено в альбом». Сейчас
   компонент подключён во всех четырёх оставшихся блоках-переписках —
   знакомство (T058), дата (T088), программа дня (T088) и анкета (T083).
   Пятый снимок (couple-embrace) — в окне подтверждения анкеты (T087,
   .rsvp-confirm__photo), variant-1 переиспользован осознанно: тот снимок
   вне шахматной колонки блоков страницы, ему не нужен отдельный номер.

   Правка по ходу задачи (заказчики, тот же день): изначально маска клипала
   сам снимок «встык» — без поля вокруг. Заказчики попросили другое: рваный
   лист бумаги ВИДЕН вокруг снимка, а снимок внутри него — обычный
   прямоугольник с отступом от края листа. Отсюда разделение слоёв:

   - `.scrapbook-photo` — сам не маскируется (если бы маска висела на нём,
     она клипировала бы и снимок-потомка — `mask` группирует элемент со всеми
     потомками в один слой, как `opacity`/`filter`). Он только держит
     пропорцию МАСКИ (=пропорция листа) через `aspect-ratio`, поворачивает
     композицию на `--scrapbook-tilt` и даёт `padding`, отступающий снимок от
     края листа. `width: calc(100% - 2 * var(--scrapbook-margin))` — запас под
     поворот и высунутую над снимком булавку: без него боундинг-бокс
     повёрнутого угла и головка булавки подпирали бы край контейнера, и на
     переполненной сетке
     (`.hello`/`.schedule__body` на узких экранах = вся ширина `.container`)
     страница поехала бы вбок (FR-008, `body{overflow-x:hidden}` маскирует
     именно такую ошибку, а не чинит).
   - `.scrapbook-photo::before` — лист бумаги: отдельный слой на всю
     `inset: 0` площадь родителя (то есть на всю пропорцию маски, включая
     поле под будущим снимком), именно на нём висит `mask-image` (силуэт
     рваного края), заливка `--m-paper` и зерно `paper-grain.webp` поверх.
     `filter: drop-shadow(...)` — тоже здесь: drop-shadow считается по альфе
     уже замаскированной фигуры, поэтому тень повторяет рваный контур листа,
     а не его исходный прямоугольник. `z-index: -1` внутри стека, который
     создаёт `transform` на `.scrapbook-photo`, — лист гарантированно уходит
     под снимок, а не поверх (без этого позиционированный слой красился бы
     после обычного потока и лёг бы поверх фото).
   - `.scrapbook-photo__frame img` — сам снимок: обычный прямоугольник,
     `object-fit: cover`, `border-radius: 0` (скруглений нет по прямому
     запросу). Никакой маски на нём больше нет.
   - Набор `--scrapbook-*` переменных — единственное, что меняется между
     блоками. Все они собраны в модификаторах `--variant-N`: подключить
     фото в новом блоке — обернуть `<picture>` в
     `<div class="scrapbook-photo scrapbook-photo--variant-N">`. Порядок
     блоков сверху вниз — знакомство (variant-1), дата (variant-4), программа
     дня (variant-3), анкета (variant-2); знак наклона и цвет булавки
     чередуются по позиции в колонке (-,+,-,+ / слейт,графит,слейт,графит),
     а не по номеру варианта — номер только указывает на пару маска+булавка.
     variant-4 — не пятый самостоятельный набор, а альбомная ротация
     variant-2 (photo-mask-5 — та же маска photo-mask-2, повёрнутая на 90°,
     T085): единственный горизонтальный кадр (couple-floor) обязан идти
     только в горизонтальную маску, растягивать портретную под альбом
     запрещено — рваный край смазывается. */
.scrapbook-photo {
  --scrapbook-margin: 1rem; /* запас под поворот + высунутую над снимком булавку */
  --scrapbook-inset: 10%; /* поле листа вокруг снимка */
  position: relative;
  width: calc(100% - 2 * var(--scrapbook-margin));
  margin-inline: var(--scrapbook-margin);
  aspect-ratio: var(--scrapbook-ratio);
  padding: var(--scrapbook-inset);
  transform: rotate(var(--scrapbook-tilt));
}

/* Поле листа на широком экране (T-задача «фотография крупнее», 2026-08-06):
   на одной колонке (узкие экраны) 10% остаётся — там снимок и так некрупный,
   и рваный край листа должен читаться полем в полную силу. На двухколоночной
   раскладке (>=60em, тот же порог, что у .hello/.rsvp__body) поле сужено до
   7% — рваный край всё ещё виден, но не съедает прирост от расширенного
   --container. Убирать поле совсем нельзя: оно и есть смысл компонента
   «бумага под снимком». */
@media (min-width: 60em) {
  .scrapbook-photo {
    --scrapbook-inset: 7%;
  }
}

/* Бумажный лист — фон, зерно и (по умолчанию, без поддержки masking) просто
   прямоугольное поле того же цвета и с той же тенью, что и в маскированном
   виде: запасной вид не «исчезает», а остаётся читаемым полем вокруг фото. */
.scrapbook-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--m-paper);
  background-image: url('../img/decor/paper-grain.webp');
  background-repeat: repeat;
  background-size: 14rem auto;
  filter: drop-shadow(0 0.6rem 0.9rem color-mix(in srgb, var(--c-ink) 32%, transparent));
}

/* Маска — только там, где браузер её реально поддержит (Baseline широкий,
   но запасной вид обязателен по заданию): без @supports старый браузер
   молча игнорирует mask-image, и лист остаётся прямоугольным полем — это и
   есть нужный запасной вид, тут страховка не по отрисовке, а по
   документации намерения. */
@supports (mask-image: none) or (-webkit-mask-image: none) {
  .scrapbook-photo::before {
    mask-image: var(--scrapbook-mask);
    -webkit-mask-image: var(--scrapbook-mask);
    mask-mode: alpha;
    mask-repeat: no-repeat;
    -webkit-mask-repeat: no-repeat;
    mask-size: 100% 100%;
    -webkit-mask-size: 100% 100%;
    mask-position: center;
    -webkit-mask-position: center;
  }
}

.scrapbook-photo__frame {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}

.scrapbook-photo__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  border-radius: 0;
}

/* Булавка (T080) — заменила ленту по замечанию заказчиков: «я заметил, что
   ты масштабируешь ленточки, давай сделаем булавки» — лента растягивалась
   под размер снимка, а это предмет постоянного размера. Один псевдоэлемент
   у верхнего края снимка, воткнутый в лист (не два, как было у ленты —
   булавка одна). Размер — `height` в `rem` (не в `%` и не привязан ни к
   какой величине снимка), ширина считается из него через `aspect-ratio` в
   натуральной пропорции файла: на любой ширине экрана булавка остаётся
   одного и того же размера относительно страницы, а не относительно фото
   (проверка — playwright, отчёт задачи). Цвет — спокойный (белый/графит),
   яркая точка на чёрно-белом снимке била бы в глаза сильнее, чем на цветном
   (решение заказчиков). Чередуется вместе с маской и знаком наклона через
   тот же `--variant-N`. */
.scrapbook-photo__frame::before {
  content: "";
  position: absolute;
  top: var(--scrapbook-pin-top);
  left: 50%;
  height: var(--scrapbook-pin-size, 3.5rem);
  width: auto;
  aspect-ratio: var(--scrapbook-pin-ratio);
  background-image: var(--scrapbook-pin);
  background-repeat: no-repeat;
  background-size: contain;
  pointer-events: none;
  z-index: 2;
  transform: translateX(-50%) rotate(var(--scrapbook-pin-rotate));
}

/* Варианты чередования. Маска/лента/знак наклона меняются вместе одной
   заменой класса — следующий блок просто берёт другой --variant-N. */
.scrapbook-photo--variant-1 {
  --scrapbook-mask: url('../img/decor/photo-mask-1.webp');
  --scrapbook-ratio: 652 / 747;
  --scrapbook-tilt: -3deg;
  /* pin-white.webp снят с сайта (T080a): игла срезана нижней границей кадра
     (брак нарезки, замечен по остающемуся внизу широкому обрубку силуэта
     вместо острия). pin-slate.webp — резкая, игла целая, грифельно-синяя,
     годится под спокойную ЧБ-гамму. */
  --scrapbook-pin: url('../img/decor/pin-slate.webp');
  --scrapbook-pin-ratio: 211 / 383;
  /* Булавка крепится к бумаге НАД снимком, а не поверх его края (T080a):
     остриё (нижний край графики — обе `pin-*` обрезаны по силуэту вплотную,
     bbox проверен PIL) должно лечь чуть выше верхней границы `__frame`,
     а не внутрь фото. top = -(высота + напуск 0.3rem), напуск — чтобы
     остриё казалось воткнутым в лист, а не висело над ним. */
  --scrapbook-pin-top: -3.8rem;
  --scrapbook-pin-rotate: -8deg;
}

.scrapbook-photo--variant-2 {
  --scrapbook-mask: url('../img/decor/photo-mask-2.webp');
  --scrapbook-ratio: 631 / 722;
  --scrapbook-tilt: 3deg;
  --scrapbook-pin: url('../img/decor/pin-graphite.webp');
  --scrapbook-pin-ratio: 220 / 395;
  --scrapbook-pin-top: -3.8rem;
  --scrapbook-pin-rotate: 6deg;
}

/* T088: третий силуэт (photo-mask-3, вырезан вместе с mask-4/mask-5 из
   6354246.psd, T085) — под couple-dress.webp в #schedule. Третья позиция
   колонки повторяет знак наклона и цвет булавки variant-1 (слейт, -3deg) —
   чередование по позиции в колонке, а не по номеру варианта. */
.scrapbook-photo--variant-3 {
  --scrapbook-mask: url('../img/decor/photo-mask-3.webp');
  --scrapbook-ratio: 693 / 794;
  --scrapbook-tilt: -3deg;
  --scrapbook-pin: url('../img/decor/pin-slate.webp');
  --scrapbook-pin-ratio: 211 / 383;
  --scrapbook-pin-top: -3.8rem;
  --scrapbook-pin-rotate: -8deg;
}

/* T088: альбомная ротация variant-2 (photo-mask-5 = photo-mask-2, повёрнута
   на 90°, T085) — единственный горизонтальный кадр couple-floor.webp в
   #date, растягивать портретную маску под него запрещено. Тилт и булавка —
   те же, что у variant-2 (та же пара маска+акцент, только другая
   ориентация холста). */
.scrapbook-photo--variant-4 {
  --scrapbook-mask: url('../img/decor/photo-mask-5.webp');
  --scrapbook-ratio: 722 / 631;
  --scrapbook-tilt: 3deg;
  --scrapbook-pin: url('../img/decor/pin-graphite.webp');
  --scrapbook-pin-ratio: 220 / 395;
  --scrapbook-pin-top: -3.8rem;
  --scrapbook-pin-rotate: 6deg;
}

/* T076: откат книжного разворота — .hello снова просто грид из двух блоков
   (фото и зеркало-переписка), без бумажной обёртки, сгиба и рамок поверх
   .mirror__glass. Раскладка ширин (T071) уцелела: на широком экране
   переписке достаётся её полный var(--measure) = 34rem (а не доля вроде
   6fr, как было в регрессии T058), фото — остаток соседней колонки. */
.hello {
  display: grid;
  gap: var(--space-lg);
  align-items: start;
}

/* Точка разреза подобрана замером (playwright, ширины 320…1600, отчёт
   T067/T071): при 60em (960px) контент .container достигает своего потолка
   904px (60rem минус паддинги), и на нём после вычета 34rem зеркала и
   зазора var(--space-lg) фото ещё получает разумный остаток — не теснее
   одной трети экрана. Ниже 60em то же вычитание уводит фото в неприлично
   узкую полосу — там блоки остаются один под другим. */
@media (min-width: 60em) {
  .hello {
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--measure));
    gap: var(--space-xl);
    /* Правка пользователя 2026-08-08: снимок сидел заметно выше центра
       блока (замер playwright — центр фото на ~216px выше центра секции
       при 1280px). align-items: start прижимал фото к верху ряда, а
       переписка выше и заметно длиннее снимка — на всю высоту строки
       фото просто повисало сверху. center ставит фото по центру строки
       грида (=по высоте зеркала, самого высокого элемента строки), не
       трогая ни зеркало (оно и задаёт высоту строки, центрирование на
       нём незаметно), ни узкую раскладку (там элементы уже в разных
       строках, align-items там ни на что не влияет). */
    align-items: center;
  }
}

/* Анкета (T083) заводит второе место под скрапбук-фото — та же раскладка
   ширин, что у .hello, но колонки в обратном порядке: там фото слева,
   переписка справа, здесь наоборот (заказчики: снимки по замыслу
   чередуются сторонами между блоками). Разметка идёт в естественном
   порядке (сначала .mirror, потом .rsvp__photo), поэтому меняются местами
   именно доли колонок, а не порядок элементов через `order`. */
.rsvp__body {
  display: grid;
  gap: var(--space-lg);
  align-items: start;
}

@media (min-width: 60em) {
  .rsvp__body {
    grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr);
    gap: var(--space-xl);
  }
}

/* T088 (шашка, решение пользователя 2026-08-07): #date продолжает
   чередование сторон — третья позиция в колонке, фото справа, как у
   .rsvp__body (разметка «сначала .mirror, потом .date__photo», доли
   колонок как у .rsvp__body). */
.date__body {
  display: grid;
  gap: var(--space-lg);
  align-items: start;
}

@media (min-width: 60em) {
  .date__body {
    grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr);
    gap: var(--space-xl);
    /* Правка пользователя 2026-08-08 — то же центрирование, что у .hello
       выше (couple-floor тоже сидел заметно выше центра блока, замер —
       ~195px). .schedule__body/.rsvp__body не трогаем: там снимок и так
       близок к центру (замер — 11-13px), новых искажений вносить незачем. */
    align-items: center;
  }
}

/* T088: #schedule — четвёртая позиция в колонке, фото слева, как у .hello
   (разметка «сначала .schedule__photo, потом .mirror», доли колонок как
   у .hello). */
.schedule__body {
  display: grid;
  gap: var(--space-lg);
  align-items: start;
}

@media (min-width: 60em) {
  .schedule__body {
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--measure));
    gap: var(--space-xl);
  }
}

.rsvp-success__title {
  margin-top: var(--space-md);
  font-size: var(--fs-h3);
}

.rsvp-success__text {
  margin-top: var(--space-xs);
}

.rsvp-success__close {
  margin-top: var(--space-md);
}

/* Постоянная плавающая кнопка «Заполнить анкету» (T109, index.html —
   заменяет T099/T100/T104: подсказку-карточку и exit-intent; текст и цвет
   правились в T118 — см. отчёт задачи). position: fixed в правом нижнем
   углу, видна с загрузки страницы — на 320px это то же место, где стоит
   парадный первый экран с венком, поэтому кнопка нарочно компактная (не
   полноразмерная карточка T104), чтобы не спорить с композицией обложки.
   rsvp-float.js прячет/показывает через `hidden` (не opacity — иначе кнопка
   осталась бы кликабельной/в таб-порядке поверх анкеты). z-index ниже, чем
   был у T104 (20): нативные <dialog> карты и успеха всегда в top layer
   браузера и выше любого z-index на странице — конфликтов с ними нет в
   любом случае.

   Фон — --c-cta-gold (T118, замечание пользователя 2026-08-12): прежний
   --c-accent-deep читался тёмным и сливался с кремовой страницей, не звал
   на анкету. --c-cta-gold — узкая роль именно под эту кнопку (обоснование
   и расчёт контраста — комментарий у объявления токена, PALETTE выше). */
.rsvp-float {
  position: fixed;
  right: var(--space-sm);
  bottom: var(--space-sm);
  z-index: 15;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.25rem 0.9rem 0.25rem 0.25rem;
  border: none;
  /* 999px, не var(--radius) (2px, общий язык скруглений страницы) — компактный
     плавающий чип читается ближе к .button (border-radius: 1.1em), пилюля
     здесь выразительнее прямых углов на мелком элементе. */
  border-radius: 999px;
  background-color: var(--c-cta-gold);
  color: var(--c-bg);
  box-shadow: 0 0.4rem 1rem rgb(0 0 0 / 0.25);
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  line-height: 1;
  transition: background-color 0.2s, transform 0.15s;
}

.rsvp-float:hover {
  background-color: var(--c-ink);
}

.rsvp-float[hidden] {
  display: none;
}

/* Обёртка фото+бейджа — квадрат ровно по размеру круглого кадра, точка
   отсчёта для абсолютно спозиционированного бейджа-значка (ниже). */
/* Размер кадра: 4.4rem, а не 2.4rem. Замечание пользователя 2026-08-13 —
   при 2.4rem кадр рендерился 35x35 px, на нём лиц молодожёнов не разглядеть,
   а значок-бейдж выходил 17x17 и читался пятном. Приоритет здесь смещён
   намеренно: кнопка должна ЗВАТЬ, компактность вторична. */
.rsvp-float__avatar {
  position: relative;
  flex-shrink: 0;
  width: 4.4rem;
  height: 4.4rem;
}

.rsvp-float__photo {
  width: 100%;
  height: 100%;
  border: 2px solid var(--c-bg);
  border-radius: 50%;
  object-fit: cover;
  object-position: top center;
}

/* Бейдж «лист с галочкой» (T118) — силуэт из assets/extracted/dfd8b779-…
   (tools/rsvp_icon_extract.py), залит --c-accent. НЕ на золотом фоне
   кнопки напрямую (тот же контраст-конфликт, что решался для текста, —
   см. --c-cta-gold выше), а на собственной кремовой монете внахлёст на
   фото, тем же языком, что и кремовая обводка фото рядом. */
.rsvp-float__badge {
  position: absolute;
  right: -0.25rem;
  bottom: -0.25rem;
  /* 1.95rem вместо 1.15rem — соразмерно увеличенному кадру (см. выше):
     при прежнем размере значок рендерился 17x17 px и не читался. */
  width: 1.95rem;
  height: 1.95rem;
  padding: 0.3rem;
  border-radius: 50%;
  background-color: var(--c-bg);
  box-shadow: 0 0 0 2px var(--c-bg);
  object-fit: contain;
}

.rsvp-float__label {
  white-space: nowrap;
}

/* ПОСТОЯННОЕ покачивание, без пауз — прямое решение пользователя 2026-08-13:
   «сделай пульсацию постоянной, пока открыта страница».
   ВНИМАНИЕ, это осознанная отмена прежнего правила фичи. Дважды (T094, T101)
   здесь уходили от непрерывного движения именно потому, что оно читается как
   «индикатор загрузки»; вариант с паузой в 88% цикла пользователь посмотрел
   живьём и отверг — движение показалось незаметным. Не возвращать паузу без
   его нового слова.
   Амплитуда оставлена малой (0.35rem): при непрерывном цикле она и решает,
   получится мягкое дыхание или назойливое дёрганье. */
.rsvp-float {
  animation: rsvp-float-breathe 2.8s ease-in-out infinite;
}

@keyframes rsvp-float-breathe {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-0.35rem);
  }
}

/* Однократный импульс внимания (T118) — НЕ постоянная пульсация: на этой
   фиче дважды уходили от эффекта «индикатор загрузки» (T094, T101).
   Класс добавляет rsvp-float.js ровно один раз за сеанс — когда гость
   впервые проскроллил мимо анкеты, не заполнив её (см. модуль). Конечное
   состояние анимации (100%) равно исходному — после отыгрыша кнопка
   визуально не отличается от состояния до импульса.
   Обе анимации правят transform, поэтому перечислены вместе и складываются
   через animation-composition: add. Где свойство не поддержано, импульс на
   0.7с просто перекрывает покачивание — деградация безобидная. */
.rsvp-float--pulse {
  animation:
    rsvp-float-breathe 2.8s ease-in-out infinite,
    rsvp-float-pulse 0.7s ease-in-out 2;
  animation-composition: add;
}

@keyframes rsvp-float-pulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.08);
  }
}

/* Явный animation: none (не расчёт на общее сжатие animation-duration) — на
   prefers-reduced-motion в этой фиче горели дважды, см. комментарий у
   .icon-globe. rsvp-float.js эту ветку не проверяет отдельно: медиа-запрос
   гасит анимацию сам, класс можно добавлять безусловно. */
@media (prefers-reduced-motion: reduce) {
  .rsvp-float,
  .rsvp-float--pulse {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   Подвал
   -------------------------------------------------------------------------- */

.footer {
  padding-block: var(--space-lg);
  text-align: center;
}

.footer__divider {
  margin-bottom: var(--space-sm);
}

.footer__names {
  /* --fs-h3 — общая роль, её нельзя было просто увеличить (задело бы все
     остальные элементы на странице). Подвал — отдельная подпись без своей
     карточки, укрупнена локально по правке заказчика 2026-08-07. */
  font-size: calc(var(--fs-h3) * 1.3);
  /* .accent красит имя в --c-accent-deep — на реальном фоне под подвалом
     (тень волны у горизонта, тот же провал, что описан у .cover__date выше)
     замер снимком дал контраст ~1.7:1. --c-ink того же фона держит ~2.9:1 —
     всё ещё ниже 4.5:1 (тот же принятый размен .page-bg::after, см. его
     комментарий), но заметно лучше, и та же роль, что у имён на обложке. */
  color: var(--c-ink);
  /* T097: имя переносить нельзя, см. комментарий у .cover__name. */
  hyphens: none;
}

.footer__date {
  margin-top: 0.4em;
  /* Та же локальная надбавка к --fs-small, что у .footer__names к --fs-h3
     выше — правка заказчика 2026-08-07 («подпись в подвале мелкая»). */
  font-size: calc(var(--fs-small) * 1.3);
  /* Правка пользователя 2026-08-07 («шрифт даты в подвале не соответствует
     обложке»): класс .small (font-family: var(--font-ui), Inter) снят из
     разметки — здесь остаётся Lora по умолчанию (font-family не переопределён,
     как и у .cover__date), плюс те же капслок и разрядка, что на обложке —
     обе даты набраны одинаково, отличается только кегль (подвал крупнее,
     см. надбавку выше). */
  letter-spacing: 0.22em;
  text-transform: uppercase;
  /* Тот же приём, что у .cover__date — на узких экранах строка рвётся
     на две неровные части, balance выравнивает их по длине. */
  text-wrap: balance;
  /* Раньше — .muted (самый низкоконтрастный цвет палитры, тот же провал,
     что был у .cover__date, см. её комментарий): дата отличалась цветом от
     имени строкой выше. --c-ink — та же роль, что теперь и у .footer__names
     и у .cover__date (замер контраста см. там), все три строки одного
     цвета. */
  color: var(--c-ink);
}

/* --------------------------------------------------------------------------
   Страница политики конфиденциальности (T025) — служебная страница,
   отдельная вкладка (index.html ссылается на неё дважды, target="_blank").
   Оформление то же (шрифты, палитра, .page-bg), но без декоративной вёрстки
   приглашения: обычный текст в измеренную колонку.
   -------------------------------------------------------------------------- */

.privacy {
  max-width: var(--measure);
  margin-inline: auto;
}

.privacy__back {
  margin-bottom: var(--space-md);
}

.privacy h2 {
  margin-top: var(--space-lg);
}

.privacy h3 {
  margin-top: var(--space-md);
}

.privacy__credits {
  padding-left: 1.2em;
}

.privacy__credits li + li {
  margin-top: var(--space-xs);
}
