/* Раскладка экранов мессенджера.

   Оформление — только в theme.css: этот файл не заводит ни цветов, ни теней,
   ни шрифтовых размеров, а собирает из готовых классов темы экраны и
   расставляет их по местам. Всё, что здесь есть про внешний вид, приходит
   через var(--...).

   Порядок подключения фиксирован: theme.css идёт первым, поэтому правила
   отсюда при равной специфичности выигрывают и могут донастраивать геометрию
   темы, не трогая её саму.

   Внешних ресурсов нет: ни @import, ни url(), ни шрифтов из сети. */

/* Геометрия раскладки. Это не токены темы, а размеры конкретных экранов:
   ширина колонки, предел ширины ленты и карточек. В theme.css им не место,
   поэтому они живут здесь и больше нигде не дублируются. */
:root {
  /* Колонка списка переписок на широком экране. Доля от окна, а не жёсткие
     точки: в привычных мессенджерах список занимает около трети ширины, и на
     мониторе в 2560 точек колонка в 360 выглядит полоской. Пределы нужны с
     обеих сторон — уже 320 строка списка не читается, шире 520 колонка
     начинает отнимать место у самой переписки. */
  --side-w: clamp(320px, 30%, 520px);
  --feed-max: 68rem;    /* предел ширины ленты, чтобы на 1920 не расползалась */
  --msg-max: 620px;     /* предел ширины сообщения */
  --auth-max: 26rem;    /* карточка входа */
  --composer-max-h: 160px; /* предел роста поля сообщения */
  /* Кнопка отправки. Меньше обычной круглой (48): всё, что она отдаёт, уходит
     под текст, а промахнуться по 40 точкам пальцем всё ещё нельзя. */
  --send-size: 40px;
  --modal-max: 32rem;   /* окно аккаунтов и диалоги */

  /* Безопасные зоны выреза и жестов: считаем один раз, используем везде. */
  --safe-t: env(safe-area-inset-top, 0px);
  /* Нижняя зона — как её отдаёт система. Гасить её под клавиатуру самим не
     нужно: при поднятой клавиатуре система сама отдаёт ноль. Здесь была своя
     арифметика с --kb-cover — она ушла вместе с ручным управлением каркасом. */
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

/* Страница целиком не прокручивается — ни вниз, ни вбок. Прокрутка живёт
   внутри ленты и списка, поэтому шапки и поле ввода никуда не уезжают. */
html,
body {
  overflow: hidden;
  overscroll-behavior: none;
}

/* ======================================================================
   Каркас
   ====================================================================== */

/* Каркас прилеплен к краям экрана, и этим всё сказано.

   Раньше здесь была высота из JS (--app-h) и сдвиг за клавиатурой
   (--app-shift): область просмотра повторялась из кода кадр за кадром. Это и
   давало подёргивания — события приходят рывками, каркас то отстаёт, то
   догоняет. Прилепленный каркас двигает браузер, а он не опаздывает.

   Высоту не задаём вовсе: у прилепленного к четырём краям элемента она и есть
   высота экрана, и вторая её запись только спорила бы с первой. */
.app {
  position: fixed;
  inset: 0;
  overflow: hidden;
  padding-left: var(--safe-l);
  padding-right: var(--safe-r);
}

.layout {
  height: 100%;
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-columns: var(--side-w) 1fr;
  align-items: stretch;
}

/* Волосяная линия между колонками — единственная допустимая рамка.

   Заливка панели здесь появилась вместе с подложкой полотна: до этого обе
   колонки показывали фон страницы и отличались только линией между ними.
   Теперь как в привычных мессенджерах — список стоит на панели, переписка на
   полотне, и панель заодно перекрывает подложку, а не просвечивает её. */
.side {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--c-surface);
  border-right: 1px solid var(--c-hairline);
}

/* Рисунок за лентой лежит на самой колонке, а не на псевдоэлементе с маской:
   лишний слой поверх прокручиваемой ленты стоил рывков на телефоне. Шапка и
   строка поиска закрывают его своей заливкой панели; строка ввода — уже нет,
   она плавает над рисунком отдельной пилюлей.
   560 точек — сторона плитки из tools/make-wallpaper.py; плитка замкнутая,
   стыков при повторении не видно. */
.main {
  /* Плашка выбора висит над лентой и считает края от колонки переписки. */
  position: relative;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background-image: var(--wallpaper);
  background-repeat: repeat;
  background-size: 560px 560px;
}

/* ======================================================================
   Колонка списка переписок
   ====================================================================== */

.side-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  padding-top: calc(var(--sp-3) + var(--safe-t));
}

/* Заголовок забирает свободное место — и он же его отдаёт. В строке стоят
   тумблеры мессенджеров со счётчиками и кнопка меню, и на узком экране им
   тесно: сжиматься умеет только заголовок (`t-clip` обрезает его многоточием),
   у остального ширина по содержимому. */
.side-head .t-title {
  flex: 1 1 auto;
  min-width: 0;
}

/* Правый край шапки: тумблеры мессенджеров и кнопка меню. Не сжимается и не
   переносится — счётчик «99+» обязан остаться читаемым на любом экране. */
