/* ═════ Sentos · Фото, видео, файлы и голосовые в ленте ═════
   Разметку рисует chat.js, и она немного другая, чем в макете: медиа
   лежит отдельным блоком внутри пузыря, а не заменяет его собой. Чтобы
   вид совпал, пузырю ставится отметка (is-media, is-voice, is-file,
   is-album), и лишние отступы с фоном снимаются здесь.

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

/* ─────────────── Пузырь под вложение ─────────────── */

/* Одно вложение без подписи: пузырь и есть само вложение. Отступ
   в 4 px — рамка вокруг снимка, как в макете. */
/* У снимка нет пузыря: форма сообщения — сам снимок. Рамка в 4 px
   здесь была, и вместе с ней вокруг фотографии шла оранжевая кайма —
   на тёмной ленте она читалась как лишняя обводка. Время и галочки
   лежат плашкой поверх картинки, подложка у них своя.

   Ширину пузырю задаёт sizeMediaFrames по пропорциям снимка:
   портретные показываются уже, горизонтальные шире. */
.message.is-media,
.message.is-album {
  padding: 0;
  background: none;
}

/* Картинка занимает кадр целиком. Кадр знает размеры от сервера, а
   картинка приходит своими — расходились они на десяток пикселей, и
   снизу оставалась полоса фона.

   Обрезка обязательна: пока картинка не подстроилась под кадр, она на
   пару кадров шире и выше него и вылезала за скруглённый край. */
.media-frame {
  overflow: hidden;
  border-radius: 18px;
}

/* Прижимаем к краям кадра, а не задаём 100% высоты: у картинки своя
   высота из пропорций, и процент она берёт не всегда — заполнить надо
   наверняка. Правило работает только там, где кадр знает свои размеры
   (их ставит sizeMediaFrames по data-атрибутам); без них кадр схлопнулся
   бы в ноль. */
.media-frame[data-media-width] .media-load-shell,
.media-frame[data-media-width] .media-asset {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.message.is-file,
.message.is-voice { padding: 6px 10px 6px 6px; }

/* Внутренний блок больше не пузырь: свои отступы и ширину он отдаёт
   тому, в ком лежит. */
.message.is-media .media-message,
.message.is-media .video-message,
.message.is-album .media-album,
.message.is-voice .voice-message,
.message.is-file .file-message {
  padding: 0;
  max-width: none;
  background: none;
}

/* ─────────────── Фото и видео ─────────────── */

.media-message,
.video-message {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 18px;
}

.media-open,
.media-preview-action,
.media-album-open {
  display: block;
  padding: 0;
  border: 0;
  background: none;
}

.media-open { width: 100%; }

.media-asset {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 18px;
  object-fit: cover;
  opacity: 0;
  transition: opacity 220ms ease;
}

/* Картинка проявляется, когда действительно загрузилась: класс ставит
   chat.js по событию load. Иначе на месте фотографии моргает пустота. */
.media-asset.is-loaded,
.media-asset.loaded { opacity: 1; }

.media-load-shell {
  position: relative;
  display: block;
  min-height: 60px;
  border-radius: 18px;
  background: var(--field-strong);
}

/* Пока грузится — тональная заливка с мягким переливом. Не спиннер:
   на своём месте картинка появится сама, а крутящееся колесо посреди
   ленты только дёргает глаз. */
.media-loader {
  position: absolute;
  inset: 0;
  border-radius: 18px;
  background: linear-gradient(100deg,
    var(--field-strong) 20%, var(--field) 44%, var(--field-strong) 68%);
  background-size: 220% 100%;
  animation: mediaShimmer 1.4s ease-in-out infinite;
}

.media-load-shell.loaded .media-loader {
  opacity: 0;
  animation: none;
  transition: opacity 220ms ease;
}

/* ── Отправленное, пока сервер его обрабатывает ──

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

   Размытие снимается переходом, когда придёт картинка с сервера
   (класс убирает bindMediaAssets), поэтому подмена не бьёт по глазам. */
.media-frame .media-asset {
  transition: opacity 220ms ease, filter 420ms ease, transform 420ms ease;
}

.media-frame.is-processing .media-asset {
  filter: blur(14px);
  /* Небольшое увеличение прячет полупрозрачную кайму, которую размытие
     оставляет по краю кадра. */
  transform: scale(1.06);
}

/* Колечко поверх размытой картинки: видно, что работа идёт. */
.media-wait {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: media-wait-spin 720ms linear infinite;
  pointer-events: none;
}

.media-frame:not(.is-processing) .media-wait {
  opacity: 0;
  animation: none;
  transition: opacity 240ms ease;
}

@keyframes media-wait-spin {
  to { transform: rotate(360deg); }
}

/* Картинка не пришла вовсе: заливка остаётся, но перестаёт переливаться —
   иначе она обещает загрузку, которой не будет. */
.media-load-shell.load-error .media-loader { animation: none; }

@keyframes mediaShimmer { 50% { background-position: 100% 0; } }

/* Размеры кадра знает сервер, и chat.js передаёт их в data-атрибутах:
   до загрузки место под фотографию уже занято, и лента не прыгает. */
.media-frame { max-width: 100%; }

.media-download,
.media-preview-action {
  position: absolute;
  top: 8px;
  right: 8px;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgba(22, 22, 24, 0.5);
  color: #fff;
  backdrop-filter: blur(14px) saturate(140%);
}

.media-preview-action { right: 44px; }

.media-download .icon,
.media-preview-action .icon { width: 16px; height: 16px; }

.media-download.is-saving,
.viewer-action.is-saving { opacity: 0.58; pointer-events: none; }

/* Видео управляется своими кнопками браузера: своих поверх не рисуем,
   иначе их будет две. */
.video-message video {
  display: block;
  width: 100%;
  border-radius: 18px;
  background: #000;
}

/* ─────────────── Несколько вложений разом ─────────────── */

.media-album {
  display: grid;
  gap: 3px;
  grid-template-columns: repeat(2, 1fr);
  border-radius: 18px;
  overflow: hidden;
}

.media-album.album-count-1 { grid-template-columns: 1fr; }
.media-album.album-count-3 .media-album-item:first-child,
.media-album.album-count-5 .media-album-item:first-child { grid-column: span 2; }

.media-album-item {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--field-strong);
}

