/* Стили web MoiMatch. Значения токенов — static/css/tokens.css (docs/design.md) */
:root {
  --mm-radius-btn: 14px;
  --mm-radius-card: 20px;
}
/* Фон страницы — токен темы: чёрный в тёмной, белый в светлой (docs/themes.md §2) */
body { background: var(--mm-bg); }
[data-bs-theme="dark"] .card { background: var(--mm-surface); border-color: var(--mm-border); }

.brand { display: inline-flex; align-items: center; gap: 10px; }
/* Логотип — файл иконки приложения (docs/design.md §6). Своего фона и скругления
   не добавляем: и градиент, и радиус углов уже внутри иконки. */
.brand-logo { display: inline-block; width: 40px; height: 40px; }
.brand-name {
  font-size: 22px; font-weight: 700;
  background: var(--mm-gradient);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.nav-link-mm { color: var(--mm-primary); text-decoration: none; font-weight: 600; }
.nav-link-mm:hover { text-decoration: underline; }
.link-mm { color: var(--mm-primary); }

.btn-mm {
  background: var(--mm-gradient); color: #fff; border: 0;
  border-radius: var(--mm-radius-btn); padding: 13px 28px; font-weight: 600;
}
.btn-mm:hover { color: #fff; filter: brightness(0.94); }
.btn-mm-outline {
  background: transparent; color: var(--mm-primary);
  border: 1.5px solid var(--mm-primary); border-radius: var(--mm-radius-btn);
  padding: 12px 26px; font-weight: 600;
}
.btn-mm-outline:hover { color: var(--mm-secondary); border-color: var(--mm-secondary); }

.card { border-radius: var(--mm-radius-card); }
/* Объявление вместо формы: например, регистрация закрыта в админке (A12) */
.note-box { background: var(--mm-surface2); border: 1px solid var(--mm-border);
  border-radius: var(--mm-radius-card); padding: 22px 20px; }
.card-mm-accent { border: 1.5px solid transparent; position: relative; }
.card-mm-accent::before {
  content: ""; position: absolute; inset: -1.5px; border-radius: var(--mm-radius-card);
  background: var(--mm-gradient); z-index: -1;
}

.hero { padding: 72px 0 48px; text-align: center; }
.hero h1 {
  font-size: 44px; font-weight: 800;
  background: var(--mm-gradient);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero p.lead { max-width: 560px; margin: 16px auto 32px; }

.status-icon { font-size: 56px; line-height: 1; }

/* --- Ожидание подтверждения почты (S05) --- */
/* Карточка по центру: знак письма, адрес и заметная плашка про «Спам».
   Кнопки «отправить ещё раз» нет (Р-83) — вместо неё и сделана плашка. */
.wait-card { max-width: 460px; margin: 24px auto; text-align: center; }
.wait-card .wait-mark { width: 64px; height: 64px; margin: 0 auto 16px;
  border-radius: var(--mm-r-card); background: var(--mm-gradient);
  display: flex; align-items: center; justify-content: center; }
.wait-card .wait-mark svg { width: 32px; height: 32px; color: #fff; }
.wait-card .wait-email { font-size: 18px; font-weight: 600; color: var(--mm-primary);
  word-break: break-all; margin-bottom: 12px; }
.wait-spam { display: flex; gap: 12px; text-align: start; margin-top: 20px;
  padding: 12px 14px; border-radius: var(--mm-r-field);
  background: rgba(245, 158, 11, .12); }
.wait-spam .ico { flex-shrink: 0; color: var(--mm-warning); display: inline-flex; }
.wait-spam .ico svg { width: 20px; height: 20px; }
/* --- Капча (S03, S04): зеркало андроидного CaptchaBox.kt ---
   Картинка приходит одного размера (340×120 px) и раньше рисовалась в нём же:
   на широком блоке она сидела маленьким прямоугольником слева, а рядом висела
   кнопка-иконка «⟳». Теперь картинка занимает всю ширину блока на любом экране
   (рамке задана та же пропорция, поэтому форма не подпрыгивает, пока картинка
   грузится), а обновление — текстовой кнопкой под ней, у правого края. */
/* Ширина блока — родные 340 px картинки, по центру формы. На телефоне экран
   уже этого, и блок занимает всю ширину (как и на Android); на широком экране
   растягивать нечего: форма шириной 560 px превращала капчу в огромный
   мутный прямоугольник — картинка приходит одного размера и вверх только
   интерполируется. */
.captcha-box { display: flex; flex-direction: column; gap: 8px;
  width: 100%; max-width: 340px; margin-inline: auto; }
.captcha-frame {
  width: 100%; aspect-ratio: 340 / 120; overflow: hidden;
  border-radius: var(--mm-r-field); border: 1px solid var(--mm-border);
  background: var(--mm-surface);
}
/* Пока картинки нет, в рамке крутится тот же «бегущий блик», что и у прочих
   заглушек сайта (.skeleton), а сама картинка проявляется за секунду. Класс
   is-ready ставит auth.js — после того, как картинка декодирована: раньше
   времени проявлять нечего. Уход старой картинки быстрее появления новой
   (0.35 с против 1 с): ожидание не должно начинаться с долгого прощания. */
/* cover, а не contain: пропорция рамки и картинки одна и та же (340×120), но
   при дробной ширине блока contain оставлял по бокам просвет подложки —
   тонкие тёмные полоски по краям. cover в худшем случае срежет доли пикселя. */
.captcha-img { display: block; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity .35s ease; }
.captcha-box.is-ready .captcha-img { opacity: 1; transition: opacity 1s ease; }
.captcha-box.is-ready .captcha-frame { background: transparent; }
.captcha-box.is-ready .captcha-frame::after { content: none; }
@media (prefers-reduced-motion: reduce) {
  .captcha-img, .captcha-box.is-ready .captcha-img { transition: none; }
}
/* На страницах клиента это <button> (картинку меняет auth.js), а на странице
   удаления аккаунта — ссылка на саму страницу: там нет JS, и новая капча
   приходит перезагрузкой. Отсюда снятое подчёркивание — чтобы обе выглядели
   одинаково. */
.captcha-refresh {
  align-self: flex-end; background: none; border: 0; padding: 2px 0;
  color: var(--mm-primary); font-weight: 600; line-height: 1.2;
  text-decoration: none;
}
.captcha-refresh:hover { text-decoration: underline; }
/* подпись поля идёт сразу над ним, без нижнего отступа Bootstrap:
   расстояния в блоке держит gap контейнера */
.captcha-box .form-label { margin-bottom: 0; }

/* ================= Веб-клиент приложения (Phase 5) ================= */
body { color: var(--mm-text); }
.text-mm2 { color: var(--mm-text2); }
.surface { background: var(--mm-surface); }

/* --- Каркас: нижний таббар (моб) / левый сайдбар (десктоп) --- */
.app-shell { min-height: 100vh; display: flex; }
/* Нижний отступ обязан учитывать safe-area: на iPhone таббар выше своей
   --mm-h-tabbar ровно на env(safe-area-inset-bottom), и без этой добавки
   последняя полоска страницы навсегда оставалась под панелью. */
.app-content { flex: 1 1 auto; width: 100%; max-width: 560px; margin: 0 auto;
  padding: 12px 16px calc(var(--mm-h-tabbar) + 24px + env(safe-area-inset-bottom)); }
.app-content.wide { max-width: none; padding: 0; }
/* Меню — на фоне страницы (в тёмной теме чёрное), а его край показывает
   яркая градиентная линия: подложка другого оттенка выглядела «плашкой», а
   нейтральный бордер на чёрном не читался (docs/themes.md §4). */
.tabbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 1030;
  height: calc(var(--mm-h-tabbar) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  display: flex; align-items: center; justify-content: space-around;
  background: var(--mm-bg); border-top: 0; }
/* Линия — абсолютным псевдоэлементом: в потоке она стала бы ещё одним
   flex-элементом и растолкала бы вкладки */
.tabbar::before { content: ""; position: absolute; left: 0; right: 0; top: 0;
  height: 1.5px; background: var(--mm-gradient); }
.tabbar a { display: flex; flex-direction: column; align-items: center; gap: 2px;
  color: var(--mm-text2); text-decoration: none; position: relative; padding: 6px 18px; }
.tabbar a svg { width: 28px; height: 28px; }
.tabbar a.active { color: var(--mm-primary); }
.tabbar a.active::after { content: ""; width: 5px; height: 5px; border-radius: 999px;
  background: var(--mm-gradient); position: absolute; bottom: 0; }
.sidebar { display: none; }
@media (min-width: 992px) {
  .tabbar { display: none; }
  .app-content { padding-bottom: 24px; }
  /* Тот же приём, что и у таббара: фон страницы + градиентная граница */
  .sidebar { display: flex; flex-direction: column; gap: 6px; width: 220px;
    padding: 20px 12px; border-right: 0; background: var(--mm-bg);
    position: sticky; top: 0; height: 100vh; flex-shrink: 0; }
  .sidebar::before { content: ""; position: absolute; top: 0; bottom: 0;
    inset-inline-end: 0; width: 1.5px; background: var(--mm-gradient); }
  .sidebar .brand-logo { width: 32px; height: 32px; }
  .sidebar a { display: flex; align-items: center; gap: 12px; padding: 12px 14px;
    border-radius: var(--mm-r-btn); color: var(--mm-text); text-decoration: none; font-weight: 600; }
  .sidebar a svg { width: 24px; height: 24px; }
  .sidebar a.active { background: var(--mm-gradient); color: #fff; }
  .sidebar a:not(.active):hover { background: var(--mm-surface); }
}
.app-topbar { display: flex; align-items: center; justify-content: space-between;
  padding: 10px 0; gap: 8px; min-height: 56px; }
.app-topbar .title { font-size: 22px; font-weight: 700; margin: 0 auto;
  background: var(--mm-gradient); -webkit-background-clip: text;
  background-clip: text; color: transparent; }
.icon-btn { width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--mm-border);
  background: var(--mm-surface2); color: var(--mm-text); display: inline-flex;
  align-items: center; justify-content: center; flex-shrink: 0; }
.icon-btn svg { width: 22px; height: 22px; }
.icon-btn:active { filter: brightness(.92); }
/* --- Активные сессии (S50) --- */
.session-row { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--mm-border); }
.session-row:last-child { border-bottom: 0; }

/* --- Ввод интересов (профиль и фильтры) --- */
.ints-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.ints-chips:not(:empty) { margin-bottom: 8px; }
.ints-row { display: flex; gap: 8px; }
.ints-row .form-control { min-width: 0; }
.ints-sug { display: flex; flex-wrap: wrap; gap: 8px; }
.ints-sug:not(:empty) { margin-top: 8px; }
.ints .form-text { margin-top: 6px; }

/* Кнопка-иконка рядом с полем ввода (например, «указать точку на карте») */
.btn-icon { flex: 0 0 52px; width: 52px; padding: 0; display: inline-flex;
  align-items: center; justify-content: center; }
.btn-icon svg { width: 22px; height: 22px; }

/* --- Поля и формы --- */
.form-control, .form-select { background: var(--mm-surface); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-field); min-height: var(--mm-h-field); color: var(--mm-text); }
.form-control:focus, .form-select:focus { border: 1.5px solid var(--mm-primary);
  box-shadow: 0 0 0 4px rgba(255,46,99,.15); background: var(--mm-surface); color: var(--mm-text); }
.btn-mm-lg { width: 100%; height: var(--mm-h-btn); font-size: 16px; }
.btn-mm:disabled { opacity: .4; }

/* --- Свой выпадающий список (Р-176) ---
   Поле оформлено темой, а список, который открывает браузер, — нет: он
   рисуется системой, прямыми углами и чужим шрифтом. Накладка повторяет поле
   и открывает свой список; настоящий `<select>` остаётся под ней. */
.mm-select { position: relative; }
/* Само поле не убираем из разметки (оно источник значения) и не прячем через
   `display: none`: так оно остаётся доступным для форм и автозаполнения */
.mm-select > select { position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none; }
.mm-select-btn { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; width: 100%; min-height: var(--mm-h-field); padding: 0 14px;
  text-align: left; background: var(--mm-surface); color: var(--mm-text);
  border: 1px solid var(--mm-border); border-radius: var(--mm-r-field); }
.mm-select-caret { display: inline-flex; color: var(--mm-text2); flex: 0 0 auto;
  transition: transform .15s; }
.mm-select.is-open .mm-select-btn { border: 1.5px solid var(--mm-primary);
  box-shadow: 0 0 0 4px rgba(255,46,99,.15); }
.mm-select.is-open .mm-select-caret { transform: rotate(180deg); color: var(--mm-primary); }
.mm-select-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mm-select-pop { position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30;
  max-height: min(320px, 50vh); overflow-y: auto; padding: 6px;
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-card); box-shadow: 0 14px 34px rgba(0,0,0,.28);
  scrollbar-width: thin; scrollbar-color: var(--mm-primary) transparent; }
.mm-select-pop::-webkit-scrollbar { width: 8px; }
.mm-select-pop::-webkit-scrollbar-track { background: transparent; }
.mm-select-pop::-webkit-scrollbar-thumb { border-radius: 999px;
  background: var(--mm-gradient); border: 2px solid var(--mm-surface2); }
.mm-select-opt { display: block; width: 100%; text-align: left; border: 0;
  background: none; color: var(--mm-text); padding: 10px 12px; border-radius: 10px; }
.mm-select-opt:hover { background: var(--mm-surface); }
.mm-select-opt.is-active { background: rgba(255,46,99,.12); color: var(--mm-primary);
  font-weight: 600; }
/* Карточка, из которой открыт список, на время показа поднимается над
   соседями (Р-177). Своего `z-index` списку не хватает: у `.menu-card` стоит
   `animation: fadeIn … both`, а заливаемая анимация непрозрачности делает
   карточку контекстом наложения навсегда — и карточки ниже по разметке
   рисуются поверх содержимого верхней. Поднимаем саму карточку: в общем
   контексте страницы она встаёт выше соседей вместе со списком. */
.mm-select-host { position: relative; z-index: 40; }

/* --- Чипы интересов --- */
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px;
  border-radius: 999px; background: rgba(255,46,99,.12); color: var(--mm-primary);
  font-weight: 600; font-size: 14px; border: 0; }