.head-tools {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Тумблеры мессенджеров — отдельной группой между заголовком и кнопкой меню.
   Своим узлом, а не вперемешку с меню: на телефоне группа целиком уезжает во
   вторую строку (см. @media ниже), и переносить туда кнопку меню не надо. */
.head-transports {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Тумблеры в шапке плотнее табов фильтра, и счётчик на них тоже: в верхней
   строке они соседствуют с заголовком, и каждая лишняя точка отступа отъедается
   у него. Самый плотный случай — «99+» на всех тумблерах плюс кнопка меню.
   Мишень кнопки при этом не страдает: высота таба прежняя. */
.head-transports .filter-tab {
  gap: var(--sp-1);
  padding-inline: var(--sp-2);
}
.head-transports .badge-count { padding-inline: var(--sp-1); }

/* Со счётчиком поле справа уже, чем слева. Числа равные, а глазу нет: слева от
   слова пустота, справа — залитый кружок, и одинаковый отступ читается как
   перекос, будто счётчик отъехал от края. Табы без счётчика (он снимается,
   когда непрочитанного нет) остаются симметричными. */
.filter-tab:has(.badge-count) { padding-right: var(--sp-2); }

/* Строка поиска: свёрнутое поле-кружок и рядом фильтр вида переписок.

   Обрезка обязательна: раскрытое поле занимает строку целиком, и фильтры
   уезжают под него, а не переносятся на второй ряд и не рождают прокрутку.
   Собственные поля в 4 точки — место под кольцо фокуса поля (--ring-accent
   рисуется снаружи пилюли, и обрезка срезала бы его); вместе с полем строки
   они дают привычные --sp-4 от края колонки. */
.side-search {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 var(--sp-3) var(--sp-2);
  padding: var(--sp-1);
  overflow: hidden;
}

/* Поле поиска. Свёрнутое — ровно кружок в высоту поля: внутрь помещается
   только лупа, ввод сжат до нуля, но остаётся достижимым фокусом (табом с
   клавиатуры, нажатием по лупе — поле-пилюля это <label>). Раскрытие —
   единственная анимация ширины в интерфейсе, поэтому оно и живёт здесь. */
.side-search .field {
  flex: none;
  width: var(--size-field);
  min-width: 0;
  gap: var(--sp-2);
  padding-inline: var(--sp-3);
  overflow: hidden;
  transition:
    width var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

.side-search.open .field { width: 100%; }

.side-search .search-clear { margin-right: calc(-1 * var(--sp-2)); }

/* Ряд фильтров стоит в той же строке, что и поиск, и отдаёт ему место: при
   раскрытии он сжимается до нуля и уходит под обрезку строки. Скрываем его
   после того, как раскрытие доиграло: `visibility` в переходе держится до
   самого конца, поэтому фильтры уезжают на глазах, а не пропадают рывком.
   Своей прокруткой и высотой ряд занимается сам (.filter-row в теме). */
.side-filters {
  flex: 1 1 auto;
  min-width: 0;
  transition: visibility var(--dur-base) var(--ease-out);
}

.side-search.open .side-filters {
  visibility: hidden;
  pointer-events: none;
}

/* Список доходит до нижнего края экрана: нижней панели под ним больше нет.
   Отсюда и запас на полосу жестов телефона — раньше его держала панель. */
.side-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: 0 var(--sp-3) calc(var(--sp-3) + var(--safe-b));
}

/* --- строка переписки --------------------------------------------------- */

.chat-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  /* Строка не растягивается содержимым: длинное имя обрезается внутри
     .chat-body, а время со счётчиком остаются на экране. */
  min-width: 0;
  flex: none;
  text-align: left;
  padding: var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: inherit;
  cursor: pointer;
  /* Долгое нажатие открывает меню действий, и выделять при этом название
     переписки незачем: выделять в строке нечего, а подсвеченный кусок текста
     под всплывшим меню читается как сбой. Приставка обязательна: Safari
     понимает только её, и без неё на iPhone строка выделялась по-прежнему.
     `touch-callout` там же — им Safari поднимает свою панель над выделенным. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Строки за пределами экрана не отрисовываем вовсе.

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

   `content-visibility: auto` снимает с браузера раскладку и отрисовку всего,
   что сейчас не видно, — вместе с распаковкой картинок. Высота при этом
   обязана быть известна заранее, иначе полоса прокрутки прыгает:
   `contain-intrinsic-size: auto` просит браузер запомнить настоящую высоту
   после первой отрисовки, а число рядом — оценка до неё. */
.chat-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 72px;
}

/* Наведение и нажатие показывает ЗАЛИВКА, а не обводка: язык здесь обязан
   быть тот же, что у круглых кнопок, иначе наведение на кнопку говорит одно,
   а на строку рядом — другое. */
.chat-item:hover { background-color: var(--c-row-hover); }
.chat-item:active { background-color: var(--c-row-press); }
/* Выбранное СВЕТЛЕЕ панели, а не темнее: пока колонка показывала фон
   страницы, --c-bg отличал строку от него; на панели тот же цвет провалил бы
   строку в дыру. */
.chat-item.active {
  background: var(--c-surface-sunken);
  box-shadow: var(--neo-in-sm);
}
.chat-item.active:hover { box-shadow: var(--neo-in-sm); }
.chat-item[disabled] { cursor: default; box-shadow: var(--neo-in-sm); }

/* Выбранная в режиме выделения строка — по тому же правилу, что и открытая:
   выбранное остаётся вдавленным. Своего оформления у неё нет намеренно, иначе
   в списке появился бы второй язык «нажатого». */
.chat-item.selected {
  background: var(--c-surface-sunken);
  box-shadow: var(--neo-in-sm);
}
.chat-item.selected:hover { box-shadow: var(--neo-in-sm); }

/* Отметка выбора: пустой вдавленный кружок, выбранный — акцентный с галочкой.
   Размер тот же, что у счётчика непрочитанного: в одной строке с ним два
   разных кружка выглядели бы как две разные сущности. */
.chat-check {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--size-badge);
  height: var(--size-badge);
  border-radius: var(--r-circle);
  background: var(--c-surface-sunken);
  box-shadow: var(--neo-in-sm);
  color: var(--c-text-3);
}
.chat-check.on {
  background-image: var(--grad-accent);
  color: var(--c-on-accent);
  box-shadow: var(--shadow-accent);
}

.chat-avatar { flex: none; }

/* Обёртка кружка переписки: держит неоновую подпись мессенджера в углу.
   Нужна из-за обрезки — у кружка `overflow: hidden` ради фотографии, и подпись
   внутри него срезало бы по кругу. Своего размера не задаём: обёртка
   обхватывает кружок, поэтому @media, увеличивающая его на телефоне,
   продолжает работать через тот же потомковый селектор. */
.chat-art {
  position: relative;
  display: inline-flex;
  flex: none;
}

.chat-body {
  flex: 1;
  min-width: 0;
  /* Ниже стоит разделитель строк, и считается он от НИЗА этой колонки —
     значит, колонка обязана дотягиваться до низа строки. На вид это ничего не
     меняет: имя и превью по-прежнему посередине, просто теперь их держит не
     выравнивание строки, а своё. */
  position: relative;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Волосок между строками списка (решение владельца продукта 19.09.2026).

   Идёт ТОЛЬКО под текстовой колонкой: ни аватар, ни время он не пересекает.
   Линия во всю ширину разрезала бы и кружок, и правый столбец, и список
   читался бы таблицей, а он остаётся списком — линия лишь подсказывает, где
   кончается одна переписка и начинается следующая.

   Поэтому линия и привязана к .chat-body, а не к самой строке: ширина
   текстовой колонки плавает от длины времени и от того, есть ли счётчик, и
   повторить её числом нельзя — она подгонит себя сама.

   Стоит по нижнему краю строки, а не в зазоре между строками: у .chat-item
   включён content-visibility, а он обрезает всё, что вылезло за поле строки.

   У последней строки линии нет: подводить итог списку нечему. */
.side-list .chat-item:not(:last-child) .chat-body::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--sp-3) * -1);
  height: 1px;
  background: var(--c-hairline);
}