.media-album-item img,
.media-album-item video {
  width: 100%;
  height: 100%;
  border-radius: 0;
  object-fit: cover;
}

.media-album-open { width: 100%; height: 100%; }

/* Метка видео в альбоме: без неё кадр не отличить от фотографии. */
.media-album-video {
  position: absolute;
  right: 6px;
  bottom: 6px;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(22, 22, 24, 0.5);
  color: #fff;
}

.media-album-video .icon { width: 14px; height: 14px; }

/* Подпись под вложением: тот же текст, что в обычном пузыре. */
.media-caption {
  padding: 6px 6px 2px;
  font-size: 15px;
  line-height: 1.35;
}

.attachment-stack {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* ─────────────── Файл ─────────────── */

.file-message {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 180px;
  color: inherit;
  text-decoration: none;
}

.file-icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 14px;
  background: var(--accent-fill);
  color: var(--accent-fill-text);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4px;
}

.outgoing .file-icon {
  background: rgba(255, 255, 255, 0.18);
  color: inherit;
}

.file-message span:not(.file-icon):not(.file-download) {
  display: flex;
  min-width: 0;
  flex-direction: column;
}

.file-message strong {
  overflow: hidden;
  font-size: 13px;
  font-weight: 500;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-message small {
  font-size: 11px;
  opacity: 0.7;
}

.file-download {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex: none;
  opacity: 0.7;
}

.file-download .icon { width: 17px; height: 17px; }

/* ─────────────── Голосовое в ленте ───────────────
   Круглая кнопка, дорожка и время — одной строкой, как в макете.
   Дорожка настоящая: полоски рисуются по звуковой волне, которую
   посчитал сервер, поэтому у каждой записи она своя. */

.voice-message.voice-player {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Кнопка и дорожка — кнопки по разметке, поэтому снимаем с них всё,
   что общее правило кнопок им добавляет. */
.voice-play,
.voice-wave {
  padding: 0;
  border: 0;
  background: none;
}

.voice-play {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  background: var(--accent-fill);
  color: var(--accent-fill-text);
}

.outgoing .voice-play {
  background: rgba(255, 255, 255, 0.2);
  color: inherit;
}

.voice-play.is-pressed:not(:disabled) { transform: scale(0.9); }
.voice-play .icon { width: 15px; height: 15px; }

/* Дорожка занимает оставшееся место и умеет ужиматься: без min-width: 0
   полоски не дают ей сжаться, и она наезжает на время. */
.voice-wave {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: none;
  height: 18px;
  cursor: pointer;
}

.voice-wave i {
  width: 2.5px;
  height: 100%;
  flex: none;
  border-radius: 2px;
  background: currentColor;
  opacity: 0.4;
  transition: opacity 140ms ease;
}

/* Прослушанная часть заметнее непрослушанной — по ней видно, докуда
   дошли. Больше они ничем не отличаются. */
.voice-wave i.is-played { opacity: 1; }

/* Время одно: в покое — длительность записи, во время прослушивания —
   сколько уже прошло. Два числа разом человеку не нужны. */
.voice-time {
  flex: none;
  font-size: 12px;
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}

.voice-elapsed { display: none; }
.voice-player.is-playing .voice-elapsed { display: inline; }
.voice-player.is-playing .voice-duration { display: none; }

/* ─────────────── Пока вложение едет на сервер ───────────────
   Человек должен видеть, что происходит: «загружаем» — файл ещё идёт,
   «оптимизируем» — сервер жмёт видео или считает волну голосового. */

.media-processing,
.media-file-processing,
.voice-processing,
.media-failed {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 190px;
  min-height: 44px;
  padding: 8px 10px;
  border-radius: 18px;
  background: var(--field);
  color: var(--text-soft);
  font-size: 12px;
}

.outgoing .media-processing,
.outgoing .media-file-processing,
.outgoing .voice-processing,
.outgoing .media-failed {
  background: rgba(255, 255, 255, 0.14);
  color: inherit;
}

.media-processing { flex-direction: column; justify-content: center; }
.media-processing strong { font-size: 13px; font-weight: 500; }
.media-processing small,
.media-file-processing small { opacity: 0.7; }

.media-local-preview {
  width: 100%;
  border-radius: 16px;
  opacity: 0.5;
}

/* Вращается только то, что действительно ждёт: колечко у файла,
   который ещё едет. */
.media-spinner,
.voice-processing-spinner {
  width: 18px;
  height: 18px;
  flex: none;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  opacity: 0.5;
  animation: mediaSpin 900ms linear infinite;
}

@keyframes mediaSpin { to { transform: rotate(360deg); } }

.media-album-item.is-processing,
.media-album-item.is-failed {
  display: grid;
  place-items: center;
  gap: 4px;
  color: var(--muted);
  font-size: 11px;
}

.media-failed { color: var(--danger); }
.media-failed .icon { width: 18px; height: 18px; }
.media-failed strong { display: block; font-size: 13px; font-weight: 500; }
.media-failed small { opacity: 0.75; }

/* Не отправилось: сообщение остаётся в ленте приглушённым, нажатие
   повторяет отправку. Убирать его нельзя — человек решит, что оно ушло. */
.message-row.failed .message { opacity: 0.65; }

/* ─────────────── Просмотр фотографии и видео ───────────────
   Всегда тёмный: под снимком светлая тема не имеет смысла. */

.media-viewer {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  flex-direction: column;
  background: rgba(9, 9, 11, 0.94);
  color: #fff;
  animation: viewerIn 200ms ease;
}

@keyframes viewerIn { from { opacity: 0; } }

.media-viewer.is-closing {
  animation: viewerOut 200ms ease forwards;
  pointer-events: none;
}

@keyframes viewerOut { to { opacity: 0; } }

.media-viewer-scrim {
  position: absolute;
  inset: 0;
  border: 0;
  background: none;
}

.media-viewer-bar {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: calc(12px + env(safe-area-inset-top)) 16px 12px;
  font-size: 15px;
}

.media-viewer-actions { display: flex; gap: 6px; }

.viewer-action {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.media-viewer-stage {
  position: relative;
  z-index: 1;
  display: flex;
  flex: 1;
  min-height: 0;
  align-items: center;
  justify-content: center;
  padding: 0 12px calc(16px + env(safe-area-inset-bottom));
  touch-action: none;
}

.viewer-load-shell {
  display: flex;
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  align-items: center;
  justify-content: center;
}

/* Кадр в просмотрщике считается от пропорций вложения, а не от того,
   что уже загружено: пока едет полный файл, на месте стоит миниатюра, и
   по её четырёмстам точкам кадр раскладывался маленьким, а потом прыгал
   до настоящего размера. Ширину по этим пропорциям ставит openMediaViewer
   (стилем, иначе её перебивает width: 100% у .media-asset), а потолок
   высоты держит переменная. */
.media-viewer { --viewer-max-h: calc(100dvh - 130px); }

/* Оболочка тянется на всю сцену: проценты в ширине снимка должны
   считаться от экрана, а не от него самого. Заливка и высота от неё при
   этом не нужны: в ленте серый прямоугольник держит место под картинку,
   а на весь экран он расплывался серым полем вокруг снимка. */
.media-viewer-stage .viewer-load-shell {
  width: 100%;
  min-height: 0;
  background: none;
}

.viewer-media {
  max-width: 100%;
  max-height: var(--viewer-max-h);
  border-radius: 18px;
  object-fit: contain;
  opacity: 0;
  transition: opacity 220ms ease, transform 220ms var(--ease);
}

.viewer-media.is-loaded,
.viewer-media.loaded { opacity: 1; }

/* Видео с заставкой открывается сразу: заставка уже лежит в сообщении,
   кадры дойдут следом, а колечко покрутится поверх неё. */
.viewer-load-shell.has-poster .viewer-media { opacity: 1; }

.viewer-loader {
  position: absolute;
  /* Колечко ставим в середину кадра. У .media-loader, от которого оно
     наследует вид, стоит inset: 0 — с собственными размерами это
     прижимало его к левому верхнему углу, и на весь тёмный экран
     крутилось маленькое кольцо где-то сбоку. */
  inset: 50% auto auto 50%;
  /* Смещаем отступами, а не transform: вращение задаёт свой transform и
     стирает сдвиг — колечко уезжало вбок и вместо вращения на месте
     медленно ползло по кругу большого радиуса. */
  margin: -17px 0 0 -17px;
  width: 34px;
  height: 34px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-right-color: transparent;
  border-radius: 50%;
  background: none;
  animation: mediaSpin 900ms linear infinite;
}

/* Снимок в просмотре открывается сразу: показывается та же картинка,
   что уже стоит в переписке, а полная подменяет её молча, когда
   распечатается. Ждать пустой экран не приходится, и колечко в этом
   случае не нужно вовсе. */
.media-load-shell.has-preview .viewer-loader {
  display: none;
}

/* Приближение щипком: пока картинку тянут, переход мешает — она должна
   идти ровно за пальцем. */
.media-viewer-stage.is-dragging .viewer-media { transition: none; }
.media-viewer-stage.is-zoomed .viewer-media { cursor: grab; }

/* ─────────────── Разделы вложений в карточке ─────────────── */

.shared-item-copy {
  display: flex;
  min-width: 0;
  flex: 1;
  flex-direction: column;
  gap: 1px;
}

.shared-item-copy strong {
  overflow: hidden;
  font-size: 13px;
  font-weight: 500;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shared-item-copy small {
  color: var(--muted);
  font-size: 11px;
}

.shared-link-thumb {
  width: 42px;
  height: 42px;
  flex: none;
  border-radius: 12px;
  object-fit: cover;
}

/* Видео в сетке медиа: без метки кадр не отличить от фотографии. */
/* Значок «это видео» стоит по центру плитки, а не жмётся в угол:
   в углу он читался как служебная пометка, а не как «нажми и посмотри».
   Размер — под палец. */
.shared-video-mark {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 50%;
  background: rgba(22, 22, 24, 0.42);
  color: #fff;
  pointer-events: none;
}

.shared-video-mark .icon {
  width: 20px;
  height: 20px;
  /* Треугольник зрительно легче круга: сдвигаем на волосок вправо,
     иначе кажется смещённым влево. */
  margin-left: 2px;
  fill: currentColor;
}

/* Голосовое, пока сервер считает по нему звуковую волну. */
.voice-processing-icon {
  position: relative;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  background: var(--field-strong);
  color: var(--muted);
}

.voice-processing-wave { opacity: 0.4; }
.voice-time-separator { opacity: 0.6; }

/* ─────────────── Фотография с подписью ───────────────

   Снимок занимает всю ширину пузыря и сливается с его верхом, а текст
   лежит под ним, внутри того же пузыря. Раньше пузырь оставлял вокруг
   снимка свои отступы, но ширину брал ровно по снимку — и правый край
   картинки уезжал за пузырь, а сообщение выглядело собранным из двух
   разных вещей. */

.message.is-captioned {
  padding: 3px;
  overflow: hidden;
  /* Ширину пузырю задаёт сам снимок, как и у медиа без подписи. Общее
     ограничение в 78% ленты здесь мешало: пузырь ужимался, а кадр
     оставался прежним и вылезал за него справа. */
  max-width: none;
}

.message.is-captioned .attachment-stack {
  gap: 0;
}

/* Снимок лежит внутри пузыря со своим скруглением — он не встык с
   краями, а вложен: у пузыря скругление больше, у снимка меньше ровно
   на толщину рамки, поэтому углы идут друг за другом. */
.message.is-captioned .media-frame,
.message.is-captioned .media-frame img,
.message.is-captioned .media-frame video {
  border-radius: 15px;
}

/* Текст и время получают отступы, которых лишился сам пузырь. */
.message.is-captioned .media-caption {
  padding: 6px 8px 0;
}

.message.is-captioned .message-meta {
  padding: 0 8px 3px;
}