.chip .x { cursor: pointer; opacity: .7; }
.chip-onphoto { background: rgba(0,0,0,.45); color: #fff; font-weight: 500; }
.chip-select { background: var(--mm-surface); color: var(--mm-text);
  border: 1px solid var(--mm-border); cursor: pointer; }
.chip-select.active { background: rgba(255,46,99,.12); color: var(--mm-primary);
  border-color: var(--mm-primary); }

/* --- Сегмент-контрол (пол) --- */
.seg { display: flex; background: var(--mm-surface); border: 1px solid var(--mm-border);
  border-radius: 999px; padding: 4px; position: relative; }
.seg button { flex: 1; border: 0; background: transparent; padding: 10px 6px;
  border-radius: 999px; font-weight: 600; color: var(--mm-text2); z-index: 1; }
.seg button.active { color: #fff; background: var(--mm-gradient); transition: background .2s; }

/* --- Колода карточек (S30) --- */
.deck-wrap { position: relative; height: min(72vh, 640px); user-select: none; }
/* Без собственного border: фото рисуется под ним и вылезало полосками из-под
   градиентной рамки. Рамка — только кольцо ::before поверх фото. */
.deck-card { position: absolute; inset: 0; border-radius: var(--mm-r-photo);
  overflow: hidden; background: var(--mm-surface) center/cover no-repeat;
  border: 0; touch-action: none; cursor: grab;
  box-shadow: 0 8px 30px rgba(0,0,0,.18); }
.deck-card::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 3px; background: var(--mm-gradient); z-index: 3;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
.deck-card .grad-bottom { position: absolute; left: 0; right: 0; bottom: 0; height: 45%;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,.72)); }
.deck-card .info { position: absolute; left: 16px; right: 16px; bottom: 14px; color: #fff; }
.deck-card .info h2 { font-size: 28px; font-weight: 700; margin: 0; text-shadow: 0 1px 6px rgba(0,0,0,.4); }
.deck-card .info .sub { opacity: .92; font-size: 14px; }
.deck-card .chips { position: absolute; left: 12px; top: 12px; right: 56px;
  display: flex; flex-wrap: wrap; gap: 6px; }
.deck-card .flag-btn { position: absolute; right: 12px; top: 12px; }
.deck-stamp { position: absolute; top: 40px; font-size: 40px; font-weight: 800;
  padding: 6px 18px; border-radius: 12px; border: 4px solid; opacity: 0;
  text-transform: uppercase; pointer-events: none; }
.deck-stamp.like { left: 24px; color: var(--mm-success); border-color: var(--mm-success);
  transform: rotate(-14deg); }
.deck-stamp.pass { right: 24px; color: var(--mm-error); border-color: var(--mm-error); transform: rotate(14deg); }
.deck-actions { display: flex; justify-content: center; gap: 28px; margin-top: 16px; }
.act-btn { width: 64px; height: 64px; border-radius: 999px; border: 1.5px solid var(--mm-border);
  background: var(--mm-surface2); display: inline-flex; align-items: center; justify-content: center; }
.act-btn svg { width: 30px; height: 30px; }
.act-btn.pass { color: var(--mm-pass); }
.act-btn.like { background: var(--mm-gradient); color: #fff; border: 0; }
.act-btn.small { width: 48px; height: 48px; align-self: center; }
.act-btn:active { transform: scale(.94); }

/* --- Мэтч-оверлей --- */
.match-overlay { position: fixed; inset: 0; z-index: 2000; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 18px;
  background: rgba(0,0,0,.82); color: #fff; text-align: center; }
.match-overlay h1 { font-size: 40px; font-weight: 800;
  background: var(--mm-gradient); -webkit-background-clip: text; background-clip: text; color: transparent; }
.match-avatars { display: flex; }
.match-avatars img { width: 120px; height: 120px; border-radius: 999px; object-fit: cover;
  border: 3px solid #fff; }
.match-avatars img:last-child { margin-left: -24px; }

/* --- Гео-карта (S20) --- */
.geo-wrap { position: fixed; inset: 0;
  bottom: calc(var(--mm-h-tabbar) + env(safe-area-inset-bottom)); }
@media (min-width: 992px) { .geo-wrap { left: 220px; bottom: 0; } }
#map { width: 100%; height: 100%; }
/* Уровни наложения баблов не выходят за пределы карты (Р-187).

   Маркеры получают z-index в сотни миллионов — так задан порядок анкет между
   собой (Р-186). Контейнер маркеров своей плоскости не заводит, поэтому эти
   числа сравнивались с чем попало на странице и накрывали всё, что лежит
   рядом с картой: верхнюю панель, кнопки зума, копирайт (у них z-index 2–5).
   `isolation` заводит контейнеру собственную плоскость: внутри порядок анкет
   прежний, наружу он больше не выходит, и любой орган управления оказывается
   выше карты — в том числе выше рекламного блока. */
.maplibregl-canvas-container { isolation: isolate; }
.geo-topbar { position: absolute; top: 10px; left: 12px; right: 12px; z-index: 5;
  display: flex; align-items: center; justify-content: space-between; pointer-events: none; }
.geo-topbar > * { pointer-events: auto; }
.geo-title { font-size: 20px; font-weight: 700; padding: 8px 18px; border-radius: 999px;
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  color: var(--mm-text); }
.geo-title span { background: var(--mm-gradient); -webkit-background-clip: text;
  background-clip: text; color: transparent; }
/* Свой столбик управления картой (Р-188) — повторяет андроидный
   `MapControls`: снизу «−», над ним «+», ещё выше «моё место». Все три кнопки
   те же, что в шапке экрана; знаки масштаба собраны в один блок со
   скруглением и разделителем, «моё место» стоит отдельным кружком. Штатный
   столбик MapLibre убран: он рисуется своим стилем, мимо наших токенов. */
.map-tools { position: absolute; right: 12px; bottom: 12px; z-index: 5;
  display: flex; flex-direction: column; align-items: center; gap: 8px; }
.map-zoom { display: flex; flex-direction: column; overflow: hidden;
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  border-radius: var(--mm-radius-btn); }
.map-zoom .icon-btn { border: 0; border-radius: 0; background: transparent; }
.map-zoom .icon-btn + .icon-btn { border-top: 1px solid var(--mm-border); }
/* Подпись источника — штатный компактный значок «i» в левом нижнем углу
   (Р-188), перекрашенный нашими токенами: белая пилюля MapLibre посреди
   тёмной карты выглядела заплаткой. Убрать её совсем нельзя — тайлы
   OpenFreeMap построены на данных OpenStreetMap, и ссылку на источник требует
   лицензия (ODbL); свёрнутая в значок, она никому не мешает. */
.maplibregl-ctrl-attrib.maplibregl-compact { margin: 6px; min-height: 22px;
  background: var(--mm-surface2); color: var(--mm-text2);
  border: 1px solid var(--mm-border); border-radius: 999px; font-size: 11px; }
.maplibregl-ctrl-attrib a { color: var(--mm-text2); }
.maplibregl-ctrl-attrib-button { background-color: transparent; }
/* Значок нарисован чёрным по прозрачному — в тёмной теме выворачиваем */
[data-bs-theme="dark"] .maplibregl-ctrl-attrib-button { filter: invert(1); opacity: .75; }

/* Бабл проявляется целиком и только когда фото загрузилось: пустой кружок с
   именем, который через секунду «дорисовывается», выглядел как сбой.
   Проявляется вложенный слой, а не сам маркер: MapLibre пишет маркеру
   непрозрачность инлайном (_updateOpacity), перебивая наш opacity — бабл
   успевал мигнуть целиком, прежде чем начиналась анимация.
   И именно анимация, а не переход: маркер попадает в DOM и получает класс в
   одном кадре, начального состояния браузер не видит — переход не запускался
   вовсе (проверено getAnimations()). */
.map-bubble { display: flex; flex-direction: column; align-items: center; cursor: pointer; }
.map-bubble .body { display: flex; flex-direction: column; align-items: center; opacity: 0; }
.map-bubble .body.shown { animation: fadeIn 1s ease forwards; }
.map-bubble img { width: 48px; height: 48px; border-radius: 999px; object-fit: cover;
  border: 2px solid transparent; background:
    linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box,
    var(--mm-gradient) border-box; }
.map-bubble .cap { margin-top: 2px; font-size: 12px; font-weight: 600; color: var(--mm-text);
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  padding: 1px 8px; border-radius: 999px; white-space: nowrap; }
/* Своя карточка на карте: золотая обводка вместо брендовой — сразу видно,
   где стоишь сам (docs/design.md §1) */
.map-bubble.me img { border-width: 2.5px; background:
  linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box,
  var(--mm-gradient-gold) border-box; }
.map-bubble.me .cap { border-color: var(--mm-gold); }
/* Сердечко у лайкнутой анкеты на карте (Р-153): цвет бренда, как у ♥ в колоде */
.map-bubble .cap .liked { color: var(--mm-primary); }
/* Точка «в сети» (Р-183): та же, что в списке диалогов, но мельче — подпись
   бабла набрана 12-м кеглем, и обычные 10px спорили бы с текстом */
.map-bubble .cap .map-dot { width: 7px; height: 7px; margin-left: 5px;
  vertical-align: middle; }

/* --- Выбор точки на карте (S14): модалка с отступами 5% от краёв --- */
.map-pick { background: rgba(0, 0, 0, .5); }
.map-pick-dialog { position: fixed; inset: 5%; margin: auto;
  max-width: 900px; max-height: 760px;
  border-radius: 20px; overflow: hidden; background: var(--mm-surface2);
  border: 1px solid var(--mm-border); box-shadow: 0 20px 60px rgba(0, 0, 0, .35); }
/* Размер задаём явно, а не через inset: MapLibre подключает свой CSS позже
   нашего и его `.maplibregl-map { position: relative }` перебивает absolute —
   контейнер схлопывался в нулевую высоту, и карта не показывалась вовсе */
.map-pick-map { position: absolute; inset: 0; width: 100%; height: 100%; }
.map-pick-btn { position: absolute; top: 12px; z-index: 3; width: 44px; height: 44px;
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .18); }
.map-pick-locate { left: 12px; }
.map-pick-close { right: 12px; }
.map-pick-foot { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  padding: 24px 14px calc(14px + env(safe-area-inset-bottom));
  background: linear-gradient(180deg, transparent, var(--mm-surface2) 42%); }
.map-pick-hint { text-align: center; font-size: 13px; color: var(--mm-text2);
  margin-bottom: 8px; }
/* Зум и копирайт карты не должны уезжать под нижнюю панель модалки */
.map-pick .maplibregl-ctrl-bottom-right { bottom: 112px; }
/* Пульсирующая точка: видно, что место выбрано именно здесь */
.pick-dot { width: 20px; height: 20px; border-radius: 999px; border: 2.5px solid #fff;
  background: var(--mm-gradient); box-shadow: 0 2px 8px rgba(0, 0, 0, .4); position: relative; }
.pick-dot::after { content: ""; position: absolute; inset: -5px; border-radius: 999px;
  border: 2px solid var(--mm-primary); animation: pickPulse 1.7s ease-out infinite; }
@keyframes pickPulse {
  0% { transform: scale(.65); opacity: .85; }
  100% { transform: scale(2); opacity: 0; }
}

/* --- Списки: диалоги, мэтчи, лайки --- */
.dlg-item { display: flex; align-items: center; gap: 12px; padding: 10px 4px;
  text-decoration: none; color: var(--mm-text); border-bottom: 1px solid var(--mm-border); }
.avatar { width: 56px; height: 56px; border-radius: 999px; object-fit: cover; flex-shrink: 0;
  border: 2px solid transparent; background:
    linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box, var(--mm-gradient) border-box; }
.avatar.lg { width: 96px; height: 96px; }
.dot-online { width: 10px; height: 10px; background: var(--mm-online); border-radius: 999px;
  display: inline-block; flex-shrink: 0; }
.dot-unread { width: 10px; height: 10px; background: var(--mm-unread); border-radius: 999px;
  display: inline-block; }
.dlg-item .prev { color: var(--mm-text2); font-size: 14px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.grid-card { border-radius: var(--mm-r-card); overflow: hidden; position: relative;
  aspect-ratio: 10/16; background: var(--mm-surface) center/cover; display: block; }
.grid-card .cap { position: absolute; left: 0; right: 0; bottom: 0; padding: 22px 10px 8px;
  color: #fff; font-weight: 600; background: linear-gradient(180deg, transparent, rgba(0,0,0,.7)); }
.grid-card .row-btns { position: absolute; top: 8px; right: 8px; display: flex; gap: 6px; }
/* «Скрыть» — в другом углу: рядом с «написать» по нему промахивались */
.grid-card .row-btns.left { right: auto; left: 8px; }
.grid-card .grid-btn { width: 36px; height: 36px; }
/* Кнопка внутри карточки сетки — «Разблокировать» в S52 (Р-151). Действие
   в этом списке одно, и ему место под именем во всю ширину: значком в углу
   по нему промахивались бы, а промах здесь возвращает человека в ленты. */
.grid-card .cap-btn { position: absolute; left: 8px; right: 8px; bottom: 8px;
  padding: 8px 6px; border: 0; border-radius: var(--mm-r-btn);
  font-size: 13px; font-weight: 600; color: #fff; background: var(--mm-gradient); }
.grid-card.has-cap-btn .cap { bottom: 46px; padding-bottom: 6px; }
/* Пояснение о блокировке в анкете (S31): не ошибка и не предупреждение —
   состояние, поэтому нейтральная плашка, а не красная. */
.note-block { background: var(--mm-surface); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-card); padding: 12px 14px; margin-bottom: 12px;
  color: var(--mm-text2); font-size: 14px; }
.match-strip { display: flex; gap: 10px; overflow-x: auto; padding: 4px 0 8px; }
.match-strip img { width: 72px; height: 72px; border-radius: 999px; object-fit: cover;
  border: 2px solid transparent; background:
    linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box, var(--mm-gradient) border-box; }

/* --- Чат (S43) --- */
.chat-page { display: flex; flex-direction: column;
  height: calc(100dvh - var(--mm-h-tabbar)); }
@media (min-width: 992px) { .chat-page { height: 100dvh; } }
/* Шапка и поле ввода стоят на фоне страницы, а край рисует градиентная линия
   1.5px — то же правило, что у таббара (docs/design.md §5). Подложка
   `surface2` читалась серой плашкой поверх чёрного экрана. */
.chat-head { display: flex; align-items: center; gap: 10px; padding: 8px 12px;
  border-bottom: 1.5px solid transparent; background:
    linear-gradient(var(--mm-bg), var(--mm-bg)) padding-box,
    var(--mm-gradient) border-box; }
.chat-scroll { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 6px; }
.bubble { max-width: 78%; padding: 9px 13px; border-radius: var(--mm-r-bubble);
  font-size: 16px; line-height: 1.35; position: relative; word-break: break-word; }
.bubble.mine { align-self: flex-end; background: var(--mm-gradient); color: #fff;
  border-bottom-right-radius: 6px; }
.bubble.their { align-self: flex-start; background: var(--mm-surface2);
  border: 1px solid var(--mm-border); border-bottom-left-radius: 6px; }
.bubble img { max-width: 100%; border-radius: 10px; display: block; cursor: zoom-in; }
.bubble .meta { display: flex; gap: 4px; justify-content: flex-end; align-items: center;
  font-size: 11px; opacity: .8; margin-top: 2px; }
/* Галочки доставки. Размер задан на самом svg: у иконок нет атрибутов
   width/height, и Safari (iOS) внутри inline-flex схлопывает такой svg в ноль —
   в браузере на айфоне под сообщением оставалось только время */
/* Ширину слота задаём заранее (Р-177): пока сообщение отправляется, здесь
   стоит «…», а после подтверждения — галочка, и без общей ширины время под
   фотографией дёргалось бы вбок ровно в момент подмены */
/* «изм.» слева от времени (Р-180): та же строка мелким шрифтом, что и время,
   и та же прозрачность — пометка о правке не должна спорить с самим текстом */
.msg-edited { margin-right: 4px; font-style: italic; }
.tick { display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; min-width: 14px; }
.tick svg { width: 14px; height: 14px; }
.dlg-item .tick svg { width: 16px; height: 16px; }
.bubble.err { outline: 1.5px solid var(--mm-error); cursor: pointer; }
.day-sep { align-self: center; font-size: 12px; color: var(--mm-text2);
  background: var(--mm-surface); padding: 2px 12px; border-radius: 999px; margin: 8px 0 2px; }
/* Линия над полем ввода идёт брендовым градиентом **навстречу** остальным:
   тёмный фиолетовый слева, розовый справа (`gradient-brand-rev`,
   docs/themes.md §3). Одинаковое направление с баблами своих сообщений под ней
   читалось как продолжение одной заливки, встречное — как граница. */
.chat-input { display: flex; align-items: flex-end; gap: 8px; padding: 8px 12px
  calc(8px + env(safe-area-inset-bottom)); border-top: 1.5px solid transparent;
  background:
    linear-gradient(var(--mm-bg), var(--mm-bg)) padding-box,
    var(--mm-gradient-rev) border-box; }
/* Диалог заблокирован: объяснение и кнопка «Разблокировать» над ним — в шапке
   она отбирала ширину у имени (docs/screens.md S43) */
.chat-blocked { display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: 100%; padding: 4px 0; }
.chat-blocked .btn { min-width: 220px; }
.chat-input textarea { flex: 1; resize: none; max-height: 132px; min-height: 44px;
  padding: 10px 14px; }
.audio-msg { display: flex; align-items: center; gap: 10px; min-width: 180px; }
.audio-msg .bar { flex: 1; height: 4px; border-radius: 4px; background: rgba(255,255,255,.35);
  position: relative; cursor: pointer; }
.bubble.their .audio-msg .bar { background: var(--mm-border); }
.audio-msg .bar i { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 4px;
  background: #fff; width: 0; }
.bubble.their .audio-msg .bar i { background: var(--mm-primary); }

/* --- Слайдеры фильтров --- */
.range-mm { width: 100%; accent-color: var(--mm-primary); }
.dual-range { position: relative; height: 34px; }
.dual-range input[type=range] { position: absolute; width: 100%; top: 6px;
  background: none; pointer-events: none; -webkit-appearance: none; height: 22px; }
.dual-range input[type=range]::-webkit-slider-thumb { pointer-events: auto; -webkit-appearance: none;
  width: 24px; height: 24px; border-radius: 999px; background: #fff;
  border: 2px solid var(--mm-primary); cursor: pointer; margin-top: -10px; }
.dual-range input[type=range]::-webkit-slider-runnable-track { height: 4px; background: transparent; }
.dual-range .track { position: absolute; top: 15px; left: 0; right: 0; height: 4px;
  border-radius: 4px; background: var(--mm-border); }
.dual-range .fill { position: absolute; top: 15px; height: 4px; border-radius: 4px;
  background: var(--mm-gradient); }

/* --- Альбом --- */
.album-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.album-cell { aspect-ratio: 10/16; border-radius: 12px; background: var(--mm-surface) center/cover;
  position: relative; overflow: hidden; border: 1px solid var(--mm-border); }
/* Управление — в нижнем углу: сверху кнопки ложились на лицо, а номер кадра
   стоит слева вверху и с ними спорил */
.album-cell .btns { position: absolute; inset-inline-end: 6px; bottom: 6px;
  display: flex; gap: 4px; }
.album-cell .mini-btn { width: 30px; height: 30px; border-radius: 999px; border: 0;
  background: rgba(0,0,0,.55); color: #fff; font-size: 14px; line-height: 1; padding: 0;
  display: flex; align-items: center; justify-content: center; }
/* Иконки, а не типографские глифы: у ‹ › ink сидит ниже центра круга, и
   отступами это не лечится — содержимое <button> Chrome центрирует сам */
.album-cell .mini-btn svg { width: 18px; height: 18px; }
.album-cell.add { display: flex; align-items: center; justify-content: center;
  border: 1.5px dashed var(--mm-primary); color: var(--mm-primary); font-size: 34px; cursor: pointer; }
.album-cell .num { position: absolute; left: 6px; top: 6px; width: 22px; height: 22px;
  border-radius: 999px; background: var(--mm-gradient); color: #fff; font-size: 12px;
  display: flex; align-items: center; justify-content: center; font-weight: 700; }

/* --- Кадрирование --- */
.crop-stage { position: relative; max-height: 60vh; overflow: hidden; touch-action: none;
  display: flex; justify-content: center; background: #000; border-radius: 12px; }
.crop-stage img { max-width: 100%; max-height: 60vh; display: block; }
.crop-frame { position: absolute; border: 2px solid #fff; box-shadow: 0 0 0 9999px rgba(0,0,0,.55);
  cursor: move; }
.crop-frame .rsz { position: absolute; right: -10px; bottom: -10px; width: 24px; height: 24px;
  background: #fff; border-radius: 999px; border: 2px solid var(--mm-primary); cursor: nwse-resize; }

/* --- Профиль (S31/S10) --- */
.carousel-photos { position: relative; border-radius: var(--mm-r-photo); overflow: hidden;
  aspect-ratio: 10/16; background: var(--mm-surface); }
.carousel-photos img { width: 100%; height: 100%; object-fit: cover; }
/* Полоска кадров опущена от края: у скругления 24px её концы иначе подходят к
   краю фото заметно ближе, чем она сама отстоит сверху */
.carousel-photos .dots { position: absolute; top: 14px; left: 0; right: 0; display: flex;
  gap: 4px; padding: 0 14px; }
.carousel-photos .dots i { flex: 1; height: 3px; border-radius: 3px; background: rgba(255,255,255,.4); }
.carousel-photos .dots i.on { background: #fff; }
/* Края листают, центр открывает фото во весь экран — курсор это показывает.
   pan-y отдаёт браузеру только вертикальную прокрутку: горизонтальный жест
   остаётся у нас, иначе свайп по фото на телефоне перехватывался */
.carousel-photos { cursor: zoom-in; touch-action: pan-y; }
.carousel-photos .nav { position: absolute; top: 0; bottom: 0; width: 28%;
  cursor: pointer; }
.carousel-photos .nav.left { left: 0; }
.carousel-photos .nav.right { right: 0; }
/* затемнение снизу: под кнопками ✖/♥ фото бывает светлым */
.carousel-photos.has-actions::after { content: ""; position: absolute;
  left: 0; right: 0; bottom: 0; height: 42%; pointer-events: none;
  background: linear-gradient(transparent, rgba(0,0,0,.55)); }
.fact-row { display: flex; justify-content: space-between; padding: 10px 0;
  border-bottom: 1px solid var(--mm-border); }
.fact-row .k { color: var(--mm-text2); }

/* --- Меню --- */
.menu-card { background: var(--mm-surface2); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-card); padding: 14px 16px; margin-bottom: 12px; }
.badge-state { padding: 4px 12px; border-radius: 999px; font-weight: 600; font-size: 13px; }
.badge-state.active { color: var(--mm-success); background: rgba(34,197,94,.12); }
.badge-state.restricted { color: var(--mm-warning); background: rgba(245,158,11,.12); }
.badge-state.blocked { color: var(--mm-error); background: rgba(255,59,48,.12); }
/* «Заморожен» — синий: это не наказание, а выбор человека (docs/themes.md §3) */
.badge-state.frozen { color: var(--mm-frozen); background: rgba(59,130,246,.12); }
.form-switch .form-check-input:checked { background-color: var(--mm-primary); border-color: var(--mm-primary); }

/* --- Тосты, скелетоны, пустые состояния --- */
/* Всплывающее сообщение. Ширина ограничена и текстом влево: на телефоне
   длинная фраза раньше растягивалась на треть экрана узкой колонкой по центру.
   pointer-events: none — тост висит там же, где кнопка формы, и без этого на
   три секунды съедал нажатие по ней. */
.toast-mm { position: fixed; left: 50%; transform: translateX(-50%); pointer-events: none;
  bottom: calc(var(--mm-h-tabbar) + 16px + env(safe-area-inset-bottom));
  z-index: 3000; width: min(520px, calc(100vw - 24px));
  background: var(--mm-surface2); color: var(--mm-text);
  border: 1px solid var(--mm-border); border-left: 4px solid var(--mm-primary);
  padding: 12px 16px; border-radius: 14px; box-shadow: 0 8px 28px rgba(0,0,0,.22);
  text-align: start; font-size: 15px; line-height: 1.35; overflow-wrap: break-word;
  animation: toastIn .18s ease-out; }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, 8px); }
                     to { opacity: 1; transform: translate(-50%, 0); } }
.toast-mm.success { border-left-color: var(--mm-success); }
.toast-mm.error { border-left-color: var(--mm-error); }
/* без таббара (страницы входа, документы) тост прижимается к низу окна */
body:not([data-tab]) .toast-mm { bottom: calc(16px + env(safe-area-inset-bottom)); }

/* --- Меню действий (контекстные меню, подтверждения) --- */
.sheet-mm { background: rgba(0, 0, 0, .5); }
/* margin auto обязателен: ниже 576px Bootstrap задаёт .modal-dialog фиксированный
   отступ без auto, и суженная нами карточка прижималась к левому краю */
/* Координаты ставит UI.place: окно открывается по центру содержимого (а не
   экрана — слева бывает меню) либо под пальцем. pointer-events возвращаем
   явно: у Bootstrap нажатия внутри .modal включает только .modal-content. */
/* Ширина по содержимому: меню из трёх слов не должно растягиваться во весь
   экран, а подтверждение с текстом упирается в max-width и переносится. */
.sheet-card { position: fixed; left: 0; top: 0; pointer-events: auto;
  width: max-content; min-width: 200px; max-width: min(360px, calc(100vw - 32px));
  padding: 8px; border: 1.5px solid transparent; border-radius: 20px;
  background: linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box,
              var(--mm-gradient) border-box;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .3); }
/* Шапка окна (Р-181): это подпись к меню, а не заголовок экрана — поэтому
   мельче основного текста, по центру и градиентом бренда. Тонкая линия под
   ней отбивает подпись от действий: без неё «Прочитано в 19:01» читалось
   первым пунктом меню, по которому можно нажать. Одна на все окна: меню
   сообщения, меню диалога, подтверждения. */
.sheet-head { position: relative; padding: 10px 14px 10px; margin-bottom: 4px;
  font-size: 13px; font-weight: 700; text-align: center; letter-spacing: .02em;
  background: var(--mm-gradient); -webkit-background-clip: text;
  background-clip: text; color: transparent; }
.sheet-head::after { content: ""; position: absolute; left: 14px; right: 14px;
  bottom: 0; height: 1px; background: var(--mm-gradient); opacity: .35; }
/* Пояснение идёт сразу под шапкой — тогда линия принадлежит им обоим */
.sheet-head + .sheet-text { padding-top: 8px; }
.sheet-text { margin: 0; padding: 4px 14px 12px; color: var(--mm-text2);
  font-size: 14px; line-height: 1.4; }
.sheet-actions { display: flex; flex-direction: column; gap: 2px; }
.sheet-btn { width: 100%; min-height: 52px; padding: 12px 14px; border: 0; border-radius: 14px;
  background: none; color: var(--mm-text); font-size: 16px; font-weight: 600; text-align: start; }
.sheet-btn:hover, .sheet-btn:focus-visible { background: var(--mm-surface); outline: none; }
.sheet-btn:active { filter: brightness(.95); }
/* Пункты меню — текстом по левому краю; в подтверждении оба варианта
   выглядят кнопками и подписаны по центру. */
.sheet-btn.primary { background: var(--mm-gradient); color: #fff; text-align: center; }
.sheet-btn.solid-danger { background: var(--mm-error); color: #fff; text-align: center; }
.sheet-btn.secondary { border: 1.5px solid var(--mm-border); color: var(--mm-text);
  text-align: center; margin-top: 6px; }
.sheet-btn.secondary:hover { border-color: var(--mm-primary); color: var(--mm-primary);
  background: none; }
.sheet-btn.danger { color: var(--mm-error); }
.sheet-btn.muted { color: var(--mm-text2); font-weight: 500; }
.sheet-btn.center { text-align: center; }
.skeleton { background: var(--mm-surface); border-radius: 12px; position: relative; overflow: hidden; }
.skeleton::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.25), transparent);
  animation: sk 1.2s infinite; }
@keyframes sk { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.empty-state { text-align: center; color: var(--mm-text2); padding: 48px 16px; }
.empty-state .ico { font-size: 44px; margin-bottom: 8px; }
/* Заглушка закрытого раздела при заморозке (Р-142): текста здесь больше,
   чем в обычном пустом состоянии, и он не должен растягиваться на всю
   ширину десктопа — читать длинную строку глазами тяжело */
.frozen-state { max-width: 420px; margin: 0 auto; }
.frozen-state .fw-semibold { color: var(--mm-text); font-size: 18px; }

/* --- Фотопросмотр (S44) --- */
/* Просмотр фото: масштаб задаём сами (браузерный зум на страницах выключен),
   поэтому у слоя touch-action: none — иначе жесты уходят браузеру */
.photo-viewer { position: fixed; inset: 0; z-index: 2500; background: #000;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; touch-action: none; cursor: zoom-in; }
.photo-viewer img { max-width: 100vw; max-height: 100dvh; transform-origin: center;
  will-change: transform; user-select: none; -webkit-user-drag: none; }
.photo-viewer.zoomed { cursor: grab; }
.pv-close { position: absolute; top: calc(12px + env(safe-area-inset-top)); right: 12px;
  width: 40px; height: 40px; border-radius: 999px; border: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.18); color: #fff; backdrop-filter: blur(6px); }
.pv-close svg { width: 22px; height: 22px; }

/* ================= Плавность и анимации ================= */
@media (prefers-reduced-motion: no-preference) {
  .bubble.is-new { animation: bubbleIn .22s ease-out both; }
  .grid-card, .dlg-item, .menu-card { animation: fadeIn .2s ease-out both; }
  /* Диалог, поднявшийся наверх из-за нового сообщения: короткий въезд сверху
     вместо подмены строки на месте (Р-149) */
  .dlg-item.is-bump { animation: bumpIn .28s ease-out both; }
  .toast-mm { animation: toastIn .2s ease-out both; }
  .match-overlay { animation: fadeIn .25s ease-out both; }
  .match-overlay .match-avatars img { animation: popIn .35s cubic-bezier(.2,1.4,.4,1) both; }
  .match-overlay .match-avatars img:last-child { animation-delay: .08s; }
}
@keyframes bubbleIn { from { opacity: 0; transform: translateY(10px) scale(.98); }
                      to { opacity: 1; transform: none; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, 12px); }
                     to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes popIn { from { opacity: 0; transform: scale(.7); } to { opacity: 1; transform: scale(1); } }
@keyframes bumpIn { from { opacity: .3; transform: translateY(-8px); }
                    to { opacity: 1; transform: none; } }

/* Заглушки шапки и поля ввода в переписке (Р-149): рамки нарисованы сервером
   ещё до ответа api, поэтому страница открывается собранной, а не пустой.
   Кружки не сжимаются — иначе на узком экране они превращаются в овалы. */
.chat-head.is-skel > .skeleton, .chat-input.is-skel > .skeleton { flex: 0 0 auto; }
.chat-input.is-skel > .skeleton.flex-grow-1 { flex: 1 1 auto; }

.icon-btn, .act-btn, .btn-mm, .btn-mm-outline, .chip-select { transition: transform .12s, filter .12s, background .2s; }
.deck-card { transition: transform .2s ease-out; }
.tabbar a, .sidebar a { transition: color .15s, background .15s; }

/* Карусель: мягкая смена кадра вместо рывка при догрузке */
.carousel-photos img { transition: opacity .18s ease, transform .18s ease; }
.carousel-photos img.fading { opacity: 0; }

/* «Печатает…» */
.typing-text { color: var(--mm-primary); font-weight: 600; }
.typing-bubble { display: inline-flex; gap: 4px; align-items: center; padding: 12px 14px; }
.typing-bubble i { width: 7px; height: 7px; border-radius: 999px; background: var(--mm-text2);
  display: inline-block; animation: typingDot 1.1s infinite ease-in-out; }
.typing-bubble i:nth-child(2) { animation-delay: .15s; }
.typing-bubble i:nth-child(3) { animation-delay: .3s; }
@keyframes typingDot { 0%, 60%, 100% { opacity: .3; transform: translateY(0); }
                       30% { opacity: 1; transform: translateY(-3px); } }

/* Поле ввода: максимум две строки, прокрутка без видимого ползунка */
.chat-input textarea { max-height: none; overflow-y: auto; scrollbar-width: none; }
.chat-input textarea::-webkit-scrollbar { width: 0; height: 0; }

/* ================= Лендинг ================= */
/* только вертикальные отступы: сокращённая запись обнуляла боковые поля
   .container, и .row со своими отрицательными полями вылезал за экран */
.landing-hero { padding-top: 56px; padding-bottom: 32px; }
/* Заголовок закрашен градиентом через background-clip: text — то есть буквы
   показывают фон **коробки заголовка**. При line-height 1.1 хвосты `g` и `y`
   последней строки выходят за эту коробку, фона там нет, и они пропадают
   (замечено оператором на «right where you are»). Лечится запасом снизу:
   коробка становится чуть выше строки, градиент доходит до хвостов. */
.landing-h1 { font-size: clamp(34px, 5vw, 52px); font-weight: 800; line-height: 1.1;
  white-space: pre-line; background: var(--mm-gradient);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  padding-bottom: .14em; }
.btn-lg-cta { display: inline-flex; align-items: center; justify-content: center;
  min-height: 54px; padding: 0 30px; font-size: 17px; text-decoration: none; }

/* Блок, который появляется уже после загрузки страницы (кнопка «Установить
   приложение» и подсказка для iPhone): раскрывается плавно, вместо того чтобы
   разом стать высоким и толкнуть вниз всё, что под ним.

   Приём с сеткой `0fr → 1fr` — единственный способ анимировать «высоту по
   содержимому»: `height: auto` не анимируется вовсе, а `max-height` наугад
   даёт либо обрезку, либо рывок в конце. `min-height: 0` у ребёнка
   обязателен: без него дорожка сетки не сжимается ниже содержимого.
   `visibility` гасим с задержкой в длину анимации — иначе скрытая кнопка
   остаётся доступной с клавиатуры, и Tab уводит фокус в пустоту. */
.reveal { display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .4s ease, opacity .4s ease; }
.reveal > * { min-height: 0; overflow: hidden;
  visibility: hidden; transition: visibility 0s .4s; }
.reveal.is-on { grid-template-rows: 1fr; opacity: 1; }
.reveal.is-on > * { visibility: visible; transition: visibility 0s 0s; }
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal > * { transition: none; }
}

/* Витрина первого экрана: карточка листается сама (Р-135). Слайды лежат
   стопкой внутри .phone-card и перетекают друг в друга; всё, кроме класса
   is-on, приходит с сервера. */
.phone-demo { max-width: 300px; }
.phone-card { position: relative; display: block; aspect-ratio: 10/16;
  border-radius: var(--mm-r-photo); background: var(--mm-gradient-hero);
  box-shadow: 0 18px 50px rgba(162, 45, 240, .28); overflow: hidden;
  color: #fff; text-decoration: none; }
.hero-slide { position: absolute; inset: 0; opacity: 0; transition: opacity 1s ease; }
.hero-slide.is-on { opacity: 1; }
/* Фирменный слайд — тот же градиент со светлым бликом, что был здесь всегда */
.hero-slide.is-brand::after { content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at 30% 20%, rgba(255,255,255,.35), transparent 55%); }
.hero-photo { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; }
/* Затемнение снизу: без него имя и город теряются на светлых снимках */
.hero-slide.is-photo::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,.15) 42%, transparent 62%); }
.demo-chip { position: absolute; left: 14px; top: 14px; z-index: 2; }
.demo-heart { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 96px; color: rgba(255,255,255,.9); z-index: 1; }
.demo-info { position: absolute; left: 18px; right: 18px; bottom: 18px; color: #fff; z-index: 2; }
.demo-name { font-size: 26px; font-weight: 700; }
.demo-sub { opacity: .9; font-size: 14px; }
.phone-actions { display: flex; justify-content: center; gap: 22px; margin-top: -26px;
  position: relative; z-index: 3; }
.demo-btn { width: 56px; height: 56px; border-radius: 999px; display: inline-flex;
  align-items: center; justify-content: center; font-size: 22px;
  background: var(--mm-surface2); border: 1.5px solid var(--mm-border);
  box-shadow: 0 6px 18px rgba(0,0,0,.15); }
.demo-btn.pass { color: var(--mm-pass); }
.demo-btn.like { background: var(--mm-gradient); color: #fff; border: 0; }
@media (prefers-reduced-motion: no-preference) {
  .phone-demo { animation: floatY 5s ease-in-out infinite; }
}
@keyframes floatY { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }

.feature-card, .step-card { background: var(--mm-surface); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-card); padding: 20px; transition: transform .15s, box-shadow .15s; }
.feature-card:hover, .step-card:hover { transform: translateY(-3px);
  box-shadow: 0 10px 26px rgba(0,0,0,.10); }

/* Блок со снимком экрана: телефон сверху, под ним значок, заголовок и текст.
   Всё по центру — четыре колонки с картинками читаются как ряд телефонов, а
   выключка влево рвала бы этот ряд. */
.feature-card.has-shot { display: flex; flex-direction: column; align-items: center;
  text-align: center; padding-top: 24px; }
.feature-shot { width: 100%; max-width: 190px; margin-bottom: 18px; padding: 6px;
  border-radius: 30px; background: var(--mm-surface2);
  border: 1px solid var(--mm-border); box-shadow: 0 16px 36px rgba(0,0,0,.22); }
.feature-shot img { display: block; width: 100%; height: auto; border-radius: 24px; }
.step-num { display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 999px; background: var(--mm-gradient);
  color: #fff; font-weight: 700; margin-bottom: 10px; }

.final-cta { border-radius: var(--mm-r-photo); padding: 48px 24px; color: #fff;
  background: var(--mm-gradient); box-shadow: 0 16px 40px rgba(162, 45, 240, .25); }
.final-btn { background: #fff !important; color: var(--mm-primary) !important; }

/* ============ Правки по замечаниям: мобильные, чат, модалки ============ */

/* --- Модалки: прокручивается тело, шапка и кнопки закреплены --- */
/* Координаты ставит UI.place — окно открывается по центру содержимого (карты,
   колоды), а не экрана: на широком экране слева стоит меню, и окно по центру
   экрана оказывалось заметно левее того, к чему относится. Карта выбора точки
   (.map-pick) позиционируется своим CSS и это правило не использует. */
.mm-modal:not(.map-pick) .modal-dialog { position: fixed; left: 0; top: 0; margin: 0;
  width: min(500px, calc(100vw - 16px)); max-width: none;
  /* display/height сбрасываем: у Bootstrap диалог тянется на всю высоту окна
     (centered + scrollable), и измерить его настоящий размер было бы нельзя */
  display: block; height: auto; min-height: 0; }
.mm-modal .modal-content { max-height: 92dvh; background: var(--mm-surface2);
  border: 1px solid var(--mm-border); }
.mm-modal .modal-body { overflow-y: auto; padding: 12px 16px; }
.mm-modal .modal-header, .mm-modal .modal-footer { padding: 12px 16px; }
.mm-modal .modal-footer > .btn { min-height: 48px; }
.mm-modal details > summary { cursor: pointer; list-style: revert; }
body.modal-open-mm { overflow: hidden; }

/* --- Чат: фиксированная область под видимый экран (iOS-клавиатура) --- */
body[data-dialog] .tabbar { display: none; }   /* внутри диалога таббар не нужен */
.chat-page { position: fixed; left: 0; right: 0; top: 0; height: 100dvh;
  display: flex; flex-direction: column; background: var(--mm-bg); z-index: 5; }
@media (min-width: 992px) { .chat-page { left: 220px; height: 100dvh; } }
.chat-head { flex-shrink: 0; overflow: hidden; }
.chat-input { flex-shrink: 0; }

/* Кнопка «Разблокировать» в шапке не должна выталкивать «⋮» за экран:
   она сжимается вместе с именем собеседника, длинная подпись обрезается. */
.chat-head #c-unblock { flex: 0 1 auto; min-width: 0; max-width: 42%; }

/* Кнопка меню у фото и голосовых: по самому баблу не нажать — он открывает
   просмотр или плеер. У своих сообщений слева снизу, у чужих справа снизу. */
.bubble { position: relative; }
.msg-menu { position: absolute; bottom: -2px; width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: 999px; background: none; color: var(--mm-text2);
  display: inline-flex; align-items: center; justify-content: center; }
.msg-menu svg { width: 18px; height: 18px; }
.msg-menu:active { color: var(--mm-primary); }
.bubble.mine .msg-menu { left: -32px; }
.bubble.their .msg-menu { right: -32px; }

/* --- Бабл с фото: одинаковая рамка и разумный размер на десктопе --- */
.bubble.has-media { padding: 4px; max-width: min(78%, 320px); }
.bubble.has-media .meta { padding: 0 8px 4px; }
.bubble img { max-width: 100%; max-height: 360px; width: auto; height: auto;
  border-radius: 14px; display: block; }
@media (min-width: 992px) {
  .bubble.has-media { max-width: 300px; }
  .bubble img { max-height: 320px; }
}

/* --- Лендинг: текст демо-карточки выше кнопок --- */
.demo-info { bottom: 58px; }

/* --- Лендинг на телефонах --- */
@media (max-width: 575.98px) {
  .landing-hero { padding-top: 28px; padding-bottom: 16px; }
  .landing-h1 { font-size: 30px; }
  .landing-hero .lead { font-size: 16px; }
  .btn-lg-cta { width: 100%; min-height: 50px; font-size: 16px; padding: 0 18px; }
  .landing-hero .d-flex.gap-3 { flex-direction: column; gap: 10px !important; width: 100%; }
  .phone-demo { max-width: 210px; }
  .demo-heart { font-size: 68px; }
  .demo-name { font-size: 20px; }
  .demo-info { bottom: 48px; }
  .demo-btn { width: 46px; height: 46px; font-size: 18px; }
  .phone-actions { gap: 16px; margin-top: -22px; }
  .final-cta { padding: 32px 16px; }
  .final-cta h2 { font-size: 24px; }
  /* шапка: логотип + компактные ссылки, без переноса */
  header .brand-name { font-size: 18px; }
  header .brand-logo { width: 34px; height: 34px; }
  header nav { gap: 10px !important; }
  header nav .btn-mm { padding: 8px 14px; font-size: 14px; }
  .hero { padding: 40px 0 28px; }
  .hero h1 { font-size: 30px; }
}
/* страница диалога не скроллится целиком — прокручивается только лента */
body[data-dialog] { overflow: hidden; }

/* шапка не переносится: три элемента налезали на логотип */
header nav { flex-wrap: nowrap; }
header nav a { white-space: nowrap; }

/* ============ Правки третьего круга ============ */

/* Кружок с иконкой (например «Кому я нравлюсь»): Safari не растягивает
   inline-SVG без явных размеров — на iPhone сердечко просто исчезало */
.avatar-icon { display: inline-flex; align-items: center; justify-content: center;
  color: var(--mm-primary); }
.avatar-icon svg { width: 26px; height: 26px; flex-shrink: 0; }
/* Аватар администрации (Р-96): фотографии у служебного аккаунта нет вовсе, на
   её месте — знак сервиса. Рамку и подложку снимаем: у обычных аватарок это
   градиентный ободок в 2 px, а логотип должен заполнять круг целиком, без
   полосок по краям. Круг вписан в квадратную иконку с запасом (скругление
   углов 205 из 1024), поэтому обрезка `cover` его не затрагивает. */
.avatar-official { border: 0; padding: 0; background: none; object-fit: cover; }

/* Бейдж непрочитанного на вкладке «Сообщения» */
.tabbar a { position: relative; }
.nav-badge { position: absolute; top: 2px; right: 12px; min-width: 18px; height: 18px;
  padding: 0 5px; border-radius: 999px; background: var(--mm-gradient); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; }
@media (min-width: 992px) {
  .sidebar a { position: relative; }
  .nav-badge { position: static; margin-left: auto; }
}

/* Шапка модалки: заголовок слева, крестик прижат вправо */
.mm-modal .modal-header { display: flex; align-items: center;
  justify-content: space-between; gap: 16px; }
.mm-modal .modal-header h5 { flex: 1 1 auto; min-width: 0; }
.mm-modal .modal-header .icon-btn { flex: 0 0 auto; }

/* Полоса прокрутки модалки — в стиле сайта */
.mm-modal .modal-body { scrollbar-width: thin;
  scrollbar-color: var(--mm-primary) transparent; }
.mm-modal .modal-body::-webkit-scrollbar { width: 8px; }
.mm-modal .modal-body::-webkit-scrollbar-track { background: transparent; }
.mm-modal .modal-body::-webkit-scrollbar-thumb { border-radius: 999px;
  background: var(--mm-gradient); border: 2px solid var(--mm-surface2); }

/* Запись голосового: точка, таймер и кнопки «удалить/отправить» */
.rec-state { display: flex; align-items: center; gap: 10px; padding: 0 6px;
  font-variant-numeric: tabular-nums; font-weight: 600; }
.rec-dot { width: 10px; height: 10px; border-radius: 999px; background: var(--mm-error);
  flex-shrink: 0; }
@media (prefers-reduced-motion: no-preference) {
  .rec-dot { animation: recPulse 1s infinite ease-in-out; }
}
@keyframes recPulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

/* ============ Правки четвёртого круга ============ */

/* Лента чата растёт снизу вверх: диалог всегда открыт на последнем
   сообщении, а подгрузка старых сверху не сдвигает прочитанное */
.chat-scroll { flex-direction: column-reverse; overflow-anchor: none; }

/* Фирменный ползунок вместо системного (чат и другие прокручиваемые ленты) */
.chat-scroll, .app-content, .mm-modal .modal-body {
  scrollbar-width: thin; scrollbar-color: var(--mm-primary) transparent; }
.chat-scroll::-webkit-scrollbar, .app-content::-webkit-scrollbar { width: 8px; }
.chat-scroll::-webkit-scrollbar-track, .app-content::-webkit-scrollbar-track {
  background: transparent; }
.chat-scroll::-webkit-scrollbar-thumb, .app-content::-webkit-scrollbar-thumb {
  border-radius: 999px; background: var(--mm-gradient);
  border: 2px solid transparent; background-clip: padding-box; }
.chat-scroll::-webkit-scrollbar-thumb:hover, .app-content::-webkit-scrollbar-thumb:hover {
  filter: brightness(1.1); }

/* Бейдж непрочитанного: красная пилюля, растёт под число. Обводка — цветом
   фона таббара (он же фон страницы), иначе на чёрном виден серый ореол */
.nav-badge { position: absolute; top: 2px; left: 50%; transform: translateX(2px);
  right: auto; min-width: 20px; height: 20px; padding: 0 7px; border-radius: 999px;
  background: var(--mm-error); color: #fff; font-size: 12px; font-weight: 700;
  line-height: 20px; text-align: center; box-shadow: 0 0 0 2px var(--mm-bg); }
@media (min-width: 992px) {
  .nav-badge { position: static; transform: none; margin-left: auto; box-shadow: none; }
}

/* Плавное появление данных после заглушек */
.fade-in { animation: fadeInSoft .22s ease-out both; }
@keyframes fadeInSoft { from { opacity: 0; transform: translateY(4px); }
                        to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .fade-in { animation: none; } }

/* Скелетоны не должны перебивать анимацию появления соседей */
.skeleton { display: block; }
.dlg-item .skeleton { flex-shrink: 0; }

/* --- Фиксированный масштаб интерфейса (страницы приложения) --- */
/* touch-action убирает зум двойным тапом; на карте жесты нужны — там свои
   правила MapLibre, поэтому её контейнер исключаем */
body[data-tab], body[data-page], body[data-user], body[data-dialog] {
  touch-action: manipulation; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
#map, .maplibregl-canvas-container { touch-action: none; }
/* Поля ввода не мельче 16px: иначе iOS сам увеличивает страницу при фокусе */
input, textarea, select { font-size: 16px; }

/* Форма ждёт данные: видно, что она ещё не готова, и нельзя ничего ввести */
.form-loading { opacity: .55; pointer-events: none; transition: opacity .2s; }

/* Точка онлайна прячется классом, а не пересборкой строки списка */
.dlg-item .dot-online.d-none { display: none !important; }

/* ============ Колода: карточка целиком помещается на экран ============ */
/* Экран карточек не прокручивается: содержимое ровно в высоту окна, а колода
   растягивается на весь остаток под шапкой. Пропорцию карточке не задаём —
   она занимает свободное место целиком, а фото подрезается по object-fit. */
/* Оболочку тоже прижимаем к окну: у .app-shell высота задана в 100vh — это
   «большой» вьюпорт, и на телефоне с прячущейся панелью браузера страница
   выходила выше экрана, из-за чего колода с шапкой уезжала вверх, а между
   карточкой и таббаром открывалась полоса.

   `100dvh` здесь — только первое приближение, до запуска скриптов: точную
   высоту ставит `fitViewport()` из cards.js по измеренной видимой области.
   В установленном веб-приложении на Android `dvh` расходился с ней, и колода
   уезжала под таббар. */
body[data-tab="cards"] .app-shell { min-height: 0; height: 100dvh; overflow: hidden; }
body[data-tab="cards"] .app-content {
  height: 100%; overflow: hidden;
  display: flex; flex-direction: column;
  padding-bottom: calc(var(--mm-h-tabbar) + 12px + env(safe-area-inset-bottom));
}
.deck-stage { flex: 1 1 auto; min-height: 0; width: 100%; position: relative; }
body[data-tab="cards"] #deck-empty { margin-block: auto; }
/* Когда анкеты кончились, пустая сцена больше не забирает всю высоту —
   иначе заглушка прижималась к низу экрана вместо середины */
body[data-tab="cards"].deck-empty .deck-stage { flex: 0 0 auto; }
@media (min-width: 992px) {                  /* на десктопе таббара нет */
  body[data-tab="cards"] .app-content { padding-bottom: 12px; }
}
.deck-wrap { position: absolute; inset: 0; height: auto; }

/* Кнопки лежат на фото — над градиентной рамкой карточки (у неё z-index 3).
   Только с классом on-photo: без него ряд кнопок обычный, иначе на анкете
   (S31) он тоже становился абсолютным и наезжал на текст. */
.deck-actions.on-photo { position: absolute; left: 0; right: 0; bottom: 16px; z-index: 5;
  margin: 0; gap: 32px; pointer-events: none; }
.deck-actions.on-photo .act-btn { pointer-events: auto;
  box-shadow: 0 6px 18px rgba(0,0,0,.35); }
.deck-actions.on-photo .act-btn.pass { background: rgba(255,255,255,.92); border: 0;
  color: var(--mm-pass); }
[data-bs-theme="dark"] .deck-actions.on-photo .act-btn.pass { background: rgba(26,26,34,.92);
  color: #fff; }

/* Подписи поднимаем над кнопками, затемнение снизу делаем выше */
.deck-card .info { bottom: 92px; }
.deck-card .grad-bottom { height: 58%; }

/* Совсем низкие экраны: кнопки и подписи компактнее */
@media (max-height: 700px) {
  .act-btn { width: 54px; height: 54px; }
  .act-btn svg { width: 26px; height: 26px; }
  .deck-actions.on-photo { bottom: 12px; gap: 26px; }
  .deck-card .info { bottom: 78px; }
  .deck-card .info h2 { font-size: 24px; }
}

/* ---------- Лендинг: FAQ и языки в подвале (Р-71) ---------- */

/* Аккордеон на <details>: работает без JS, а поисковик видит весь текст —
   ответы лежат в разметке, а не подгружаются по клику. */
.faq-item { border: 1px solid var(--mm-border); border-radius: var(--mm-r-card);
  padding: 14px 18px; margin-bottom: 10px; background: var(--mm-surface2); }
.faq-item summary { cursor: pointer; font-weight: 600; list-style: none;
  display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; font-size: 22px; line-height: 1;
  color: var(--mm-text2); }
.faq-item[open] summary::after { content: "−"; }
.faq-item[open] { border-color: var(--mm-primary); }

/* ---------- Выбор темы (docs/themes.md §6) ---------- */

/* Кнопка-иконка: показывает, что получится после нажатия — в тёмной теме
   солнце, в светлой месяц. Состояние рисует CSS по атрибуту темы, поэтому
   иконка верна с первого кадра и не зависит от загрузки скриптов. */
.theme-btn { padding: 0; }
.theme-btn svg { width: 22px; height: 22px; }
[data-bs-theme="dark"] .theme-btn .ico-moon,
[data-bs-theme="light"] .theme-btn .ico-sun { display: none; }
.theme-btn:hover { color: var(--mm-primary); border-color: var(--mm-primary); }
/* Вход и регистрация: кнопка прижата к верхнему углу колонки и не отодвигает
   содержимое — отрицательный отступ съедает её высоту */
.theme-corner { display: flex; justify-content: flex-end; margin-bottom: -12px; }

/* Сегмент-контрол в меню (S50): активную сторону задаёт сама тема страницы —
   в JS состояние не дублируется и при загрузке ничего не «перещёлкивается» */
[data-bs-theme="dark"] .seg button[data-theme-set="dark"],
[data-bs-theme="light"] .seg button[data-theme-set="light"] {
  color: #fff; background: var(--mm-gradient); }

.lang-links { display: flex; flex-wrap: wrap; gap: 6px 14px; }
.lang-links a { color: var(--mm-text2); text-decoration: none; font-size: 13px; }
.lang-links a:hover { color: var(--mm-primary); }
.lang-links a.active { color: var(--mm-primary); font-weight: 600; }

/* --- Реклама (Р-109, docs/ads.md §11) ---
   Новых цветов нет: блоки собраны из тех же токенов, что бабл карты и карточка
   колоды. Пометка «Реклама» нейтральная — она обязана быть заметной, но не
   спорить с содержимым. */

/* Место пометки одно на весь сервис — **правый нижний угол** (решение
   оператора): где бы человек ни встретил рекламу — на карте, в колоде, в
   предпросмотре панели, — он ищет её признак там же. Сверху и слева место
   нужно содержимому объявления. */
.promo-mark { position: absolute; right: 10px; bottom: 10px; z-index: 3;
  padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600;
  white-space: nowrap; color: var(--mm-text2); background: var(--mm-surface2);
  border: 1px solid var(--mm-border); }

/* Блок на карте: прямоугольник 2:1 с брендовой рамкой — как бабл анкеты, но
   не круглый: круг читался бы как человек.

   Размер вдвое меньше первой версии (было 260×130). Маркер привязан **центром**
   к точке, и широкий блок у края окна наполовину уезжал за экран: пометка в
   углу оставалась видна, а текст по центру — нет. Заодно 260 px рядом с
   48-пиксельными аватарками анкет выглядели рекламным щитом. */
/* Проявляется так же, как бабл анкеты: сначала невидим, потом та же плавная
   `fadeIn` в секунду. Иначе блок выскакивал рывком на фоне мягко всплывающих
   анкет и притягивал взгляд сильнее, чем должен. */
.promo-bubble { opacity: 0; }
.promo-bubble.shown { animation: fadeIn 1s ease forwards; }
.promo-bubble { position: relative; width: min(130px, 34vw); aspect-ratio: 2 / 1;
  cursor: pointer; border-radius: 10px; overflow: hidden;
  border: 1.5px solid transparent; background:
    linear-gradient(var(--mm-surface2), var(--mm-surface2)) padding-box,
    var(--mm-gradient) border-box;
  box-shadow: 0 3px 12px rgba(0,0,0,.18); }
.promo-bubble .promo-body { position: absolute; inset: 0; background-size: cover;
  background-position: center; }
/* Текст в маленьком блоке: мельче, плотнее и с обрезкой по строкам — лучше
   недосказать, чем вылезти за край */
.promo-bubble .promo-body.is-text { display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 4px 6px 12px; background: var(--mm-gradient);
  color: #fff; font-weight: 600; font-size: 10px; line-height: 1.15;
  overflow: hidden; }
.promo-bubble .promo-body.is-text > * { min-width: 0; }

/* На блоке карты пометка прижата в угол вплотную: блок 130×65, и отступ съел бы
   поле, нужное тексту. Скругление только с внутренней стороны — плашка читается
   как загнутый уголок самого блока. */
.promo-bubble .promo-mark { right: 0; bottom: 0;
  padding: 1px 6px 2px; font-size: 9px; line-height: 1.3;
  border-radius: 8px 0 0 0; border-width: 1px 0 0 1px; }

/* Рекламная карточка колоды: та же геометрия, что у анкеты, — кадр 10:16 на
   всё поле. Ни имени, ни интересов, ни жалобы: это не человек */
.deck-card.is-promo { background: var(--mm-surface2); }
.deck-card.is-promo .promo-body { position: absolute; inset: 0; border-radius: inherit;
  background-size: cover; background-position: center; }
/* Снизу поля больше: там и пометка, и кнопки, лежащие на фото */
.deck-card.is-promo .promo-body.is-text { display: flex; align-items: center;
  justify-content: center; text-align: center; padding: 32px 24px 76px;
  background: var(--mm-gradient); color: #fff; font-weight: 700;
  font-size: 22px; line-height: 1.3; white-space: pre-wrap; }
.deck-card.is-promo .promo-mark { right: 14px; bottom: 14px; }
/* Штампы у рекламы — словами («Открыть» / «Закрыть»), а не «Like»/«Nope»:
   кегль мельче, иначе длинное слово не помещается в поворот */
.deck-card.is-promo .deck-stamp { font-size: 24px; }
/* Кнопка «Открыть» вместо сердца: то же место, тот же размер, другой смысл */
.act-btn.like.is-promo { background: var(--mm-gradient-accent); }

/* --- W9 окно разработчика (Р-132) ---
   Экран технический, но живёт в тех же токенах, что весь клиент: карточки
   `menu-card`, кнопки `btn-mm`. Своего здесь ровно три вещи — строка «поле +
   кнопка», моноширинные значения (адреса и коды читают глазами, и ширина
   символа важнее красоты) и окно журнала с прокруткой. */
.dev-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.dev-row .form-select { flex: 1 1 200px; min-width: 0; }
.dev-row .btn { flex: 0 0 auto; }
.dev-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; word-break: break-all; }
.dev-line { padding: 3px 0; font-size: 13px; line-height: 1.45; }
.dev-line + .dev-line { border-top: 1px solid var(--mm-border); }
/* Журнал: высота фиксированная — иначе полсотни ошибок растягивают страницу
   так, что кнопки «Назад» и «Скопировать» уезжают за экран */
.dev-log { max-height: 40vh; overflow-y: auto; overscroll-behavior: contain;
  background: var(--mm-surface); border: 1px solid var(--mm-border);
  border-radius: var(--mm-r-field); padding: 8px 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.5; word-break: break-word; }
.dev-log .dev-line { padding: 4px 0; }
.dev-tag { display: inline-block; padding: 0 6px; border-radius: 6px;
  background: var(--mm-surface2); border: 1px solid var(--mm-border);
  color: var(--mm-text2); font-size: 11px; }
.dev-count { color: var(--mm-warning); }