.chat-title {
  display: block;
  min-width: 0;
  font-weight: var(--fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-preview {
  display: block;
  min-width: 0;
  margin-top: var(--sp-1);
  color: var(--c-text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Отметка «печатает»: точки и подпись в одну строку. Встаёт и на место превью
   в списке, и в строку под именем в шапке переписки, поэтому правило одно.
   Идёт ПОСЛЕ .chat-preview намеренно: специфичность у них одинаковая, и
   строчную раскладку превью должно перебить то, что ниже. */
.typing-line {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.chat-meta {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-1);
}

.chat-time { white-space: nowrap; }

/* Закреплённая и беззвучная переписка: иголка и перечёркнутый колокольчик
   стоят в одной строке со временем, поэтому правая колонка остаётся в два
   ряда и строка не становится выше аватара. Оба значка служебные — берут
   самый тихий уровень текста, а не акцент. */
.chat-when { gap: var(--sp-1); }
.chat-pin,
.chat-mute { color: var(--c-text-3); }

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

   Находка — это строка того же роста и с той же геометрией, что строка
   переписки: аватар, две строки текста, время справа. Своего вида у неё нет
   намеренно — в одном списке не должно быть двух языков строк.
   ------------------------------------------------------------------------ */

/* Группа находок под списком переписок. */
.side-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.side-group-head {
  padding: var(--sp-3) var(--sp-3) var(--sp-1);
  text-transform: uppercase;
}

.search-hit {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-width: 0;
  flex: none;
  text-align: left;
  padding: var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: inherit;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
.search-hit:hover { background-color: var(--c-row-hover); }
.search-hit:active { background-color: var(--c-row-press); }

/* Совпадение, на котором человек стоит сейчас: то же вдавленное состояние, что
   у открытой переписки, — «выбранное остаётся вдавленным». */
.search-hit.active {
  background: var(--c-surface-sunken);
  box-shadow: var(--neo-in-sm);
}

/* Имя и время в одну строку: во второй строке — сам текст с подсветкой. */
.search-hit-top {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-width: 0;
}
.search-hit-top .chat-title { flex: 1 1 auto; }
.search-hit-top .chat-time { flex: none; }

/* Найденное слово внутри строки. Подложка акцентная, но тон текста прежний:
   это отметка места, а не второй акцент в списке. */
.search-mark {
  background: var(--c-accent-soft);
  color: var(--c-text);
  border-radius: var(--r-sm);
  padding: 0 2px;
}

.search-empty,
.search-more {
  padding: var(--sp-3);
  text-align: center;
}

.search-more {
  width: 100%;
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--c-accent-text);
  cursor: pointer;
  font-family: inherit;
}
.search-more:hover { background-color: var(--c-row-hover); }

/* ======================================================================
   Колонка переписки
   ====================================================================== */

/* Шапка, строка поиска и поле ввода — панели ПОВЕРХ подложки: заливка им
   нужна не для красоты, а чтобы подложка не просвечивала сквозь них и не
   мешала читать имя собеседника и набранный текст. */
.main-head {
  position: relative;
  z-index: var(--z-head);
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  padding-top: calc(var(--sp-3) + var(--safe-t));
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-hairline);
}

/* Длинное имя обязано обрезаться, а не распирать шапку. */
.main-head > * { min-width: 0; }

/* Имя собеседника и строка под ним. Съёживается первой (flex-shrink), потому
   что кнопки в шапке ужиматься не умеют: у них фиксированный круг. */
.main-who {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Кнопка «назад» нужна только там, где экран один. */
.main-back { display: none; }

/* Кроме выхода из обсуждения: тред закрывает собой переписку на ЛЮБОЙ ширине,
   и без этой кнопки из него не выйти мышью — слева в этот момент список
   переписок, а не та переписка, из которой пришли. */
.thread-back { display: inline-grid; }

/* --- поиск внутри переписки ----------------------------------------------

   Строка поиска стоит между шапкой и лентой и НЕ прокручивается вместе с ней:
   пока ищут, счётчик «3 из 17» и стрелки обязаны оставаться под рукой.
   Раскрытое поле здесь сразу во всю ширину — в отличие от строки над списком:
   лупу уже нажали, и кружок-заготовка был бы лишним шагом.
   ------------------------------------------------------------------------ */

.chat-search-wrap {
  flex: none;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-hairline);
}

.chat-search {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
}

.chat-search .field { flex: 1 1 auto; min-width: 0; }

/* Счётчик не сжимается и не переносится: «120 из 500+» обязано читаться
   целиком, иначе человек не понимает, далеко ли ещё идти. */
.chat-search-count {
  flex: none;
  white-space: nowrap;
}

/* Список находок: выпадает под строкой поиска и закрывает верх ленты, но не
   всю её — под ним видно, куда ведёт переход. Своя прокрутка обязательна:
   находок бывают сотни. */
.chat-search-list {
  display: none;
  flex-direction: column;
  gap: var(--sp-1);
  min-height: 0;
  max-height: 45vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--sp-3) var(--sp-2);
}
.chat-search-list.open { display: flex; }

.feed {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  /* На широком экране лента не растягивается на всю ширину: поля растут
     сами, пока колонка содержимого не упрётся в --feed-max. */
  padding: var(--sp-4) max(var(--sp-4), (100% - var(--feed-max)) / 2) var(--sp-5);
  /* Плавности прокрутки здесь НЕТ намеренно. Объявленная на самой ленте, она
     распространяется на каждое присваивание scrollTop из кода — а таких мест
     много, и почти все они не прокрутка, а возвращение ленты на место после
     перерисовки или под уехавшую клавиатуру. Плавность превращала их в
     отдельные, догоняющие движения: экран уже встал, а лента ещё ползёт.
     Поэтому по умолчанию мгновенно, а плавно — только там, где движение и
     есть смысл: своё отправленное сообщение (scrollFeedToEnd) и переход к
     цитируемому (scrollToMessage) просят её у браузера явно. */
}

.msg {
  display: flex;
  flex-direction: column;
  max-width: min(var(--msg-max), 88%);
}
.msg.in { align-self: flex-start; align-items: flex-start; }
.msg.out { align-self: flex-end; align-items: flex-end; }

/* Пузырь сам знает свой предел ширины; внутри .msg он уже ограничен. */
.msg .bubble { max-width: 100%; }

/* Разделитель дня и отметка начала переписки идут во всю ширину ленты. */
.msg-day,
.feed-start {
  align-self: stretch;
  width: 100%;
}

/* Заглушка страницы прошлого: стоит над лентой на месте будущего пузыря
   собеседника, поэтому и ширина у неё как у пузыря. */
.feed-older {
  flex: none;
  align-self: flex-start;
  width: min(var(--msg-max), 60%);
}

/* Имя автора в группе и канале — над телом сообщения, в ширину пузыря. */
.bubble-author {
  display: block;
  max-width: 100%;
  margin-bottom: var(--sp-1);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
}

/* Время и галочка садятся В КОНЕЦ последней строки текста, а не под неё —
   так короткое сообщение занимает одну строку вместо двух, и лента вмещает
   заметно больше. Приём тот же, что у привычных мессенджеров.

   Делает это `float`, а не абсолютное положение: плавающая метка встаёт в
   остаток последней строки, если он есть, и сама съезжает на свою строку,
   если текст занял её до конца. Абсолютной пришлось бы вручную считать, есть
   ли место, — а ширина метки зависит от шрифта и от «изменено» рядом.

   Разбег между временем и галочкой меньше, чем у обычного hstack: это одна
   метка, а не два элемента. Сдвиг вниз сделан transform'ом, а НЕ отступом:
   отступ прибавился бы к высоте строки и пузырь подрос бы на пиксель. */
.bubble .bubble-meta {
  float: right;
  justify-content: flex-end;
  gap: var(--sp-1);
  margin-left: var(--sp-3);
  transform: translateY(3px);
}

/* Подпись под кадром: своё поле, потому что у медиа-пузыря его нет. flow-root
   держит внутри плавающую метку времени — без него она вывалилась бы из
   обёртки и встала бы рядом с кадром. */
.bubble-caption {
  display: flow-root;
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
}

/* Время, севшее на кадр, уже не плавает в строке текста: текста под ним нет, а
   место ему задают края самого кадра. */
.bubble .bubble-meta.on-media {
  position: absolute;
  right: var(--sp-2);
  bottom: var(--sp-2);
  float: none;
  margin-left: 0;
  transform: none;
  /* Нажимать на метку не на что, а лежит она поверх кадра и при этом НЕ внутри
     него — она соседка кнопки по пузырю. Без этого касание по правому нижнему
     углу картинки доставалось подписи и до просмотра не доходило вовсе. */
  pointer-events: none;
}

/* Цифры времени одной ширины: иначе метка дёргается при каждой минуте. */
.bubble-time { font-variant-numeric: tabular-nums; }

/* «изменено» рядом со временем: та же метка, тот же приглушённый тон. Цвет
   берёт .bubble-meta от пузыря, поэтому своего здесь нет — на акцентном фоне
   серый не читался бы. */
.bubble-edited { font-style: italic; }

/* --- разметка текста сообщения ------------------------------------------ */

/* Ссылка. Длинный адрес рвётся по любому знаку: иначе он распирает пузырь на
   всю ленту. В исходящем пузыре фон акцентный, и красить ссылку акцентом же
   нельзя — она сливается; там она берёт цвет текста пузыря, а отличается
   подчёркиванием. */
.msg-link {
  color: var(--c-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
}
.bubble.out .msg-link { color: var(--c-on-bubble-out); }

.msg-code,
.msg-pre {
  font-family: var(--font-mono);
  font-size: 0.92em;
  background: var(--c-surface-sunken);
}
.msg-code {
  padding: 0 var(--sp-1);
  border-radius: var(--r-sm);
}
/* Код блоком: строки не переносим, а прокручиваем — перенос ломает те самые
   отступы, ради которых код и пишут блоком. */
.msg-pre {
  display: block;
  margin: var(--sp-1) 0;
  padding: var(--sp-2);
  border-radius: var(--r-md);
  overflow-x: auto;
  white-space: pre;
}
/* На акцентном фоне утопленная поверхность не видна вовсе — берём затемнение
   поверх самого пузыря. */
/* Код в исходящем пузыре стоит на той же вложенной плашке, что цитата и
   реакции: одна сущность — один цвет. */
.bubble.out .msg-code,
.bubble.out .msg-pre { background: var(--c-bubble-out-panel); }

/* Цитата внутри сообщения — это разметка автора, а не наша плашка ответа. */
.msg-quote {
  display: block;
  margin: var(--sp-1) 0;
  padding-left: var(--sp-2);
  border-left: 2px solid var(--c-accent);
  color: var(--c-text-2);
}
.bubble.out .msg-quote {
  border-left-color: var(--c-on-bubble-out);
  color: var(--c-on-bubble-out-2);
}

/* Скрытый текст: до нажатия виден только залитый прямоугольник. Прячем цветом
   самого текста, а не подложкой — подложка выдала бы его форму. */
.msg-spoiler {
  border-radius: var(--r-sm);
  background: var(--c-text-3);
  color: transparent;
  cursor: pointer;
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}
.msg-spoiler.shown {
  background: none;
  color: inherit;
  cursor: auto;
}

/* --- поле ввода --------------------------------------------------------- */

/* Строка ввода занимает ВСЮ ширину окна переписки, а не колонку ленты, и
   ПЛАВАЕТ над ней.

   Раньше она стояла в той же центрированной колонке, что и лента, — чтобы на
   широком экране пилюля не уезжала к краю. В привычных мессенджерах строка
   ввода идёт во всю ширину окна, и владелец продукта выбрал так же
   (17.09.2026): лента остаётся колонкой, а поле ввода — полосой под ней.

   Своей заливки и линии сверху у полосы больше нет (18.09.2026, по образцу
   WhatsApp): над обоями висит сама пилюля поля, а справа от неё — кнопка
   отправки. Отступы полосы и есть те поля, сквозь которые видны обои, и
   поэтому они меньше прежних: раньше их держала заливка. */
.composer {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  padding-bottom: calc(var(--sp-2) + var(--safe-b));
}

/* Строка ввода — поле и кнопка отправки. Отдельным рядом, потому что над ней
   встаёт плашка выбранного файла, а с ней поле ввода и кнопка обязаны
   остаться на одной линии. */
.composer-row {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
}

/* Пилюля поля занимает всё, что осталось от кнопки отправки: снаружи стоит
   только она, а скрепка и эмодзи вернулись ВНУТРЬ пилюли, слева от текста.
   Так поле шире — на телефоне это самое тесное место экрана.

   Кнопки внутри прижаты к нижнему краю, а не к середине: поле растёт вверх
   под многострочный текст, и от середины они уезжали бы вместе с ним.
   Справа отступ больше: там текст упирается в край пилюли, а не в кнопку.

   Радиус в половину высоты пустого поля: пока строка одна, это ровно пилюля,
   а когда поле выросло — скруглённый прямоугольник, из которого текст не
   вылезает за срезанные углы (с --r-pill вылезал). */
.composer .field {
  flex: 1;
  min-width: 0;
  align-items: flex-end;
  gap: var(--sp-1);
  padding: var(--sp-1);
  padding-right: var(--sp-4);
  border-radius: calc(var(--size-field) / 2);
}

/* Кнопки внутри пилюли своей ширины не отдают: место в строке забирает
   растущее поле, и без этого длинный текст сплющил бы скрепку в овал. */
.composer .field > .btn-round { flex: none; }

/* Кнопка отправки мельче обычной круглой — см. --send-size.

   На ней же живёт жест записи голосового: прижать, подержать, увести влево.
   Этот ход нельзя отдавать браузеру — он примет его за прокрутку или за
   двойное касание с масштабом и оборвёт поток pointermove на полпути.
   Прокрутки ленты запрет не касается: он стоит на самой кнопке, а лента —
   другой узел в другом месте экрана, и касание до неё не доходит. */
.composer-send {
  width: var(--send-size);
  height: var(--send-size);
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* Поле сообщения растёт под текст, но не бесконечно: дальше текст
   прокручивается внутри поля, а лента остаётся на экране. Потолок задан здесь,
   а не в коде: разметка ставит высоту по содержимому, а срезает её max-height. */
.composer .composer-input {
  max-height: var(--composer-max-h);
  padding-block: var(--sp-2);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  word-break: break-word;
}

.composer-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Плашки над пилюлей — ответ, правка, выбранный файл — идут по ширине САМОЙ
   пилюли: справа от них место кнопки отправки, и растягивать плашку под
   кнопку незачем — она висела бы над пустотой. Отбирается ровно ширина
   кнопки и разбег строки.

   Правило прицельно к детям .composer: та же .composer-reply собирает
   плашку «переслать вот это» в окне выбора адресата, и там места кнопки
   отправки нет. */
.composer > .composer-reply,
.composer > .composer-attach {
  margin-right: calc(var(--send-size) + var(--sp-3));
}

/* --- запись голосового ---------------------------------------------------- */

/* Строка записи встаёт на место поля ввода и берёт ту же пилюлю темы: высота
   не меняется, и кнопка отправки рядом не прыгает. Содержимое здесь в одну
   строку и стоит по центру, а не по нижнему краю, как растущее поле. */
.composer .voice-bar {
  align-items: center;
  padding-block: 0;
  /* Поля здесь шире, чем у поля ввода: слева у пилюли стоит не кнопка, а
     точка записи, и вплотную к краю она читается как брак. */
  padding-inline: var(--sp-4);
}

/* Точка «идёт запись». Цвет тот же, которым помечено всё срочное; цвет здесь
   не единственный носитель смысла — рядом стоят время и слово. */
.voice-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: var(--r-circle);
  background: var(--c-error);
  animation: voice-pulse 1.4s var(--ease-in-out) infinite;
}
@keyframes voice-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .3; }
}

/* Секунды не должны дёргать соседей шириной знаков. */
.voice-time {
  flex: none;
  font-variant-numeric: tabular-nums;
}

/* Слово о том, что происходит: место отдаёт волне, но само не прячется —
   цвет точки не должен оставаться единственным носителем смысла. */
.voice-note {
  flex: 0 1 auto;
  color: var(--c-text-2);
}

/* Живая волна. Это холст, а не ряд узлов, — почему именно так, написано в
   app.js у drawWave(). Высота постоянная: от громкости пилюля меняться не
   должна, иначе кнопка отправки рядом запрыгает. */
.voice-wave {
  flex: 1 1 0;
  min-width: 24px;
  height: 22px;
  /* Отсюда холст берёт цвет столбиков: внутри canvas currentColor не
     работает, и код читает вычисленный color этого узла. Цвет приглушённый,
     а не акцентный: акцент на экране один, и он уже занят кнопкой отправки. */
  color: var(--c-text-2);
}

/* Подсказка про увод влево. Стоит только пока палец на кнопке: отпущенная
   запись отменяется кнопкой, и говорить про жест уже незачем. */
.voice-hint {
  flex: none;
  min-width: 0;
  color: var(--c-text-2);
  font-size: var(--fs-xs);
  transition: color var(--dur-fast) var(--ease-out);
}

/* Полоса едет за пальцем: сдвиг ставит обработчик жеста. Перехода на сдвиге
   нет намеренно — полоса обязана идти ЗА пальцем, а не догонять его. */
.composer .voice-bar.held {
  transform: translateX(var(--voice-swipe, 0px));
}

/* Порог отмены рядом: подсказка и волна краснеют тем же цветом, что и точка
   записи, — отпустишь сейчас, и записанного не станет. */
.voice-bar.arming .voice-hint,
.voice-bar.arming .voice-wave { color: var(--c-error); }

/* --- реакции ------------------------------------------------------------- */

/* Ряд под текстом сообщения. Переносится: реакций может набраться много, а
   расти в ширину пузырю некуда. */
.bubble-reactions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-top: var(--sp-1);
}

/* Пилюля со знаком и счётчиком. Своя — ВЫПУКЛАЯ: она поднимается над рядом
   чужих, как поднимается кнопка над фоном. Алой обводки здесь больше нет —
   на тёмном пузыре она читалась как ошибка, а не как «моя», и спорила с
   акцентом, которым выделено куда более важное. */
.reaction {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: var(--c-surface-sunken);
  color: var(--c-text-2);
  font: inherit;
  font-size: var(--fs-xs);
  line-height: 1.6;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
.reaction:hover { background: var(--c-row-hover); }
.reaction.mine {
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out-sm);
  color: var(--c-text);
}
.reaction.mine:hover { background: var(--c-bg); }
/* Знак рисует шрифт системы — цвет ему не задаём. */
.reaction-emoji { font-size: var(--fs-sm); }
.reaction-count { font-variant-numeric: tabular-nums; }

/* На пузыре утопленная поверхность не видна — берём вложенную плашку. */
.bubble.out .reaction {
  background: var(--c-bubble-out-panel);
  color: var(--c-on-bubble-out-2);
}
/* На пузыре выпуклость не работает — «моя» передаётся осветлением. */
.bubble.out .reaction.mine {
  background: var(--c-on-bubble-out-fill);
  background-image: none;
  box-shadow: none;
  color: var(--c-on-bubble-out);
}
.bubble.out .reaction.mine:hover { background: var(--c-on-bubble-out-fill); }

/* Своя реакция выскакивает в момент постановки — один раз, на той пилюле,
   которую человек только что нажал. Это единственная анимация, которую мы
   рисуем сами: настоящие анимированные реакции мессенджера — отдельный
   формат, и подменять их этим нельзя. */
@keyframes reaction-pop {
  0% { transform: scale(.5); }
  60% { transform: scale(1.22); }
  100% { transform: scale(1); }
}
.reaction.pop { animation: reaction-pop var(--dur-slow) var(--ease-spring); }

@media (prefers-reduced-motion: reduce) {
  .reaction.pop { animation: none; }
}

/* --- темы форума ---------------------------------------------------------- */

/* Список тем стоит на месте ленты и читается как список переписок слева:
   значок, две строки текста, время и счётчик. Своего языка ему заводить
   незачем — это тот же выбор «куда пойти», только внутри группы. */
.topics-list { padding: var(--sp-2); }

.topic-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--r-card);
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.topic-row:hover { background: var(--c-row-hover); }

/* Кружок со значком: свой эмодзи темы Telegram отдаёт номером набора, за
   которым надо идти отдельным запросом, — вместо него буква названия и цвет
   из палитры, тем же приёмом, что аватарки переписок. */
.topic-art {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--r-pill);
  background: var(--c-surface-raised);
  font-size: var(--fs-sm);
}

.topic-body { flex: 1; min-width: 0; }
.topic-head { gap: var(--sp-2); }
.topic-closed {
  flex: none;
  padding: 0 var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--c-surface-sunken);
  color: var(--c-text-3);
  font-size: var(--fs-xs);
}
.topic-side {
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

/* Строка вместо поля ввода: в закрытую тему писать нельзя, и поле, которое
   всегда отвечает отказом, хуже его отсутствия. */
.composer-note {
  padding: var(--sp-3);
  text-align: center;
}

/* --- обсуждение под постом ----------------------------------------------- */

/* Вход в обсуждение — короткий сдвиг от правого края, как переход на
   следующий экран в телефоне: тред и правда лежит «поверх» переписки, и без
   движения подмена содержимого читается как рывок.

   Только появление и только один раз: класс ставится при открытии (см.
   renderThread), а не на каждую перерисовку. Уходит экран без анимации —
   узел просто снимается, и провожать его нечем.

   Общий блок prefers-reduced-motion гасит это вместе со всей прочей
   анимацией — отдельного правила не нужно. */
@keyframes thread-in {
  from { opacity: 0; transform: translateX(3%); }
  to   { opacity: 1; transform: none; }
}
.thread-enter { animation: thread-in var(--dur-base) var(--ease-out) both; }

/* Вход в комментарии — не пилюля, как реакция, а полоса во всю ширину пузыря:
   реакция это отметка на самом сообщении, а это переход на другой экран, и
   путать их нельзя. Линия сверху по той же причине: ниже неё уже не
   сообщение. */
.comments-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  margin-top: var(--sp-2);
  padding: var(--sp-2) 0 0;
  border: 0;
  border-top: 1px solid var(--c-hairline);
  background: none;
  /* Акцент как ТЕКСТ — свой токен: сплошной алый на тёмном фоне не читается. */
  color: var(--c-accent-text);
  font: inherit;
  font-size: var(--fs-xs);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.comments-bar-label { font-variant-numeric: tabular-nums; }

/* На акцентном фоне своего пузыря акцентный текст не читается — там всё
   строится на цветах поверх акцента, как у реакций выше. */
.bubble.out .comments-bar {
  border-top-color: var(--c-on-bubble-out-2);
  color: var(--c-on-bubble-out);
}

/* --- ответ на сообщение -------------------------------------------------- */

/* Плашка «отвечаем на это» над строкой ввода и цитата внутри пузыря собраны
   одинаково: полоска, две строки текста, — поэтому и внутренности у них общие
   (`reply-bar`, `reply-body`). Разное только обрамление. */
.composer-reply,
.bubble-reply {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  text-align: left;
}

.composer-reply { padding-right: var(--sp-1); }

/* Цитата — кнопка: нажатие уводит к оригиналу. Своей рамки и объёма ей не
   нужно, она живёт внутри пузыря. */
.bubble-reply {
  width: 100%;
  margin-bottom: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  border: 0;
  border-radius: var(--r-md);
  background: var(--c-surface-sunken);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
/* Не кнопка — оригинала у нас нет, вести некуда. */
div.bubble-reply { cursor: default; }
button.bubble-reply:hover,
button.bubble-reply:focus-visible { background-color: var(--c-row-hover); }
/* На пузыре утопленная поверхность не видна — берём вложенную плашку. */
.bubble.out .bubble-reply { background: var(--c-bubble-out-panel); }

.reply-bar {
  flex: none;
  align-self: stretch;
  width: 2px;
  min-height: 2rem;
  border-radius: 1px;
  background: var(--c-accent);
}
.bubble.out .reply-bar { background: var(--c-on-bubble-out); }

.reply-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.reply-author { color: var(--c-accent); }
.bubble.out .reply-author { color: var(--c-on-bubble-out); }
/* Цитата — всегда одна строка: она напоминание, а не второе сообщение. */
.reply-text {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--c-text-2);
}
.bubble.out .reply-text { color: var(--c-on-bubble-out-2); }

/* --- пометка «переслано» ------------------------------------------------- */

/* Строка над сообщением: иконка и от кого. Не цитата: своего фона и полоски
   у неё нет намеренно — у пузыря с пересланным ОТВЕТОМ иначе встали бы две
   одинаковые плашки подряд, и глаз перестал бы их различать. */
.bubble-forward {
  gap: var(--sp-1);
  margin-bottom: var(--sp-1);
  min-width: 0;
  color: var(--c-accent);
}
.bubble.out .bubble-forward { color: var(--c-on-bubble-out-2); }
/* Подпись берёт цвет строки: своего приглушённого она здесь не заслуживает —
   это не служебная мелочь, а имя источника. */
.bubble-forward .t-micro { color: inherit; }

/* «копия» — не украшение, а предупреждение: у собеседника это сообщение
   выглядит обычным, без всякой пометки. Поэтому она отделена подложкой, а не
   слита с именем источника. */
.forward-copy {
  flex: none;
  padding: 0 var(--sp-1);
  border-radius: var(--r-sm);
  background: var(--c-surface-sunken);
}
.bubble.out .forward-copy { background: var(--c-bubble-out-panel); }

/* Найденный по цитате оригинал: короткая подсветка, чтобы глаз зацепился. */
.msg.found .bubble {
  animation: found-flash 2s var(--ease-out);
}
@keyframes found-flash {
  0%, 55% { box-shadow: 0 0 0 2px var(--c-accent); }
  100% { box-shadow: 0 0 0 2px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .msg.found .bubble { animation: none; box-shadow: 0 0 0 2px var(--c-accent); }
}

/* ======================================================================
   Пустые состояния, скелетоны, тосты — только позиционирование
   ====================================================================== */

.main > .empty-state,
.side-list > .empty-state {
  flex: 1;
  min-height: 0;
}

/* Скелетон повторяет геометрию строки списка, чтобы список не прыгал,
   когда данные пришли. */
.skeleton-row {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
}
.skeleton-row .skeleton.circle { flex: none; }

.skeleton-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* Заглушка ленты: те же пузыри, стороны и ширины чередует разметка.
   Заглушка занимает всю ленту и прижата к её низу — переписка читается снизу,
   и настоящие сообщения встанут ровно туда же. Своих полей у заглушки нет:
   их даёт сама лента, иначе пузыри-заглушки стояли бы уже настоящих.
   Появляется заглушка не сразу: короткая задержка `--delay-skeleton` гасит её
   для быстрых ответов, чтобы она не мигнула на пустом месте. Держит паузу
   fill-mode `both` — до конца задержки прозрачность нулевая. */
.skeleton-feed {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--sp-3);
  animation: fade-in var(--dur-base) var(--ease-out) var(--delay-skeleton) both;
}
.skeleton-feed .skeleton { align-self: flex-start; flex: none; }
.skeleton-feed .skeleton.out { align-self: flex-end; }

/* Заглушка исходящего — это ЗАГЛУШКА, а не сообщение.
   Акцентную заливку и тень она получала от `.bubble.out`: у того два класса
   против одного у `.skeleton`, и красный побеждал серый. Лента на долю
   секунды показывала алые пузыри там, где ещё ничего нет, — заметнее всего
   при открытии обсуждения, где ответ приходит быстро и заглушка сразу
   гаснет. */
.skeleton-feed .skeleton.bubble.out {
  background-image: var(--grad-skeleton);
  box-shadow: none;
}
.skeleton-feed .skeleton.bubble { max-width: min(var(--msg-max), 72%); }
.skeleton-feed .skeleton.bubble.wide { width: 68%; }
.skeleton-feed .skeleton.bubble.mid { width: 46%; }
.skeleton-feed .skeleton.bubble.narrow { width: 28%; }

/* Тост не должен садиться на поле ввода: поднимаем его на высоту пилюли
   плюс её отступы. */
.toast-stack { bottom: calc(var(--size-field) + var(--sp-7) + var(--safe-b)); }

/* ======================================================================
   Вход и подключение аккаунта
   ====================================================================== */

.auth {
  min-height: 100%;
  display: grid;
  place-items: center;
  padding: var(--sp-6) var(--sp-5);
  padding-top: calc(var(--sp-6) + var(--safe-t));
  padding-bottom: calc(var(--sp-6) + var(--safe-b));
  overflow-y: auto;
}

.auth-card {
  width: 100%;
  max-width: var(--auth-max);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* Форма входа. Своего разбега между всеми полями нет: подпись обязана
   прижиматься к своему полю, а не висеть ровно посередине между двумя. Поэтому
   расстояния задают отступы подписей (.field-label) и отбивки ниже. */
.auth-form {
  display: flex;
  flex-direction: column;
}
.auth-form .field-label { margin-top: var(--sp-4); }
.auth-form .field-label:first-child { margin-top: 0; }

/* Место под сообщение об ошибке держим всегда — форма не должна прыгать под
   руками, когда ошибка появилась или ушла. */
.auth-form .field-error {
  min-height: calc(var(--fs-sm) * var(--lh-normal));
  margin-top: var(--sp-3);
}

/* Главное действие занимает строку целиком, «Зарегистрироваться» — под ним:
   на узкий экран две пилюли в ряд не встают, а перенос ломает выравнивание. */
.auth-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

.auth-note {
  margin: var(--sp-5) 0 0;
  text-align: center;
}

/* Код входа. Белое поле обязательно: с тонированной подложки часть сканеров
   код не берёт, поэтому здесь чистый --c-white, а не самая светлая
   поверхность темы. Раньше тут стоял --c-on-accent — «цвет поверх заливки»,
   который тогда случайно был #fff; после перекраски он перестал им быть. */
.qr {
  width: min(260px, 70vw);
  margin: var(--sp-3) auto;
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--c-white);
}
.qr svg,
.qr img { display: block; width: 100%; height: auto; }

/* Виджет проверки «я не робот»: высота с запасом, на телефоне он
   разворачивается заметно выше, чем на широком экране. */
.captcha-frame {
  width: 100%;
  height: 420px;
  border: 0;
  margin-top: var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--c-white);
}

/* --- режим выбора сообщений ---------------------------------------------- *
 *
 * Строка сообщения перестраивается в ряд: слева круглая отметка, справа
 * прежнее содержимое одной колонкой. Отметка стоит у КРАЯ ЛЕНТЫ, одинаково
 * для своих и чужих сообщений, — так её легко найти глазом, ведя палец сверху
 * вниз, и так это сделано во всех привычных мессенджерах.
 *
 * Сами пузыри при этом остаются на своих сторонах: выбор не должен менять
 * привычную раскладку переписки, иначе лента перестаёт читаться.
 */
.msg.pickable {
  flex-direction: row;
  align-items: center;
  align-self: stretch;
  max-width: 100%;
  gap: var(--sp-3);
  cursor: pointer;
}

.msg.pickable > .msg-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  max-width: min(var(--msg-max), 88%);
}
.msg.in.pickable > .msg-body { align-items: flex-start; }
/* Своё держится правого края: колонка занимает остаток строки и прижимает
   содержимое к нему. */
.msg.out.pickable > .msg-body { align-items: flex-end; margin-left: auto; }

/* Плашка действий над выбранным ВИСИТ НАД ЛЕНТОЙ, а не занимает строку под
   ней. Сообщения уходят под неё и там размываются — так видно, что лента
   продолжается, и плашка не отъедает у неё высоту. Действия слева, под
   большим пальцем; счётчик справа, где его читают, а не нажимают. */
.pick-bar {
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: calc(var(--sp-4) + var(--safe-b));
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
}

/* Лента под плашкой: запас снизу ровно на её высоту с полями, иначе последнее
   сообщение пряталось бы под ней насовсем. */
.feed.picking {
  padding-bottom: calc(var(--pick-bar-h) + var(--sp-6) + var(--safe-b));
}

/* ======================================================================
   Модальное окно
   ====================================================================== */

.modal-back {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  padding-top: calc(var(--sp-5) + var(--safe-t));
  padding-bottom: calc(var(--sp-5) + var(--safe-b));
}

/* Содержимое окна — столбик с общим разбегом. Без него шапка, строки
   аккаунтов, карточки шагов и кнопки лежали бы вплотную: своих полей у них
   нет, а умолчания браузера тема снимает. */
.modal {
  width: 100%;
  max-width: var(--modal-max);
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* Прокрутка внутри окна, а не сплющивание содержимого. */
.modal > * { flex: none; }

/* Разбег даёт контейнер, собственная отбивка шапки здесь лишняя. */
.modal .sheet-head { margin-bottom: 0; }

/* Кнопки «подключить»: на узком экране переносятся, а не распирают окно. */
.sheet-actions {
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.sheet-actions .btn-pill { flex: 1 1 auto; }

/* --- кнопка «Написать» и адресная книга ----------------------------------- */

/* Кнопка лежит НАД списком, в правом нижнем углу колонки: на телефоне туда
   дотягивается большой палец, и именно там её ищут — так она стоит в любом
   мессенджере. Отсюда и `position: relative` у самой колонки: кнопка
   прижимается к ней, а не к окну, и на широком экране не уезжает на ленту. */
.side { position: relative; }

.side-write {
  position: absolute;
  right: var(--sp-4);
  /* Полоса дома на айфоне и панель браузера занимают нижний край экрана:
     без отступа на безопасную область кнопка оказывается под ними. */
  bottom: calc(var(--sp-4) + var(--safe-b));
  z-index: 2;
  box-shadow: var(--shadow-modal);
}

/* Вкладки книг — по одной на подключённый мессенджер, одной строкой с
   прокруткой вбок. Перенос на вторую строку разрывал окно пополам: у четырёх
   вкладок одна оставалась висеть под остальными. Тот же ход, что у фильтров
   над списком переписок (.filter-row), и полосы прокрутки так же нет. */
.book-tabs {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.book-tabs::-webkit-scrollbar { width: 0; height: 0; }
.book-tabs .btn-pill { flex: none; }

/* Окно книги живёт с ПОСТОЯННОЙ высотой.
 *
 * Обычное окно приложения растёт под содержимое, и это правильно там, где
 * содержимое одно. Здесь оно меняется на каждое нажатие: другая вкладка,
 * другой поиск, приехавшие строки. Окно стоит по центру экрана, поэтому любая
 * смена высоты дёргает его сразу в обе стороны — за это его и ругали.
 *
 * Теперь рамка задана раз и навсегда, а меняется только список внутри неё.
 */
.book-sheet {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  height: min(34rem, 80vh);
  /* Прокручивается только список: у самого окна прокрутки больше нет. */
  overflow: hidden;
}

/* Шапка, вкладки, поиск и форма своей высоты не меняют — растягивается список. */
.book-sheet > .sheet-head,
.book-sheet > .book-tabs,
.book-sheet > .field { flex: none; }

/* Форма занимает место списка целиком: окно у неё то же самое. */
.book-sheet > .book-form { flex: 1 1 auto; }

/* Кнопки формы прижаты к низу окна: высота у окна постоянная, и болтаться
   посередине им незачем. */
.book-form .sheet-actions { margin-top: auto; }

.book-sheet > .sheet-head { margin-bottom: 0; }

.book-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  transition: opacity var(--dur-base) var(--ease-out);
}

/* Идёт запрос: прежние строки на месте и приглушены. Список не пустеет, окно
   не схлопывается, и человеку видно, что ответ ещё едет. */
.book-list.is-loading { opacity: .45; }

/* Строки появляются мягко и все разом: по одной с задержкой — это уже
   представление, а человек пришёл писать сообщение. */
.book-list > * {
  animation: book-row-in var(--dur-base) var(--ease-out) both;
}

/* Заглушка ждёт --delay-skeleton и просто проявляется: книга часто отвечает
   быстрее задержки, и тогда заглушек не видно вовсе — вместо мелькания
   человек видит сразу строки. Всплывать заглушке незачем, она временная. */
.book-list > .skeleton-row {
  animation: fade-in var(--dur-base) var(--ease-out) var(--delay-skeleton) both;
}

/* Книгу уже переключали — дальше список меняется БЕЗ ДВИЖЕНИЯ.
 *
 * Всплытие строк уместно ровно один раз: когда окно наполняется впервые. На
 * каждое переключение вкладки то же движение читается дёрганьем — заглушки
 * проявляются, строки подлетают, и так по кругу на каждое нажатие. Заглушки
 * при этом встают сразу, без выдержки: они заменяют собой прежние строки, и
 * пауза оставила бы на их месте дыру.
 *
 * Класс ставит setBookTransport() и снимает пересборка окна.
 */
.book-list.calm > *,
.book-list.calm > .skeleton-row {
  animation: none;
}

@keyframes book-row-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* Форма нового контакта живёт внутри того же окна: два поля и две кнопки. */
.book-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* Кружок первой строки — тот же, что у переписки, только с плюсом и в
   фирменной заливке: строка «завести человека» читается как действие, а не
   как ещё один собеседник. */
.book-add {
  background-image: var(--grad-accent);
  color: var(--c-on-accent);
  display: grid;
  place-items: center;
}

/* «Пока никого» — тихая строка под списком, без рамок и кнопок: звать уже
   позвали строкой выше. */
.book-quiet {
  padding: var(--sp-2) var(--sp-1);
}

/* --- окно пересылки ------------------------------------------------------ */

/* Список адресатов прокручивается САМ, а не вместе с окном: поле поиска и
   строка про копию обязаны оставаться на виду, пока человек листает список.
   Высота в строках, а не в долях экрана: само окно и так ограничено по
   высоте, а доля экрана на телефоне схлопнула бы список до двух строк. */
.forward-list {
  max-height: 22rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  /* Место под полосу прокрутки: без него она ложится поверх правого края
     строк, а у них там время и счётчик. */
  padding-right: var(--sp-1);
}

/* Строки внутри окна — те же, что в списке слева, своего вида им не нужно. */
.forward-list .chat-item { width: 100%; }

/* --- строка подключённого аккаунта -------------------------------------- */

/* Строка не кликается целиком: действия в ней — отдельные кнопки. Поэтому ни
   курсора-руки, ни подсветки под курсором.
   Перенос запрещён намеренно: строка одна на любом экране, а лишнему тексту
   есть куда деться — название обрезается многоточием. */
.acc {
  align-items: center;
  flex-wrap: nowrap;
  padding-inline: 0;
  cursor: default;
}
.acc:hover { background-color: transparent; }

/* Кружок мессенджера со знаком состояния в углу. Обёртка нужна из-за обрезки:
   у аватара `overflow: hidden`, и знак внутри него срезало бы по кругу.
   Размер держим на обёртке — и у настоящей строки, и у заглушки: когда список
   встаёт на место заглушек, строка не должна подпрыгнуть. */
.acc .acc-art {
  position: relative;
  width: var(--size-avatar-sm);
  height: var(--size-avatar-sm);
  flex: none;
}

/* Селектор из нескольких классов не для красоты: у `.skeleton.circle` в теме
   свой размер, и одним классом его не перебить. */
.acc .acc-art > .avatar,
.acc .acc-art > .skeleton.circle {
  width: 100%;
  height: 100%;
}

/* Место под кнопки в строке-заглушке: ровно та же высота, что у кнопки. */
.acc .acc-hold {
  width: var(--size-avatar-lg);
  height: var(--size-control-sm);
  border-radius: var(--r-pill);
}

/* Знак состояния сидит в правом нижнем углу кружка мессенджера — как отметка
   «в сети» на аватарке. Отдельного места в строке он больше не занимает, и
   строка от этого помещается целиком даже на телефоне. Своя подложка под тон
   поверхности обязательна: знак ложится поверх кружка, а он бывает и
   фотографией, и градиентом. Подсказка со словами остаётся на самом знаке. */
.acc-state {
  position: absolute;
  right: 0;
  bottom: 0;
  display: grid;
  place-items: center;
  width: var(--size-mark);
  height: var(--size-mark);
  border-radius: var(--r-circle);
  /* Подложка тоном поверхности — она и отделяет знак от кружка под ним.
     Кольца сверху не даём: на кружке в 32 точки знак с кольцом закрыл бы
     собой почти весь кружок. */
  background: var(--c-bg);
}

/* Действия — в конце той же строки. Столбиком, как в списке переписок, им
   здесь стоять незачем: подписи под названием больше нет. */
.acc .row-side {
  flex-direction: row;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
}

/* Кнопки строки компактнее общих: полная пилюля в 48 точек вместе с названием
   в узкое окно не влезает, а строка обязана остаться одной. Нижний предел —
   мелкая круглая кнопка (36): меньше в неё не попадают пальцем. */
.acc .row-side .btn-pill {
  min-height: var(--size-control-sm);
  padding-inline: var(--sp-3);
  font-size: var(--fs-sm);
}

/* --- шаги подключения ---------------------------------------------------- */

/* Код входа, облачный пароль и вход по номеру — карточки внутри окна.
   Содержимое столбиком, отбивки внутри элементов не нужны: их даёт gap. */
.qr-step,
.password-step,
.sms-step {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
}

.qr-step { text-align: center; }
.qr-step .qr { margin-block: 0; }
.sms-step .captcha-frame { margin-top: 0; }

/* ======================================================================
   Просмотр картинки во весь экран
   ====================================================================== */

/* Подложка лежит вне .app и не зависит ни от раскладки, ни от того, какой
   экран показан: на узком экране список и переписка сменяют друг друга, а
   просмотр перекрывает и то, и другое целиком. Перерисовка ленты его тоже не
   трогает — он не внутри #app, как и тосты.

   Поля с запасом на вырез и жесты: на телефоне картинка иначе уезжает под
   системные часы и под полосу жестов. */
.viewer-back {
  position: fixed;
  inset: 0;
  z-index: var(--z-viewer);
  display: grid;
  /* Строка и колонка заданы ЯВНО, и это не украшение. У сетки со строкой по
     содержимому высота ячейки неопределённая, а `max-height: 100%` от
     неопределённой высоты не считается вовсе — браузер его молча отбрасывает.
     Картинка вставала в натуральную величину и уезжала за низ экрана: замер на
     широком экране показывал портрет 1200x2400 как есть, с обрезкой на полторы
     тысячи точек. На телефоне это не бросалось в глаза только потому, что там
     хватало ограничения по ШИРИНЕ, — то есть работало случайно.
     `minmax(0, 1fr)` делает ячейку определённой, и оба ограничения оживают. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  padding: var(--sp-4);
  padding-top: calc(var(--sp-4) + var(--safe-t));
  padding-bottom: calc(var(--sp-4) + var(--safe-b));
  background: var(--c-overlay);
  animation: fade-in var(--dur-base) var(--ease-out);
}

/* Картинка вписывается в свободное место целиком и не обрезается. */
.viewer-img {
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--r-card);
}

/* Кнопка закрытия в углу поверх картинки. На узком экране картинка занимает
   почти весь экран, и подложки, по которой можно нажать мимо, почти не
   остаётся — без кнопки закрыть просмотр пальцем было бы нечем. */
.viewer-close {
  position: absolute;
  top: calc(var(--sp-3) + var(--safe-t));
  right: calc(var(--sp-3) + var(--safe-r));
}

/* ======================================================================
   Узкий экран: один экран за раз
   ====================================================================== */

@media (max-width: 899px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
  }

  /* Колонки лежат друг на друге в одной ячейке — переключение видимостью
     не двигает раскладку и не рождает горизонтальную прокрутку. */
  .side,
  .main {
    grid-column: 1;
    grid-row: 1;
    border-right: 0;
  }

  .main { display: none; }

  .app.chat-open .side { display: none; }
  .app.chat-open .main { display: flex; }

  .main-back { display: inline-grid; }

  /* Строка на телефоне выше: кружок здесь крупнее (--size-avatar-touch). */
  .chat-item { contain-intrinsic-size: auto 80px; }

  /* Панель эмодзи — полка во всю ширину у нижнего края, а не окошко у кнопки.

     Окошко приходится ставить относительно экрана, а экран на телефоне
     меняется от появления клавиатуры — от этого панель и прыгала. У полки
     место задано краями, и перестановка ей не нужна вовсе.

     Слой под панелью запрещает жесты, чтобы под ним не ехала лента; самой
     полке вертикальную прокрутку возвращаем — знаков в ней много. */
  .emoji-panel.emoji-sheet {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-width: none;
    max-height: 45vh;
    padding-bottom: calc(var(--sp-2) + var(--safe-b));
    border-radius: var(--r-card) var(--r-card) 0 0;
    touch-action: pan-y;
  }
  /* Во всю ширину знаков в ряд помещается больше, чем восемь. */
  .emoji-sheet .emoji-grid {
    grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr));
  }

  /* Ни наведения, ни отметки «открыта».

     Наведения на телефоне не бывает вовсе: палец его не даёт, зато после
     нажатия подсветка ЗАЛИПАЕТ — браузер держит :hover на последней нажатой
     строке, и по возвращении из переписки список показывает подсвеченную
     строку без причины.

     Отметка «открыта» здесь тоже лишняя: открытая переписка закрывает список
     целиком, и подсвечивать нечего — а после возврата подсветка читается как
     выделение, которого человек не делал. Отметка ВЫБРАННОЙ (.selected)
     остаётся: её человек ставит сам, и список при этом на экране. */
  .chat-item:hover,
  .chat-item.active,
  .chat-item.active:hover,
  .search-hit:hover {
    background-color: transparent;
  }
  .chat-item.active,
  .chat-item.active:hover { box-shadow: none; }

  /* Поля колонки на телефоне уже: место в верхней строке нужно тумблерам со
     счётчиками, а не отступам. Строка поиска идёт по тому же краю — её
     собственные 4 точки под кольцо фокуса входят в этот же отступ. */
  .side-head { padding-inline: var(--sp-3); }

  /* Тумблеры мессенджеров — своей строкой под заголовком. Втроём со счётчиками
     они съедали «Чаты» до многоточия, а четвёртый мессенджер не поместился бы
     вовсе. Строка ездит по горизонтали: сжимать тумблеры нельзя — «99+» обязан
     остаться читаемым.

     order отправляет группу в конец, ширина в 100% переносит её на свою
     строку — заголовок и кнопка меню остаются в первой. Полосу прокрутки
     прячем: на телефоне её и так не видно, а на узком окне настольного
     браузера она съела бы высоту строки. */
  .side-head { flex-wrap: wrap; row-gap: var(--sp-2); }
  .head-transports {
    order: 1;
    flex: 1 0 100%;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .head-transports::-webkit-scrollbar { display: none; }
  .side-search { margin-inline: var(--sp-2); }
  .feed { padding-inline: var(--sp-3); }
  .msg { max-width: 88%; }

  /* Плотность списка и ленты на телефоне — как в привычных мессенджерах:
     кружок крупнее, название и превью на ступень выше по шкале, тело
     сообщения — тоже. Органы управления не растут: шапка, поиск и фильтры
     остаются компактными, иначе весь выигрыш уйдёт им под отступы. Заглушка
     строки повторяет новый размер кружка — иначе список подпрыгнет, когда
     данные придут. */
  .chat-item .chat-avatar,
  .skeleton-row .skeleton.circle {
    width: var(--size-avatar-touch);
    height: var(--size-avatar-touch);
  }
  .chat-item .chat-avatar { font-size: var(--fs-lg); }
  .chat-title { font-size: var(--fs-lg); }
  .chat-preview { font-size: var(--fs-md); }
  .bubble { font-size: var(--fs-lg); }

  /* Окно аккаунтов на телефоне узкое, но строка аккаунта остаётся одной:
     состояние переехало на сам кружок мессенджера, а кнопки стали компактнее,
     и место нашлось. Тесно тут только названию — оно и обрезается. */
  .acc .row-title { font-size: var(--fs-sm); }

  /* Поля на телефоне крупнее: Safari на iOS зумит страницу при фокусе в поле
     мельче 16px, и после этого каркас уезжает — шапка лезет под системные
     часы, правый край строк уходит за экран, нижняя панель повисает
     посередине. Мета-тегом это не лечится: user-scalable=no Safari
     игнорирует. Размер — токен темы, здесь только условие «узкий экран»,
     чтобы порог экрана жил в одном месте с остальной адаптацией.
     Перечислены и голые поля, и поля внутри .field: у правил темы
     специфичность выше, одним `input` их не перебить. */
  input,
  textarea,
  select,
  .field input,
  .field textarea,
  .field-input,
  input.field,
  textarea.field { font-size: var(--fs-field-touch); }
}

/* ======================================================================
   Меньше движения
   ====================================================================== */

@media (prefers-reduced-motion: reduce) {
  /* Ленты здесь больше нет: плавность она не объявляет вовсе, а те два места,
     где движение осмысленно, спрашивают о просьбе сами (calmMotion в app.js) —
     иначе браузер сгладил бы то, что должно быть мгновенным. */
  .chat-item { transition: none; }
}
