/* Тема мессенджера: плоская, по образцу WhatsApp.
   Единственный источник правды по оформлению. Экраны собираются из токенов и
   служебных классов этого файла; styles.css отвечает только за раскладку.

   Решение владельца продукта 17.09.2026: берём привычные людям приёмы
   WhatsApp — зелёный акцент, плоские поверхности, волосяная линия вместо
   объёма, мелкие радиусы. Неоморфизм, на котором тема стояла до этого, ушёл,
   а вот ИМЕНА токенов --neo-* остались: их знает весь слой компонентов и
   styles.css, и переопределить семь значений дешевле и обратимее, чем
   переписать двести правил. Читать их теперь так: «out» — приподнятое, то
   есть обведённое волосяной линией; «in» — утопленное, то есть заполненное.

   Три правила, на которых держится вся картинка:
   1. Поверхность отличается ЗАЛИВКОЙ, а не тенью: полотно темнее панели,
      панель темнее пузыря входящего. Настоящая тень осталась только под тем,
      что реально висит над экраном — окно, меню, всплывающая подсказка.
   2. Границу рисует волосяная линия в один пиксель. Нажатое и выбранное
      отвечают сменой заливки, а не рельефом.
   3. Зелёный акцент — только на одном элементе экрана за раз (главное
      действие, счётчик непрочитанного). Пузырь исходящего — СВОЙ тёмный
      зелёный, не акцентный: акцентным он превращает ленту в зелёное полотно,
      на котором не читается текст.

   Палитра лежит одним куском в начале :root, структура (отступы, размеры,
   типографика, длительности) — ниже, и от палитры она не зависит. Разделение
   нужно для второй темы: светлая переопределит только палитру.

   На чужие адреса приложение не ходит: шрифты системные, иконки — инлайновый
   SVG. Единственные свои файлы в теме — две картинки фона полотна; они лежат
   рядом с оболочкой и попадают в кэш service worker. */

:root {
  color-scheme: dark;

  /* --- палитра: фон и поверхности ------------------------------------- */
  --c-bg: #0b141a;             /* полотно переписки, самый тёмный слой */
  --c-bg-deep: #0b141a;        /* градиента у фона больше нет: оба края равны фону */
  --c-bg-lift: #0b141a;
  --c-surface: #111b21;        /* панель: список переписок, шапка, композер */
  --c-chip: #0b141a;           /* пилюля фильтра и свёрнутый поиск */
  --c-surface-raised: #202c33; /* пузырь входящего, строка под курсором */
  --c-surface-sunken: #2a3942; /* заливка поля ввода и поиска */
  --c-hairline: rgba(255, 255, 255, .08); /* разделитель строк и границы, 1px */
  --c-overlay: rgba(11, 20, 26, .72);     /* подложка модального окна */
  /* Стекло: панель, сквозь которую видно ленту. Не сплошная --c-surface —
     под ней должно читаться, что переписка продолжается, а размывает её
     backdrop-filter. */
  --c-glass: rgba(17, 27, 33, .72);

  /* --- палитра: текст (ровно три уровня, четвёртого не заводить) -------- */
  --c-text: #e9edef;    /* заголовки, имена, тело сообщения */
  --c-text-2: #8696a0;  /* подписи, превью переписки, подсказки */
  --c-text-3: #76858e;  /* время, служебные метки, disabled-подпись */

  /* --- палитра: акцент -------------------------------------------------- */
  --c-accent: #00a884;      /* сплошной зелёный: заливка, рамки, индикаторы */
  --c-accent-hi: #00a884;   /* градиента у акцента нет: оба края равны */
  --c-accent-lo: #00a884;
  --c-accent-text: #00a884; /* акцент как ТЕКСТ или иконка на тёмном фоне */
  --c-on-accent: #e9edef;   /* глиф и текст поверх акцентной заливки */
  --c-accent-deep: #00705a; /* акцент как текст на БЕЛОЙ подложке: цифра счётчика
                               на включённом фильтре */
  --c-accent-soft: rgba(0, 168, 132, .16); /* подсветка активной строки */

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

     Сам пузырь тоже не акцентный: акцентом помечают ОДНО действие на экране,
     а пузырями заполнена половина ленты, и на ярком зелёном текст не
     читается. Оттенок тот же, светлота вдвое ниже. */
  --c-bubble-out: #005c4b;       /* сам пузырь */
  --c-tick-read: #25d366;        /* галочки «прочитано» */
  /* Вложенная плашка ВНУТРИ пузыря: цитата, реакция, опрос, служебное
     вложение, карточка ссылки, блок кода. Темнее самого пузыря — «утопленное
     заполняют». Раньше её красил --c-accent-lo: у алого акцента тёмный край
     был ТЕМНЕЕ пузыря и годился подложкой, у зелёного он пузыря светлее, и
     все эти плашки засветились. */
  --c-bubble-out-panel: #024c3e;
  --c-on-bubble-out: #e9edef;    /* текст и значки на пузыре */
  --c-on-bubble-out-2: rgba(233, 237, 239, .62); /* время, галочки, подписи */
  /* Осветление поверх пузыря: своя реакция. Линией её не метим — линия на
     зелёном читается как обводка. */
  --c-on-bubble-out-fill: rgba(255, 255, 255, .18);

  /* --- палитра: служебное ------------------------------------------------ */
  --c-row-hover: rgba(255, 255, 255, .04); /* подсветка строки под курсором */
  --c-row-press: rgba(255, 255, 255, .09); /* та же подсветка под нажатием */
  /* Рисунок за лентой. Своя картинка на каждую тему, а не одна под маской:
     маска — это лишний слой поверх прокручиваемой ленты, и на телефоне он
     стоил заметных рывков. Здесь обычная фоновая картинка, самый быстрый
     путь отрисовки; цена — два файла вместо одного токена цвета.
     Оба собирает tools/make-wallpaper.py, руками не правим. */
  --wallpaper: url(/wallpaper-dark.png);
  --c-scrollbar: #374248;                  /* ползунок полосы прокрутки */
  /* Чистый белый, одинаковый в любой теме: белое поле под кодом входа. Часть
     сканеров не берёт код с тонированной подложки, поэтому здесь нужен именно
     #fff, а не «самая светлая поверхность темы». */
  --c-white: #ffffff;

  /* --- тона аватара -----------------------------------------------------
     Шесть приглушённых оттенков вместо фотографии: сервер картинок почти
     никогда не отдаёт, а одноцветный список нечитаем. Все шесть — тёмные и
     низкой насыщенности, чтобы алый акцент оставался единственным ярким
     пятном экрана, и все держат светлый текст инициалов.
     Номер оттенка выбирает разметка по идентификатору переписки. */
  --grad-tone-0: linear-gradient(145deg, #8f4a51 0%, #6b3239 100%); /* роза */
  --grad-tone-1: linear-gradient(145deg, #8a5c3c 0%, #653f27 100%); /* глина */
  --grad-tone-2: linear-gradient(145deg, #5b6d45 0%, #3f4d2f 100%); /* мох */
  --grad-tone-3: linear-gradient(145deg, #3d6b66 0%, #294b47 100%); /* морская волна */
  --grad-tone-4: linear-gradient(145deg, #4b5a85 0%, #333e5e 100%); /* индиго */
  --grad-tone-5: linear-gradient(145deg, #6a4573 0%, #4a2f51 100%); /* слива */

  /* --- тона подписи автора ----------------------------------------------
     Те же шесть оттенков, но как ТЕКСТ на пузыре входящего: заливка аватара
     тёмная, и имя, набранное ею, на пузыре не читалось бы вовсе. Оттенок и
     номер те же, светлота поднята до 5.5:1 на `--c-surface-raised` и 6.3:1 на
     `--c-bg` (порог основного текста — 4.5). Насыщенность держим низкой: имя
     в группе — подпись, а не второй акцент.
     Номер выбирает разметка по идентификатору автора. */
  --c-author-0: #d1929b; /* роза */
  --c-author-1: #c9a077; /* глина */
  --c-author-2: #a3b781; /* мох */
  --c-author-3: #7bb8b1; /* морская волна */
  --c-author-4: #97a6cf; /* индиго */
  --c-author-5: #b596c4; /* слива */

  /* --- фирменные цвета мессенджеров --------------------------------------
     Ими помечаем принадлежность переписки: кольцо вокруг аватара, подпись в
     его углу и обводка включённого таба фильтра. Заливкой не бывают — линией
     и мелким текстом:
     подобраны под тёмный фон так, чтобы оттенок узнавался, но алый акцент
     оставался единственным ярким пятном экрана.
     Telegram — от голубого к синему, MAX — от светло-голубого к фиолетовому.
     С зелёным акцентом эти два не спорят, поэтому здесь не тронуты. */
  --c-brand-tg: #4b93c9;
  --c-brand-max: #7a5cc4;
  /* Тёмный край фирменного градиента. Им красится цифра на белом счётчике,
     который лежит поверх залитого тумблера: сам --c-brand для этого светловат
     и на белом теряется. */
  --c-brand-tg-deep: #2f68b0;
  --c-brand-max-deep: #6b3fb5;
  --grad-brand-tg: linear-gradient(135deg, #59a6d8 0%, #2f68b0 100%);
  --grad-brand-max: linear-gradient(135deg, #63b6dd 0%, #7a4fc0 100%);
  /* WhatsApp — единственный, чей фирменный цвет спорит с акцентом: зелёный
     против зелёного. Поэтому взят ЕГО зелёный, яркий и желтее нашего, а
     различает мессенджеры в первую очередь подпись «WA» в углу кружка —
     кольцо тут только поддерживает. Градиент уводим в их же тёмный бирюзовый:
     так пятно не сливается с акцентной кнопкой, которая ровного тона. */
  --c-brand-wa: #25d366;
  --c-brand-wa-deep: #0f7a6e;
  --grad-brand-wa: linear-gradient(135deg, #4ae07f 0%, #128c7e 100%);
  /* Толщина фирменного кольца: тоньше не читается на телефоне, толще спорит
     с акцентом и съедает место под инициалами. */
  --ring-brand-w: 2px;

  /* Неоновая подпись мессенджера в углу кружка: яркий оттенок под текст и
     свечение вокруг него. Тона отдельные от --c-brand-*: те подобраны под
     тонкую линию кольца и в мелкой подписи поверх фотографии тонут. Свечение
     держим в фирменном цвете, а НЕ в алом: алый остаётся единственным
     акцентом экрана, а голубой с фиолетовым с ним не спорят. */
  --c-brand-tg-neon: #6ec7ff;
  --c-brand-max-neon: #b892ff;
  --c-brand-wa-neon: #5ef2a0;
  /* Свечение в два слоя: плотное ядро вплотную к буквам и мягкий ореол вокруг.
     Одним слоем неона не выходит — получается просто размытая тень. */
  --glow-brand-tg: 0 0 4px rgba(110, 199, 255, .9), 0 0 10px rgba(110, 199, 255, .45);
  --glow-brand-max: 0 0 4px rgba(184, 146, 255, .9), 0 0 10px rgba(184, 146, 255, .45);
  --glow-brand-wa: 0 0 4px rgba(94, 242, 160, .9), 0 0 10px rgba(94, 242, 160, .45);
  /* Мельче любого текста интерфейса: подпись в две-три буквы читается и такой,
     а крупнее закрыла бы собой кружок. */
  --fs-tag: 9px;

  /* --- палитра: состояния ---------------------------------------------- */
  --c-ok: #4ed09a;
  --c-ok-soft: rgba(78, 208, 154, .14);
  --c-warn: #f2b33d;
  --c-warn-soft: rgba(242, 179, 61, .14);
  --c-error: #ff7a7e;
  --c-error-soft: rgba(255, 122, 126, .14);

  /* --- палитра: виды файлов ----------------------------------------------
     Цвет бейджика на плашке файла. Отдельные токены, а НЕ цвета состояний:
     красный у PDF означает вид документа, а не ошибку. Смешать эти два смысла
     — значит на первой же правке состояний перекрасить половину вложений.
     Цвета взяты привычные по офисным пакетам: их узнают, не читая букв. */
  --c-file-pdf:  #e5484d;
  --c-file-doc:  #4a8cf7;
  --c-file-xls:  #2fa96b;
  --c-file-ppt:  #e8833a;
  --c-file-zip:  #c9a227;
  --c-file-txt:  #7c8598;
  --c-file-data: #7c6cf0;
  --c-file-app:  #3aa0a8;
  --c-file-book: #b4655f;
  --c-file-art:  #c4589e;
  --c-file-other: #6b7280;
  --c-file-on: #fff;
  /* Лист значка файла и его загнутый угол. Бумага белая в обеих палитрах:
     значок документа тем и узнаётся, что он похож на лист, а не на плитку
     темы, — поэтому у этой пары переопределения светлой палитрой нет. */
  --c-file-paper: #fff;
  --c-file-fold: rgba(0, 0, 0, 0.16);

  /* --- градиенты -------------------------------------------------------- */
  /* Заливки остались ГРАДИЕНТАМИ с равными краями, а не превратились в `none`:
     компоненты красятся через `background-image`, и `none` показал бы сквозь
     себя `background` — у панелей это цвет полотна, то есть панель исчезла бы.
     Равные края дают ту же плоскую заливку, не требуя правок в компонентах. */
  --grad-bg: none;
  --grad-surface: linear-gradient(0deg, var(--c-surface) 0%, var(--c-surface) 100%);
  --grad-surface-soft: linear-gradient(0deg, var(--c-surface-raised) 0%, var(--c-surface-raised) 100%);
  --grad-accent: linear-gradient(180deg, var(--c-accent-hi) 0%, var(--c-accent-lo) 100%);
  --grad-accent-hover: linear-gradient(180deg, #06cf9c 0%, #06cf9c 100%);
  --grad-bubble-out: linear-gradient(0deg, var(--c-bubble-out) 0%, var(--c-bubble-out) 100%);
  --grad-skeleton: linear-gradient(100deg, #1b262c 20%, #26333a 45%, #1b262c 70%);
  /* Та же заглушка, но внутри своего пузыря: он зелёный, и тёмный градиент на
     нём виден пятном без движения. Здесь то же переливание, только светом. */
  --c-skeleton-on-bubble-out: rgba(255, 255, 255, .18);
  --grad-skeleton-on-bubble-out: linear-gradient(
    100deg,
    rgba(255, 255, 255, .12) 20%,
    rgba(255, 255, 255, .34) 45%,
    rgba(255, 255, 255, .12) 70%
  );

  /* --- объём: волосяная линия вместо пары теней -------------------------
     Имена --neo-* достались от прежней темы — их знает весь слой компонентов
     и styles.css, поэтому переопределяем значения, а не переписываем правила.
     «out» — приподнятое, обведено линией; «in» — утопленное, обведено линией
     потемнее; «press» — нажатое. Настоящая тень осталась у --neo-out-lg и
     --shadow-modal: они под тем, что реально висит над экраном. */
  --shadow-dark: rgba(0, 0, 0, .28);

  --neo-out: 0 0 0 1px var(--c-hairline);
  --neo-out-sm: 0 0 0 1px var(--c-hairline);
  --neo-out-lg: 0 8px 24px rgba(0, 0, 0, .45), 0 0 0 1px var(--c-hairline);
  --neo-in: inset 0 0 0 1px var(--c-hairline);
  --neo-in-sm: inset 0 0 0 1px var(--c-hairline);
  --neo-press: inset 0 0 0 1px var(--shadow-dark);
  --neo-flat: 0 0 0 0 rgba(0, 0, 0, 0);

  /* Тень под зелёной заливкой: у WhatsApp её нет вовсе, оставлена волосяная
     подложка — иначе кнопка на панели того же тона теряет край. */
  --shadow-accent: 0 1px 2px rgba(0, 0, 0, .28);
  --shadow-bubble: 0 1px 1px rgba(0, 0, 0, .3);
  --shadow-modal: 0 16px 40px rgba(0, 0, 0, .55);

  /* Кольца-подсветки. Тень без размытия, поверх неоморфных: ими помечаем
     поле в фокусе, поле с ошибкой, неотправленный пузырь и точку «в сети». */
  --ring-accent: 0 0 0 2px var(--c-accent-soft);
  --ring-error: 0 0 0 2px var(--c-error-soft);
  --ring-failed: 0 0 0 1px var(--c-error);
  --ring-dot: 0 0 0 3px var(--c-bg);
  /* Кольцо фокуса: outline для обычных случаев, box-shadow-версия — для
     элементов внутри overflow:hidden, где outline обрезается. */
  --focus-ring: 2px solid var(--c-accent-text);
  --focus-offset: 2px;
  --shadow-focus: 0 0 0 2px var(--c-bg), 0 0 0 4px var(--c-accent-text);

  /* --- радиусы ---------------------------------------------------------- */
  --r-circle: 50%;
  --r-pill: 999px;
  --r-bubble: 8px;
  --size-bubble-tail: 8px; /* вылет хвостика у первого пузыря серии */
  --r-card: 12px;
  --r-md: 8px;
  --r-sm: 6px;

  /* --- отступы ---------------------------------------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 40px;
  --sp-9: 56px;

  /* --- типографика ------------------------------------------------------ */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;

  --fs-xs: 11px;   /* счётчик, служебная метка */
  --fs-sm: 13px;   /* превью, время, подписи */
  --fs-md: 15px;   /* тело: сообщения, поля ввода */
  --fs-lg: 17px;   /* имя в шапке переписки */
  --fs-xl: 22px;   /* заголовок экрана */
  --fs-2xl: 28px;  /* заголовок пустого состояния/входа */

  /* Поле ввода на телефоне. Safari на iOS сам зумит страницу, когда фокус
     попадает в поле мельче 16px, и после зума каркас уезжает: шапка лезет под
     системные часы, правый край строк уходит за экран. Запретить зум мета-тегом
     нельзя — user-scalable=no Safari игнорирует. Поэтому на узком экране поля
     крупнее, чем --fs-md; применяет это styles.css в своей @media. */
  --fs-field-touch: 16px;

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  --lh-tight: 1.2;
  --lh-normal: 1.45;
  --lh-loose: 1.6;

  --tracking-wide: .02em;

  /* --- размеры повторяющихся форм --------------------------------------- */
  /* Круглая отметка выбора слева от сообщения. Меньше мелкой кнопки: это не
     орган управления, а признак состояния строки — нажимают саму строку. */
  --size-check: 24px;
  /* Высота плашки выбора: круглая кнопка плюс её поля. Ею же лента считает
     запас снизу, поэтому число живёт в одном месте. */
  --pick-bar-h: 64px;
  --size-control: 48px;     /* круглая кнопка, кнопка отправки */
  --size-control-sm: 36px;  /* мелкая круглая кнопка внутри поля */
  --size-field: 48px;       /* высота поля-пилюли */
  --size-avatar: 46px;
  --size-avatar-sm: 32px;
  --size-avatar-lg: 72px;
  /* Кружок переписки на телефоне. Крупнее настольного: на экране в 360 точек
     список — это всё, ради чего приложение открыли, и плотность у него должна
     быть та же, что у привычных мессенджеров. Строка от этого выходит в 80
     точек (кружок плюс поля строки) — ровно как там. Шапки и органов
     управления это не касается, они остаются компактными. */
  --size-avatar-touch: 56px;
  --size-badge: 22px;
  /* Кружок со знаком состояния в углу аватара: отметка «в сети», но со
     значком внутри. Крупнее простой точки — в неё должен поместиться знак и
     остаться узнаваемым, но не настолько, чтобы закрыть собой сам кружок. */
  --size-mark: 18px;

  /* Вложение в пузыре. Превью стоит в окне заранее известного размера, и
     заглушка занимает ровно то же окно: настоящих размеров картинки до её
     загрузки не знает никто (сервер их не отдаёт), а лента не имеет права
     раздвигаться под приехавшим файлом. Целиком картинку показывает просмотр
     во весь экран. Без предела лента к тому же превращалась бы в столбик
     фотографий, и переписки в ней было бы не видно. */
  --size-media-w: 280px;
  --size-media-h: 220px;
  /* Картинка карточки ссылки. Ниже превью вложения и по той же причине, по
     какой она вообще есть: карточка это подсказка о том, куда ведёт ссылка, а
     не само содержимое сообщения — занимать ленту в полный рост ей незачем.
     Ширину она берёт у превью вложения: два разных окна в одной ленте
     смотрелись бы небрежностью. */
  --size-link-shot-h: 140px;
  /* Миниатюра выбранного, но ещё не отправленного файла. Мельче превью в
     ленте на порядок: плашка над полем ввода обязана остаться одной строкой,
     иначе она съедает ленту, ради которой экран и открыт. */
  --size-attach: 56px;

  /* --- иконки (инлайновый SVG) ------------------------------------------ */
  /* Самый мелкий значок — тот, что живёт внутри кружка --size-mark: там
     16 точек упёрлись бы в его края. Больше нигде он не нужен. */
  --icon-xs: 12px;
  --icon-sm: 16px;
  --icon-md: 20px;
  --icon-lg: 24px;
  --icon-stroke: 1.75;
  --c-icon: var(--c-text-2);
  --c-icon-strong: var(--c-text);
  --c-icon-accent: var(--c-accent-text);
  --c-icon-on-accent: var(--c-on-accent);

  /* --- анимации ---------------------------------------------------------- */
  --dur-fast: 120ms;
  --dur-base: 200ms;
  --dur-slow: 320ms;
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-in-out: cubic-bezier(.4, 0, .2, 1);
  --ease-spring: cubic-bezier(.34, 1.28, .64, 1);
  /* Задержка перед показом заглушек. Данные часто приходят быстрее — тогда
     заглушки не показываются вовсе, и человек не видит мигания на пустом
     месте. Больше четверти секунды держать нельзя: ожидание без единого
     признака жизни читается как поломка. */
  --delay-skeleton: 220ms;

  /* --- слои --------------------------------------------------------------- */
  --z-head: 10;
  --z-modal: 100;
  /* Просмотр картинки — поверх окна: открывают его из ленты, а закрывать
     приходится в том же порядке, в каком открывали. */
  --z-viewer: 150;
  /* Меню действий — поверх всего, кроме тостов: открывают его из ленты и из
     списка, и накрывать его нечему. */
  --z-menu: 160;
  --z-toast: 200;
}

/* ======================================================================
   Светлая палитра
   ======================================================================

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

   Признак — атрибут на корне документа, а не `prefers-color-scheme`: тему
   выбирает человек в меню, а не его система. Ставит атрибут theme-boot.js
   ДО первой отрисовки, иначе выбравший светлую увидел бы вспышку тёмной.
   Тёмная остаётся значением по умолчанию: она в :root, и `data-theme="dark"`
   просто ничего не переопределяет.

   Цвета взяты у светлого WhatsApp: тёплое песочное полотно, белые панели и
   белый пузырь входящего, светло-зелёный пузырь исходящего. */

:root[data-theme="light"] {
  color-scheme: light;

  /* --- фон и поверхности ------------------------------------------------ */
  --c-bg: #efeae2;             /* полотно переписки: тёплый песок */
  --c-bg-deep: #efeae2;
  --c-bg-lift: #efeae2;
  --c-surface: #ffffff;        /* панель: список, шапка, композер */
  --c-chip: #ffffff;           /* пилюля фильтра: белая, а не песочная */
  --c-surface-raised: #ffffff; /* пузырь входящего */
  --c-surface-sunken: #f0f2f5; /* поле ввода, выбранная строка списка */
  --c-hairline: rgba(11, 20, 26, .1);
  --c-overlay: rgba(11, 20, 26, .45);
  /* Стекло плашки выбора: в светлой теме оно БЕЛОЕ. Прозрачные токены
     обязаны переопределяться в обеих палитрах — тёмное стекло, забытое здесь,
     висит чёрной плашкой посреди светлой переписки. */
  --c-glass: rgba(255, 255, 255, .78);

  /* --- текст ------------------------------------------------------------ */
  --c-text: #111b21;
  --c-text-2: #54656f;
  --c-text-3: #667781;

  /* --- акцент -----------------------------------------------------------
     Заливка та же зелёная, что в тёмной: кнопка и счётчик узнаются по цвету,
     и менять его от темы к теме незачем. А вот акцент КАК ТЕКСТ обязан
     потемнеть: #00a884 на белом даёт 2.6:1, для подписи этого мало. */
  --c-accent: #00a884;
  --c-accent-hi: #00a884;
  --c-accent-lo: #00a884;
  --c-accent-text: #00705a;
  --c-on-accent: #ffffff;
  --c-accent-deep: #00705a;
  --c-accent-soft: rgba(0, 168, 132, .16);

  /* --- исходящий пузырь -------------------------------------------------
     Здесь и видно, зачем пузырю свой набор: заливка светлая, и всё его
     содержимое темнеет, тогда как глиф на акцентной кнопке остаётся белым. */
  --c-bubble-out: #d9fdd3;
  --c-tick-read: #1da851;
  --c-bubble-out-panel: #c9f2c1;
  --c-on-bubble-out: #111b21;
  --c-on-bubble-out-2: rgba(17, 27, 33, .5);
  --c-on-bubble-out-fill: rgba(11, 20, 26, .14);

  /* --- служебное -------------------------------------------------------- */
  --c-row-hover: rgba(11, 20, 26, .04);
  --c-row-press: rgba(11, 20, 26, .09);
  --wallpaper: url(/wallpaper-light.png);
  --c-scrollbar: #c4cdd2;
  /* --c-white не переопределяем: он чистый белый в любой теме. */

  /* --- состояния --------------------------------------------------------
     Тёмные версии: светлые оттенки тёмной темы на белом не читаются
     (#4ed09a на белом — 1.9:1). */
  --c-ok: #0d7f57;
  --c-ok-soft: rgba(13, 127, 87, .12);
  --c-warn: #8a5d00;
  --c-warn-soft: rgba(138, 93, 0, .12);
  --c-error: #c62828;
  --c-error-soft: rgba(198, 40, 40, .1);

  /* --- имена авторов в группе -------------------------------------------
     Те же шесть оттенков и те же номера, но темнее: в тёмной теме они стоят
     на тёмном пузыре, здесь — на белом. */
  --c-author-0: #b0434f; /* роза */
  --c-author-1: #8f5a1a; /* глина */
  --c-author-2: #4c6a2b; /* мох */
  --c-author-3: #1f6f68; /* морская волна */
  --c-author-4: #3f5591; /* индиго */
  --c-author-5: #7a4a86; /* слива */

  /* --- фирменные цвета мессенджеров -------------------------------------
     Кольцо вокруг аватара оставляем как есть — линия на белом читается. А
     неоновая подпись в углу кружка светом здесь не работает вовсе: свечение
     убираем, буквы темнеем. */
  --c-brand-tg-neon: #1a6ea8;
  --c-brand-max-neon: #5b3ea3;
  --c-brand-wa-neon: #0b7a46;
  --glow-brand-tg: none;
  --glow-brand-max: none;
  --glow-brand-wa: none;

  /* --- заливки ----------------------------------------------------------- */
  --grad-skeleton: linear-gradient(100deg, #e4e6e9 20%, #eff1f3 45%, #e4e6e9 70%);
  /* Заглушка внутри своего пузыря: он светлый, и осветлением её не видно —
     здесь то же переливание, но затемнением. */
  --c-skeleton-on-bubble-out: rgba(11, 20, 26, .08);
  --grad-skeleton-on-bubble-out: linear-gradient(
    100deg,
    rgba(11, 20, 26, .05) 20%,
    rgba(11, 20, 26, .13) 45%,
    rgba(11, 20, 26, .05) 70%
  );

  /* --- объём ------------------------------------------------------------- */
  --shadow-dark: rgba(11, 20, 26, .18);
  --neo-out-lg: 0 8px 24px rgba(11, 20, 26, .16), 0 0 0 1px var(--c-hairline);
  --shadow-accent: 0 1px 2px rgba(11, 20, 26, .14);
  --shadow-bubble: 0 1px 1px rgba(11, 20, 26, .13);
  --shadow-modal: 0 16px 40px rgba(11, 20, 26, .25);
}

/* ======================================================================
   База
   ====================================================================== */

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

html,
body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--c-bg);
  background-image: var(--grad-bg);
  background-attachment: fixed;
  color: var(--c-text);
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* Фокус показываем только с клавиатуры: мышью по кнопке кольца нет.
   Кольцо рисуем outline'ом — он сам повторяет border-radius элемента и не
   конфликтует с волосяными линиями, которые живут в box-shadow. */
:focus { outline: none; }
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

::selection { background: var(--c-accent-soft); color: var(--c-text); }

/* Двойной тап по кнопке, строке списка или полю на телефоне не должен ни
   зумить страницу, ни добавлять к нажатию задержку, пока браузер ждёт второй
   тап. Прокрутке это не мешает: запрещён только зум двумя пальцами по самому
   элементу. */
button,
.btn-round,
.btn-pill,
.filter-tab,
.row-item,
.field,
input,
textarea,
select { touch-action: manipulation; }

/* Полоса прокрутки под тему: своя, но не навязчивая. */
* { scrollbar-width: thin; scrollbar-color: var(--c-scrollbar) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--c-scrollbar); border-radius: var(--r-pill); }

/* ======================================================================
   Текст
   ====================================================================== */

.t-title { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); margin: 0; }
.t-head { font-size: var(--fs-lg); font-weight: var(--fw-semibold); line-height: var(--lh-tight); margin: 0; }
.t-body { font-size: var(--fs-md); font-weight: var(--fw-regular); line-height: var(--lh-normal); }
.t-sub { font-size: var(--fs-sm); color: var(--c-text-2); line-height: var(--lh-normal); }
.t-meta { font-size: var(--fs-sm); color: var(--c-text-3); }
.t-micro { font-size: var(--fs-xs); color: var(--c-text-3); letter-spacing: var(--tracking-wide); }
.t-accent { color: var(--c-accent-text); }
.t-ok { color: var(--c-ok); }
.t-warn { color: var(--c-warn); }
.t-error { color: var(--c-error); }
.t-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* Расстояния между блоками задаёт контейнер (flex + gap). Своих полей у текста
   нет: иначе к разбегу контейнера прибавляется ещё и умолчание браузера для
   <p>, и один и тот же класс отбивается по-разному на <p> и на <div>. */
.t-body,
.t-sub,
.t-meta,
.t-micro,
.t-accent,
.t-ok,
.t-warn,
.t-error { margin: 0; }

/* ======================================================================
   Поверхности
   ====================================================================== */

/* Выпуклая поверхность: карточка, панель, док. */
.neo-raised {
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out);
  border-radius: var(--r-card);
}

/* Утопленная поверхность: поле ввода, лента, выбранный сегмент. */
.neo-sunken {
  background: var(--c-surface-sunken);
  box-shadow: var(--neo-in);
  border-radius: var(--r-card);
}

/* Плоская поверхность: строка списка, ячейка. Объём даёт только ховер. */
.neo-flat {
  background: transparent;
  box-shadow: var(--neo-flat);
}

.card {
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out);
  border-radius: var(--r-card);
  padding: var(--sp-6);
}

/* Разделитель. Горизонтальный по умолчанию, .divider.v — вертикальный. */
.divider {
  border: 0;
  margin: 0;
  align-self: stretch;
  background: var(--c-hairline);
  height: 1px;
  width: 100%;
}
.divider.v { height: auto; width: 1px; }

/* Разделитель с подписью посередине: «Сегодня», «Новые сообщения». */
.divider-label {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--c-text-3);
  margin: var(--sp-4) 0;
}
.divider-label::before,
.divider-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--c-hairline);
}

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

button,
.btn-round,
.btn-pill {
  font-family: inherit;
  color: var(--c-text);
  border: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* Круглая кнопка — базовая форма всего кликабельного. Только значок: ни
   подложки, ни обводки в покое.

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

   Сдвига под нажатием тоже больше нет: он передавал «кнопка вдавилась»,
   а вдавливаться теперь нечему. */
.btn-round {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--size-control);
  height: var(--size-control);
  padding: 0;
  border-radius: var(--r-circle);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: var(--c-icon);
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.btn-round:hover { color: var(--c-icon-strong); background-color: var(--c-row-hover); }
.btn-round:active { color: var(--c-icon-strong); background-color: var(--c-row-press); }
.btn-round.sm { width: var(--size-control-sm); height: var(--size-control-sm); }
.btn-round.lg { width: var(--size-avatar-lg); height: var(--size-avatar-lg); }

/* Выбранная круглая кнопка: держит акцентную подложку, пока выбор держится.
   Раньше она оставалась вдавленной — по правилу «вдавленное заполняют»; в
   плоской теме заполнение и есть единственный способ это сказать. */
.btn-round.on {
  background-color: var(--c-accent-soft);
  color: var(--c-icon-accent);
}

/* Акцентная круглая кнопка: главное действие экрана. Обводки у неё нет по
   той же причине, что у остальных, — зелёный круг и без неё виден. */
.btn-round.accent {
  background-image: var(--grad-accent);
  color: var(--c-icon-on-accent);
  box-shadow: var(--shadow-accent);
}
.btn-round.accent:hover { background-image: var(--grad-accent-hover); }
.btn-round.accent:active { background-image: var(--grad-accent); }

/* Кнопка-пилюля с текстом: формы входа, диалоги, действия в списке. */
.btn-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--size-field);
  padding: 0 var(--sp-5);
  border-radius: var(--r-pill);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out-sm);
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    background-image var(--dur-fast) var(--ease-out);
}
.btn-pill:hover { box-shadow: var(--neo-out); }
.btn-pill:active { box-shadow: var(--neo-press); }

.btn-pill.accent {
  background-image: var(--grad-accent);
  color: var(--c-on-accent);
  box-shadow: var(--neo-out-sm), var(--shadow-accent);
}
.btn-pill.accent:hover { background-image: var(--grad-accent-hover); }
.btn-pill.accent:active { box-shadow: var(--neo-press); background-image: var(--grad-accent); }

/* Тихая кнопка: без объёма, только текст. Для «Отмена», «Ещё». */
.btn-pill.quiet {
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: var(--c-text-2);
}
.btn-pill.quiet:hover { color: var(--c-text); background-image: var(--grad-surface); box-shadow: var(--neo-out-sm); }

.btn-pill.danger { color: var(--c-error); }

/* Выключенная кнопка гаснет ЦВЕТОМ значка и подписи, а подложку и обводку
   сохраняет от своего вида: круглая остаётся прозрачной, пилюля — залитой.
   Раньше здесь навязывалась вдавленная подложка, и в плоской теме
   выключенная круглая кнопка выглядела бы тёмным кружком посреди шапки. */
.btn-round[disabled],
.btn-pill[disabled],
button[disabled] {
  cursor: default;
  color: var(--c-text-3);
  transform: none;
}
/* Заливку теряет только круглая: у неё в покое подложки нет вовсе, и
   наведение на выключенную не должно её рисовать. */
.btn-round[disabled]:hover,
.btn-round[disabled]:active { background-color: transparent; color: var(--c-text-3); }

/* ======================================================================
   Ряд фильтров
   ====================================================================== */

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

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

.filter-row .divider.v { flex: none; }

/* Таб фильтра: пилюля с волосяной линией, границу рисует она, а не объём.
   Заливка своя (--c-chip): в светлой теме пилюля БЕЛАЯ, а не песочная, как
   была (решение владельца продукта 18.09.2026) — на белой панели списка тёплый
   песок читался как грязь. В тёмной теме это по-прежнему полотно: там пилюля
   должна быть темнее панели, иначе она с ней сливается.
   Фирменную обводку включённому табу мессенджера добавляет класс .brand-ring. */
.filter-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  min-height: var(--size-control-sm);
  padding: 0 var(--sp-3);
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  color: var(--c-text-2);
  background: var(--c-chip);
  box-shadow: var(--neo-out-sm);
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.filter-tab:hover { color: var(--c-text); box-shadow: var(--neo-out); }
.filter-tab:active { box-shadow: var(--neo-press); }
.filter-tab[aria-pressed="true"] {
  color: var(--c-text);
  box-shadow: var(--neo-press);
}

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

   Тумблеров мессенджера (Telegram, MAX) это НЕ касается: у них своя фирменная
   обводка и свой цвет счётчика, и зелёная заливка спорила бы с ними. Поэтому
   правило прицельно к ряду видов, а не ко всем табам. */
.side-filters .filter-tab[aria-pressed="true"] {
  color: var(--c-on-accent);
  background: var(--c-accent);
  box-shadow: none;
}
.side-filters .filter-tab[aria-pressed="true"]:hover { color: var(--c-on-accent); }
.side-filters .filter-tab[aria-pressed="true"] .badge-count {
  background: var(--c-on-accent);
  background-image: none;
  color: var(--c-accent-deep);
  box-shadow: none;
}

/* Включённый ТУМБЛЕР МЕССЕНДЖЕРА — фирменный градиент во всю пилюлю: синий у
   Telegram, сине-фиолетовый у MAX, зелёный у WhatsApp. Градиент приходит
   классом brand-* через --grad-brand, тем же, что красит кольцо аватара, —
   цвет мессенджера в окне один и живёт в одном месте.

   Зелёная заливка ряда ВИДОВ переписок сюда не относится и не должна: она
   говорит «фильтр включён», а здесь цвет говорит, какой это мессенджер. */
.head-transports .filter-tab[aria-pressed="true"] {
  color: var(--c-on-accent);
  background: var(--grad-brand);
  box-shadow: none;
}
.head-transports .filter-tab[aria-pressed="true"]:hover { color: var(--c-on-accent); }

/* Счётчик на залитом тумблере выворачивается: белая пилюля с фирменной
   цифрой. Акцентный градиент поверх фирменного не читается вовсе — два
   градиента спорят и оба проигрывают. */
.head-transports .filter-tab[aria-pressed="true"] .badge-count {
  background: var(--c-on-accent);
  background-image: none;
  color: var(--c-brand-deep);
  box-shadow: none;
}

/* Счётчик внутри таба стоит вплотную к слову, отдельного разбега ему не надо:
   gap таба уже отбивает его от подписи. */
.filter-tab .badge-count { flex: none; }

/* ВКЛАДКА КНИГИ в окне «Написать» — тот же разговор о цвете, что у тумблеров:
   включённая залита цветом своего мессенджера, и приходит он тем же классом
   brand-* через --grad-brand. Книга «Мои контакты» мессенджером не является,
   своего градиента у неё нет — подставляется акцент.

   Зелёного свечения под пилюлей здесь нет намеренно: под синей вкладкой
   Telegram оно читается грязью, а ряду вкладок свечение и не нужно — они и
   так залиты. */
.book-tabs .btn-pill.accent,
.book-tabs .btn-pill.accent:hover,
.book-tabs .btn-pill.accent:active {
  background-image: var(--grad-brand, var(--grad-accent));
}
.book-tabs .btn-pill.accent { box-shadow: var(--neo-out-sm); }
.book-tabs .btn-pill.accent:hover { box-shadow: var(--neo-out); }
.book-tabs .btn-pill.accent:active { box-shadow: var(--neo-press); }

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

   Обводка кружка — цветом значка, а не волосяной линией: он лежит поверх
   обоев, и линия в восемь процентов белого на них теряется. */
.pick-box {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: var(--size-check);
  height: var(--size-check);
  border-radius: var(--r-circle);
  color: var(--c-icon);
  box-shadow: inset 0 0 0 2px currentColor;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* Галочка не появляется, а РИСУЕТСЯ: линия выезжает из нуля в свою длину.
   Это одно свойство на одном коротком пути — ни перерисовки страницы, ни
   просчёта раскладки; «меньше движения» в системе гасит её тем же общим
   правилом, что и всё остальное. Длина пути (m4 12.5 5 5L20 6.5) — 23
   единицы, отсюда и штрих. */
.pick-box > svg {
  width: var(--icon-xs);
  height: var(--icon-xs);
  overflow: visible;
}
.pick-box > svg path {
  stroke-dasharray: 23;
  stroke-dashoffset: 23;
  transition: stroke-dashoffset var(--dur-base) var(--ease-out);
}

.msg.picked .pick-box {
  color: var(--c-on-accent);
  background-color: var(--c-accent);
  box-shadow: none;
}
.msg.picked .pick-box > svg path { stroke-dashoffset: 0; }

/* Плашка выбора — стекло: полупрозрачная панель с размытием, сквозь которую
   видно уезжающую под неё ленту. Тень та же, что у модального окна: она и
   вправду висит над содержимым, а не лежит в потоке. */
.pick-bar {
  border-radius: var(--r-pill);
  background: var(--c-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: var(--shadow-modal), inset 0 0 0 1px var(--c-hairline);
  animation: rise-in var(--dur-base) var(--ease-out);
}

.pick-bar .pick-count { padding-right: var(--sp-3); }

/* Корзина в панели выбора — единственное необратимое действие на экране, и
   цвет у неё свой. Заливки нет: красный круг под пальцем посреди переписки
   выглядит тревожнее, чем само действие. */
.pick-bar .btn-round.danger { color: var(--c-error); }
.pick-bar .btn-round.danger:hover,
.pick-bar .btn-round.danger:active { color: var(--c-error); background-color: var(--c-error-soft); }

/* ======================================================================
   Поля ввода
   ====================================================================== */

/* Поле-пилюля: вдавленное, того же тона, что фон. Поиск, логин, пароль. */
.field {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: var(--size-field);
  padding: 0 var(--sp-5);
  border-radius: var(--r-pill);
  background: var(--c-bg);
  box-shadow: var(--neo-in);
  color: var(--c-text);
  transition: box-shadow var(--dur-base) var(--ease-out);
}
.field:focus-within { box-shadow: var(--neo-in), var(--ring-accent); }
.field.invalid { box-shadow: var(--neo-in), var(--ring-error); }

/* Поле переписки — единственное, которое ни во что не вложено: полосы с
   заливкой под ним больше нет, оно висит прямо над обоями. Поэтому заливка у
   него панельная, а не фоновая, и тень та же, что у пузырей, — они висят над
   тем же полотном. Вдавленности здесь нет: край плавающей формы рисует тень,
   волосяная линия остаётся только тем, что лежит НА поверхности. */
.composer .field {
  background: var(--c-surface);
  box-shadow: var(--neo-out), var(--shadow-bubble);
}
.composer .field:focus-within {
  box-shadow: var(--neo-out), var(--shadow-bubble), var(--ring-accent);
}

/* Поиск в списке переписок — та же пилюля, что у фильтров рядом: на светлой
   теме песочная заливка поля спорила с белой панелью списка. */
.side-search .field { background: var(--c-chip); }

/* Само поле внутри пилюли — прозрачное, рамка и объём на .field. */
.field input,
.field textarea,
.field-input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: none;
  background: transparent;
  color: var(--c-text);
  font-family: inherit;
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  padding: var(--sp-3) 0;
  resize: none;
}
.field input::placeholder,
.field textarea::placeholder,
.field-input::placeholder { color: var(--c-text-3); }

/* Одиночный input без обёртки — та же пилюля. */
input.field,
textarea.field {
  border: 0;
  outline: none;
  font-family: inherit;
  font-size: var(--fs-md);
  padding: var(--sp-3) var(--sp-5);
}
textarea.field { border-radius: var(--r-md); min-height: var(--size-field); }

.field[aria-disabled="true"],
.field input:disabled { color: var(--c-text-3); cursor: default; }

/* Подпись над полем. */
.field-label {
  display: block;
  margin: 0 0 var(--sp-2) var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--c-text-2);
}

/* Сообщение об ошибке под полем. */
.field-error {
  display: block;
  margin: var(--sp-2) 0 0 var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--c-error);
}

/* ======================================================================
   Пузыри сообщений
   ====================================================================== */

.bubble {
  position: relative;
  /* flow-root, а не block: время внутри пузыря плавающее (см. .bubble-meta),
     а обычный блок высоту плавающего потомка не считает — метка вылезала бы
     за нижнее поле пузыря. */
  display: flow-root;
  max-width: min(62ch, 78%);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-bubble);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-bubble);
}

/* Входящее: поверхность чуть светлее фона. */
.bubble.in {
  align-self: flex-start;
  background: var(--c-surface-raised);
  background-image: var(--grad-surface-soft);
  color: var(--c-text);
}

/* Исходящее: свой тёмный зелёный, НЕ акцентный — см. правило 3 в начале файла. */
.bubble.out {
  align-self: flex-end;
  background-image: var(--grad-bubble-out);
  color: var(--c-on-bubble-out);
  box-shadow: var(--shadow-bubble);
}

/* Хвостик у первого пузыря серии (решение владельца продукта 18.09.2026).

   Раньше на его месте был просто срезанный до 3 точек НИЖНИЙ угол — приём от
   старой тёмной темы. В привычных мессенджерах хвостик растёт из ВЕРХНЕГО угла
   со своей стороны, и этот угол у такого пузыря не скруглён вовсе.

   Рисуем псевдоэлементом: своя картинка на каждый пузырь была бы лишним
   запросом, а границами треугольник такой заливки не собрать. Заливка названа
   цветом, а не `inherit`: у пузыря она градиентная, и в крошечном
   псевдоэлементе тот же градиент лёг бы другим куском. */
.bubble.tail.in { border-top-left-radius: 0; }
.bubble.tail.out { border-top-right-radius: 0; }

.bubble.tail::before {
  content: '';
  position: absolute;
  top: 0;
  width: var(--size-bubble-tail);
  height: calc(var(--size-bubble-tail) * 1.5);
}
.bubble.tail.in::before {
  left: calc(-1 * var(--size-bubble-tail));
  background: var(--c-surface-raised);
  clip-path: polygon(100% 0, 100% 100%, 0 0);
}
.bubble.tail.out::before {
  right: calc(-1 * var(--size-bubble-tail));
  background: var(--c-bubble-out);
  clip-path: polygon(0 0, 0 100%, 100% 0);
}

/* Имя автора в групповой переписке и в канале. Оттенок тот же, что у аватара,
   но в светлом варианте под текст, и считается он от автора, а не от переписки:
   в группе людей много, и по цвету имени они различаются с одного взгляда.
   В диалоге подписи нет — там собеседник один. Свой оттенок берётся классом
   `tone-0` … `tone-5`, как у аватара; без него имя остаётся приглушённым. */
.bubble-author { color: var(--c-text-2); }
.bubble-author.tone-0 { color: var(--c-author-0); }
.bubble-author.tone-1 { color: var(--c-author-1); }
.bubble-author.tone-2 { color: var(--c-author-2); }
.bubble-author.tone-3 { color: var(--c-author-3); }
.bubble-author.tone-4 { color: var(--c-author-4); }
.bubble-author.tone-5 { color: var(--c-author-5); }

/* Время и статус внутри пузыря. */
.bubble .bubble-meta {
  font-size: var(--fs-sm);
  color: var(--c-text-3);
  white-space: nowrap;
}
.bubble.out .bubble-meta { color: var(--c-on-bubble-out-2); }

/* Галочка доставки. Цвет не свой, а от подписи пузыря: на зелёном исходящем
   любой другой оттенок читался бы как второй акцент. Отличаем состояния
   прозрачностью — «ушло» бледнее, «прочитано» в полную силу. */
.msg-status {
  flex: none;
  opacity: .7;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.msg-status.seen { opacity: 1; }

/* Прочитанное горит зелёным, и это единственное место, где зелёный не
   акцентный и не пузырный: он должен читаться поверх самого пузыря. В светлой
   теме тон глубже — на бледно-зелёном исходящем ярко-зелёный теряется. */
.msg-status.read { opacity: 1; color: var(--c-tick-read); }

/* Неотправленное: галочки нет, вместо неё восклицательный знак. Цвет — тот же,
   что у подписи пузыря: знак стоит на зелёном, где красный не виден. Ошибку здесь
   называет не цвет, а кольцо вокруг пузыря (.bubble.failed) и подпись для
   скринридера. */
.msg-status.failed {
  opacity: 1;
  font-weight: var(--fw-bold);
}

/* --- пузырь-картинка ------------------------------------------------------

   Поле пузыря отдано кадру: от краёв остаётся волосок, как в привычных
   мессенджерах, и картинка идёт от края до края. Всё, что под кадром, живёт в
   .bubble-caption и держит своё поле само — иначе подпись прилипла бы к краям.

   Скругление кадра на волосок меньше пузырного: вложенная форма с тем же
   радиусом выглядит острее внешней. */
.bubble.media { padding: 3px; }
/* Ширину пузыря с кадром задаёт КАДР, а не подпись. Раньше длинный текст
   растягивал пузырь до своих 62ch, а кадр оставался в своих --size-media-w — и
   справа от картинки зияло пустое поле. Теперь подпись переносится под кадром,
   как в привычных мессенджерах, а сам кадр не растягиваем: его пропорции
   считаны по присланным размерам, и тянуть его — значит мылить картинку.
   Шесть точек — это поле пузыря по 3px с каждой стороны (box-sizing: border-box).

   Селектор через .msg, а не просто .bubble.media: styles.css подключается ПОСЛЕ
   темы, и его `.msg .bubble { max-width: 100% }` при равном весе перебил бы
   правило. Этот же предел здесь и сохранён — на узком экране пузырь обязан
   остаться в берегах .msg, а кадр внутри дожмёт свой собственный max-width. */
.msg .bubble.media { max-width: min(calc(var(--size-media-w) + 6px), 100%); }
.bubble.media > .msg-media.shaped,
.bubble.media > .msg-album {
  margin-bottom: 0;
  border-radius: calc(var(--r-bubble) - 3px);
}
/* Имя автора, пометка «переслано» и цитата стоят НАД кадром, и поле им нужно
   своё: у пузыря его больше нет. Сверху тоже — иначе имя липнет к краю. */
.bubble.media > .bubble-author,
.bubble.media > .bubble-forward,
.bubble.media > .bubble-reply {
  margin-inline: var(--sp-2);
  margin-top: var(--sp-1);
}

/* Время НА кадре: под картинкой ничего больше нет, и своя строка ему ни к
   чему. Подложка обязательна — на светлом кадре белые цифры пропадают. */
.bubble .bubble-meta.on-media,
.bubble.out .bubble-meta.on-media {
  padding: 1px var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--c-overlay);
  color: var(--c-on-accent);
}

/* Неотправленное сообщение: тот же пузырь, но приглушённый. */
.bubble.pending { opacity: .6; }
.bubble.failed { box-shadow: var(--shadow-bubble), var(--ring-failed); }

/* Повтор неудачной отправки — под самим пузырём, со стороны отправителя.
   Выглядит подписью, а не кнопкой действия: главное здесь по-прежнему
   сообщение, а красный цвет уже занят кольцом вокруг пузыря. */
.msg-retry {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  margin-top: var(--sp-1);
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-error);
  cursor: pointer;
}
.msg-retry:hover { text-decoration: underline; }

/* --- вложение в пузыре ---------------------------------------------------

   Картинка, файл или голосовое стоят НАД текстом сообщения, ширину им задаёт
   сам пузырь. Отбивка снизу маленькая: подпись под картинкой — часть того же
   сообщения, а не отдельный блок. */
.msg-media {
  display: block;
  margin-bottom: var(--sp-1);
}

/* Окно с ИЗВЕСТНЫМИ пропорциями кадра. Ширина прежняя, высоту задаёт сам кадр:
   размеры приходят вместе с сообщением, и разметка ставит их до выкачки файла
   (см. mediaRatio в app.js). Пропорции живут на окне, а не на картинке, чтобы
   размытая заглушка, мигающий скелетон и сам кадр занимали РОВНО одно и то же
   место: подстановка не должна двигать ленту ни на точку.

   Дети растягиваются на всё окно и своих скруглений не несут — их держит окно,
   иначе на краях вылезают светлые уголки. */
.msg-media.shaped {
  position: relative;
  width: var(--size-media-w);
  max-width: 100%;
  aspect-ratio: var(--media-ratio, 4 / 3);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.msg-media.shaped > .media-photo,
.msg-media.shaped > .media-video,
.msg-media.shaped > .media-video-player,
.msg-media.shaped > .media-blur,
.msg-media.shaped > .skeleton.media-hold {
  width: 100%;
  height: 100%;
  border-radius: 0;
}

/* Заглушка, по которой можно нажать: та же кнопка, что и у готовой картинки,
   только внутри неё пока размытое пятно. Своей заливки у неё нет — пятно
   занимает её целиком. */
.media-hold-press {
  padding: 0;
  border: 0;
  background: transparent;
  cursor: zoom-in;
}
/* Пятно внутри кнопки занимает её целиком. Названо в упор, а не звёздочкой:
   у самого пятна размеры заданы своим правилом, и звёздочка их не перебьёт. */
.media-hold-press > .media-blur,
.media-hold-press > .skeleton.media-hold {
  width: 100%;
  height: 100%;
}

/* Кольцо ожидания поверх заглушки: нажали по картинке, которой ещё нет. */
.media-wait {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Кольцо — подпись «едет», а не орган управления, и садится оно ровно в
     середину кадра: без этого повторное нажатие по ждущей картинке упиралось в
     кольцо вместо кнопки. */
  pointer-events: none;
}

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

   Ширина задана числом, а не процентом, и это не мелочь. Пузырь сжимается по
   содержимому, а процент в такую ширину не входит вовсе: пока картинки нет,
   просить 280 точек в пузыре некому, и окно превью схлопывалось до ширины
   времени — картинка приезжала вертикальной полоской и выправлялась только
   следующей перерисовкой, когда уже загруженный файл отдавал свою натуральную
   ширину. С числом место занято сразу и не зависит от того, приехал файл или
   нет; `max-width` держит окно в берегах узкого экрана. */
.media-photo {
  display: block;
  width: var(--size-media-w);
  max-width: 100%;
  height: var(--size-media-h);
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: transparent;
  line-height: 0;
  cursor: zoom-in;
}
/* Картинка заполняет окно целиком и обрезается по нему: настоящих размеров
   файла разметка не знает до самой загрузки, а подстраивать под них окно —
   значит раздвигать ленту у человека под руками. Что не поместилось, видно в
   просмотре во весь экран — он открывается нажатием. */
.media-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Плитка альбома. Ширина та же, что у одиночного вложения: в ленте они стоят
   рядом, и разъехавшиеся размеры читаются как небрежность. Ячейки квадратные —
   так плитка держит форму при любом числе фотографий, а сами снимки заполняют
   ячейку по короткой стороне. */
.msg-album {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2px;
  width: var(--size-media-w);
  max-width: 100%;
  border-radius: var(--r-sm);
  overflow: hidden;
}

/* От пяти штук — три в ряд: по два получалась бы колонка в пол-экрана. */
.msg-album.n5,
.msg-album.n6 { grid-template-columns: repeat(3, 1fr); }

.msg-album .msg-media.album { aspect-ratio: 1; }

/* Внутри ячейки вложение занимает её целиком и своих скруглений не несёт:
   их держит сама плитка, иначе на стыках появляются белые уголки. */
.msg-album .media-blur,
.msg-album .media-photo,
.msg-album .media-video,
.msg-album .media-video-player,
.msg-album .skeleton.media-hold {
  width: 100%;
  height: 100%;
  border-radius: 0;
}

/* Обложка видео. Геометрия та же, что у превью картинки: в ленте они стоят
   рядом, и разъехавшиеся размеры читаются как небрежность. */
.media-video {
  position: relative;
  display: block;
  width: var(--size-media-w);
  max-width: 100%;
  height: var(--size-media-h);
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: transparent;
  cursor: pointer;
}

/* Настоящая обложка ролика — тот же узел, что у размытой заглушки, но кадр
   честный: размытие снимаем, увеличение под кайму размытия тоже. */
.media-cover {
  filter: none;
  transform: none;
}

/* Обложка внутри кнопки занимает её целиком: своё размытие у неё уже есть. */
.media-video .media-blur {
  width: 100%;
  height: 100%;
  border-radius: 0;
}

/* Кнопка проигрывания по центру обложки. Круг с подложкой, а не голый значок:
   на светлом кадре белый треугольник пропадает. */
.media-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--size-control);
  height: var(--size-control);
  border-radius: var(--r-circle);
  background: var(--c-overlay);
  color: var(--c-on-accent);
  pointer-events: none;
}

/* Размер ролика — в углу обложки, как в мессенджерах: по нему человек решает,
   качать ли сейчас. */
.media-video-size {
  position: absolute;
  right: var(--sp-2);
  bottom: var(--sp-2);
  padding: 2px var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--c-overlay);
  color: var(--c-on-accent);
  pointer-events: none;
}

/* Анимированный стикер: размер как у обычного стикера-картинки, без рамок и
   подложки — у него своя прозрачность. */
.media-sticker {
  display: block;
  width: 160px;
  max-width: 100%;
  height: auto;
  background: transparent;
}

.media-video-player {
  display: block;
  width: var(--size-media-w);
  max-width: 100%;
  border-radius: var(--r-sm);
  background: #000;
}

/* Проигрыватель МУЗЫКАЛЬНОГО файла — свой, браузерный. Темой он не красится:
   рисовать собственный ради редкого присланного трека дороже, чем это стоит.
   У голосового проигрыватель свой — см. .media-voice ниже. */
.media-audio {
  display: block;
  /* Ширина та же и по той же причине, что у превью картинки: число, а не
     процент, иначе проигрыватель в сжимающемся пузыре остаётся полоской. */
  width: var(--size-media-w);
  max-width: 100%;
  /* Высота та же, что у заглушки на его месте: иначе лента подпрыгивала бы,
     когда голосовое приезжает. */
  height: var(--size-control);
}

/* --- голосовое: своя волна -----------------------------------------------

   Кнопка, волна столбиками и время — как в WhatsApp и MAX. Волна лежит В
   ПУЗЫРЕ, поэтому тонов у неё две пары: на входящем это цвета текста, на
   исходящем — свои цвета пузыря. Ни одного нового токена здесь нет и быть не
   должно: волна — СОДЕРЖИМОЕ сообщения, а не акцент, а акцент в теме один на
   экран.

   Прослушанное — полный тон, ещё не прослушанное — приглушённый; обе фигуры
   рисует один и тот же путь, верхний обрезан по месту проигрывания
   (см. voiceWave в app.js). */
.media-voice {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: var(--size-media-w);
  max-width: 100%;
  /* Ровно высота заглушки, которая стояла тут, пока файл ехал: подстановка не
     должна двигать ленту ни на точку. */
  min-height: var(--size-control);
}

/* Сам звук: узел есть, места нет. Совсем убрать его нельзя — на нём держится
   и проигрывание, и сторож мёртвых ссылок (он слушает `error` от audio). */
.media-voice-sound { display: none; }

/* Кнопка «играть/пауза». Круглая форма и размер — общие, от .btn-round.sm;
   здесь только заливка, которой у круглой кнопки в покое нет: посреди пузыря
   голый значок теряется. Во входящем это утопленная поверхность, как у плашки
   файла рядом, в исходящем — осветление поверх зелёного, как у своей реакции:
   линия на зелёном читалась бы обводкой. */
.media-voice-play {
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
  color: var(--c-text);
}
.media-voice-play:hover,
.media-voice-play:active {
  background-color: var(--c-bg);
  box-shadow: var(--neo-press);
  color: var(--c-text);
}
.bubble.out .media-voice-play {
  background: var(--c-on-bubble-out-fill);
  box-shadow: none;
  color: var(--c-on-bubble-out);
}
.bubble.out .media-voice-play:hover,
.bubble.out .media-voice-play:active {
  background-color: var(--c-on-bubble-out-fill);
  color: var(--c-on-bubble-out);
}

.media-voice-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Полоса перемотки. Отбивка сверху и снизу — это площадь под палец: сама
   волна для пальца низковата, а расти вверх ей некуда — высота проигрывателя
   задана заглушкой, которая стояла на его месте.
   `touch-action` не трогаем: прокрутка ленты пальцем по волне должна
   оставаться прокруткой, и решает это браузер. */
.media-voice-track {
  padding: var(--sp-1) 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.media-voice-track:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* Волна тянется по ширине пузыря (это делает preserveAspectRatio="none" в
   разметке), а нажатия не ловит вовсе: между столбиками пустота, и палец
   попадал бы мимо — ловит их полоса вокруг. */
.media-voice-wave {
  display: block;
  width: 100%;
  /* Вся высота проигрывателя — --size-control (48), и складывается она РОВНО:
     волна 22, отбивки полосы 4 + 4, разбег 2 и строка времени 13 × 1.2 ≈ 16.
     Меняешь одно — пересчитывай остальное, иначе лента дёрнется в тот миг,
     когда файл приехал и заглушка сменилась проигрывателем. */
  height: 22px;
  pointer-events: none;
}

/* Столбики. Толщина — в ТОЧКАХ, а не в единицах волны: волна растягивается по
   ширине пузыря, и без vector-effect столбики растягивались бы вместе с ней,
   превращаясь в заливку на широком экране и в нитки на узком. */
.media-voice-bars {
  fill: none;
  stroke-width: 3px;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.media-voice-bars.dim { stroke: var(--c-text-3); }
.media-voice-bars.lit { stroke: var(--c-text); }
.bubble.out .media-voice-bars.dim { stroke: var(--c-on-bubble-out-2); }
.bubble.out .media-voice-bars.lit { stroke: var(--c-on-bubble-out); }

/* Дорожки мессенджер не прислал: ровная полоса вместо выдуманной формы.
   Тоньше столбиков намеренно — это честная подпись «формы записи мы не
   знаем», а не сама форма. Перематывать по ней можно так же. */
.media-voice-wave.flat .media-voice-bars { stroke-width: 2px; }

/* Время под волной: цифры одной ширины, иначе подпись дёргается на каждой
   секунде. */
.media-voice-time {
  font-size: var(--fs-sm);
  /* Плотная строка, а не общая 1.45: высота проигрывателя считана под неё. */
  line-height: var(--lh-tight);
  color: var(--c-text-2);
  font-variant-numeric: tabular-nums;
}
.bubble.out .media-voice-time { color: var(--c-on-bubble-out-2); }

/* Плашка файла: значок, имя, размер и кнопка «скачать». Во входящем пузыре
   она вдавлена, в исходящем остаётся плоской — на акцентном градиенте
   неоморфная выемка превратилась бы в грязь. */
.media-file {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
}
.bubble.out .media-file {
  background: transparent;
  box-shadow: none;
  padding-inline: 0;
}
.media-file-art {
  flex: none;
  display: inline-grid;
  place-items: center;
  color: var(--c-icon);
}
.bubble.out .media-file-art { color: var(--c-on-bubble-out-2); }

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

   Лист собран заливками, а не картинкой: цвет берётся из токенов темы, размер
   — из этих же правил, и в тёмной палитре подменять нечего. Срез правого
   верхнего угла и треугольник загиба рисуются одной парой чисел (21/11), и
   менять их нужно вместе, иначе загиб съедет с выреза. */
.media-file-doc {
  position: relative;
  display: block;
  width: 32px;
  height: 40px;
  border-radius: 3px;
  background: var(--c-file-paper);
  clip-path: polygon(0 0, 21px 0, 32px 11px, 32px 40px, 0 40px);
  overflow: hidden;
}

.media-file-doc::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 11px;
  height: 11px;
  background: var(--c-file-fold);
  clip-path: polygon(0 0, 100% 100%, 0 100%);
}

/* Полоса с расширением — поперёк листа во всю его ширину. Кегль здесь числом,
   а не токеном: полоса шириной 32 точки обязана вместить четыре буквы
   (XLSX, PPTX, WEBM), и мельчайший из токенов для этого уже велик. */
.media-file-ext {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 5px;
  display: grid;
  place-items: center;
  height: 14px;
  background: var(--c-file-other);
  color: var(--c-file-on);
  font-size: 9px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.03em;
  line-height: 1;
}
.media-file-ext.f-pdf  { background: var(--c-file-pdf); }
.media-file-ext.f-doc  { background: var(--c-file-doc); }
.media-file-ext.f-xls  { background: var(--c-file-xls); }
.media-file-ext.f-ppt  { background: var(--c-file-ppt); }
.media-file-ext.f-zip  { background: var(--c-file-zip); }
.media-file-ext.f-txt  { background: var(--c-file-txt); }
.media-file-ext.f-data { background: var(--c-file-data); }
.media-file-ext.f-app  { background: var(--c-file-app); }
.media-file-ext.f-book { background: var(--c-file-book); }
.media-file-ext.f-art  { background: var(--c-file-art); }
/* На акцентном пузыре цвет бейджика свой, а не унаследованный от подписи:
   иначе исходящий файл терял бы вид, ради которого бейджик и заведён. */
.bubble.out .media-file-ext { color: var(--c-file-on); }
.media-file-body { flex: 1; min-width: 0; }

/* В пузыре плашка растянута на его ширину, и кнопка скачивания уезжала к
   дальнему краю — от файла, к которому относится, её отделяла пустая строка.
   Здесь подпись занимает лишь своё, и кнопка стоит сразу за ней. Плашки над
   строкой ввода это не касается: там крестик у края и есть его место. */
.msg-media .media-file-body { flex: 0 1 auto; }
.media-file-name { font-size: var(--fs-sm); font-weight: var(--fw-medium); }

/* Размер и вид файла. Свой класс, а не .t-meta: на акцентном пузыре
   приглушённый серый не читается, и цвет там берётся от подписи пузыря. */
.media-sub { font-size: var(--fs-sm); color: var(--c-text-2); }
.bubble.out .media-sub { color: var(--c-on-bubble-out-2); }

/* Отказ показываем строкой в самом пузыре: человек ничего не нажимал, и тост
   на всю страницу здесь был бы шумом (см. DESIGN, «Медиа в ленте»). */
.media-note { font-size: var(--fs-sm); color: var(--c-text-2); }
.bubble.out .media-note { color: var(--c-on-bubble-out-2); }
.media-retry { margin-top: var(--sp-2); }

/* --- служебное вложение в пузыре -----------------------------------------

   Звонок, опрос, геометка, карточка контакта. Файла у них нет вовсе, поэтому
   и плашки файла с кнопкой скачивания здесь нет — каждый вид показан по
   смыслу. Обрамление то же, что у плашки файла и карточки ссылки: вдавленная
   поверхность во входящем пузыре, ровный фон в исходящем — на акцентном
   градиенте неоморфная выемка превращается в грязь. */
.msg-service {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
  font-size: var(--fs-sm);
}
.bubble.out .msg-service {
  background: var(--c-bubble-out-panel);
  box-shadow: none;
}
.msg-service-art { flex: none; color: var(--c-icon); }
.bubble.out .msg-service-art { color: var(--c-on-bubble-out-2); }
.msg-service-body { min-width: 0; display: grid; gap: 2px; }

/* Знак стикера, у которого не досталось картинки: сам эмодзи крупнее подписи,
   чтобы строка читалась как стикер, а не как ещё одна метка. */
.msg-sticker-emoji { font-size: var(--fs-lg); line-height: 1; }

/* Опрос: вопрос и варианты. Без счётчиков и без кнопок — проголосовать
   отсюда нельзя ни в одном из мессенджеров, а проценты рядом с ненажимаемыми
   строками были бы обманом. */
.msg-poll {
  margin-bottom: var(--sp-1);
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
}
.bubble.out .msg-poll {
  background: var(--c-bubble-out-panel);
  box-shadow: none;
}
.msg-poll-head { color: var(--c-text-3); }
.bubble.out .msg-poll-head { color: var(--c-on-bubble-out-2); }
.msg-poll-question {
  margin-top: 2px;
  font-weight: var(--fw-semibold);
  overflow-wrap: anywhere;
}
.msg-poll-options {
  margin: var(--sp-1) 0 0;
  padding-left: var(--sp-3);
  font-size: var(--fs-sm);
}
.msg-poll-options li { margin-top: 2px; }

/* --- карточка ссылки в пузыре -------------------------------------------

   Мессенджер присылает превью ссылки вложением, но файла в нём нет: есть
   заголовок, описание, имя сайта и картинка. Карточка стоит ПОД текстом (сам
   адрес остаётся в тексте, второй раз его не показываем) и целиком является
   ссылкой — попасть в неё пальцем проще, чем в строку текста.

   Обрамление то же, что у цитаты и плашки файла: вдавленная поверхность во
   входящем пузыре, затемнение поверх акцента в исходящем — на цветном фоне
   неоморфной выемки не видно. Своего объёма карточке не даём: она внутри
   пузыря, и второй выпуклостью спорила бы с ним. */
.link-card {
  display: flex;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
  color: inherit;
  text-decoration: none;
}
.bubble.out .link-card {
  background: var(--c-bubble-out-panel);
  box-shadow: none;
}
/* Подчёркивание — только под заголовком и только под курсором: рамкой
   наведение не помечаем, а красить всю карточку значило бы завести второе
   акцентное пятно в пузыре. */
.link-card:hover .link-title,
.link-card:focus-visible .link-title { text-decoration: underline; }

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

/* Имя сайта — акцентом, как автор цитаты: по нему видно, куда ведёт ссылка,
   ещё до чтения заголовка. Берём цвет для ТЕКСТА, а не заливочный алый: на
   тёмном фоне он не дотягивает по контрасту (см. 2.3). */
.link-site {
  font-size: var(--fs-sm);
  color: var(--c-accent-text);
}
.bubble.out .link-site { color: var(--c-on-bubble-out); }

/* Заголовок в две строки, описание в три: карточка это подсказка, а не
   вторая статья в ленте. Дальше — многоточие. */
.link-title,
.link-desc {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.link-title {
  -webkit-line-clamp: 2;
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
}
.link-desc {
  -webkit-line-clamp: 3;
  font-size: var(--fs-sm);
  color: var(--c-text-2);
}
.bubble.out .link-desc { color: var(--c-on-bubble-out-2); }

/* Окно картинки: ширина числом, а не процентом, — ровно по той же причине,
   что у превью вложения (пузырь сжимается по содержимому, и процент в этот
   расчёт не входит). Заглушка занимает то же окно, поэтому приехавшая
   картинка не двигает ленту. */
.link-shot {
  width: var(--size-media-w);
  max-width: 100%;
  height: var(--size-link-shot-h);
  margin-top: var(--sp-1);
  border-radius: var(--r-sm);
  overflow: hidden;
  line-height: 0;
}
.link-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.skeleton.link-hold {
  width: 100%;
  height: 100%;
}

/* --- выбранный файл в поле ввода ----------------------------------------

   Плашка над строкой ввода собрана из той же .media-file, что и вложение в
   пузыре: человек видит ровно то, что уйдёт. Отличается один слот — вместо
   значка в нём миниатюра картинки. Своего объёма плашке не даём, вдавленность
   она берёт от .media-file. */
.composer-attach { flex: none; }

/* Плашки над плавающей пилюлей — та же панель и та же тень: полосы под ними
   больше нет, и без своей заливки плашка висела бы прямо на обоях. Выбранный
   файл перебивает вдавленность .media-file: внутри пузыря она к месту, над
   обоями — нет. */
.composer > .composer-reply,
.composer > .composer-attach,
.composer > .composer-note {
  padding: var(--sp-2);
  border-radius: var(--r-card);
  background: var(--c-surface);
  box-shadow: var(--neo-out), var(--shadow-bubble);
}
.attach-art img {
  display: block;
  width: var(--size-attach);
  height: var(--size-attach);
  border-radius: var(--r-sm);
  object-fit: cover;
}

/* Область, в которую тащат файл. Кольцо внутрь, а не заливка: алое пятно во
   всю переписку спорило бы с кнопкой отправки, а кольцо — та же подсказка,
   что и у поля в фокусе. */
.drop-active {
  outline: var(--focus-ring);
  outline-offset: calc(-1 * var(--focus-offset));
}

/* ======================================================================
   Счётчики, метки, аватары
   ====================================================================== */

/* Круглый счётчик непрочитанного. */
.badge-count {
  display: inline-grid;
  place-items: center;
  min-width: var(--size-badge);
  height: var(--size-badge);
  padding: 0 var(--sp-2);
  border-radius: var(--r-pill);
  background-image: var(--grad-accent);
  color: var(--c-on-accent);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: 1;
  box-shadow: var(--shadow-accent);
}
.badge-count.mute {
  background-image: none;
  background: var(--c-bg);
  color: var(--c-text-2);
  box-shadow: var(--neo-out-sm);
}

/* Текстовая метка канала/статуса. */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--c-text-2);
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
}
.tag.ok { color: var(--c-ok); background: var(--c-ok-soft); box-shadow: none; }
.tag.warn { color: var(--c-warn); background: var(--c-warn-soft); box-shadow: none; }
.tag.error { color: var(--c-error); background: var(--c-error-soft); box-shadow: none; }
.tag.accent { color: var(--c-accent-text); background: var(--c-accent-soft); box-shadow: none; }

/* Аватар: круг с выпуклой оправой; внутри img или инициалы. */
.avatar {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--size-avatar);
  height: var(--size-avatar);
  border-radius: var(--r-circle);
  overflow: hidden;
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out-sm);
  color: var(--c-text-2);
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  user-select: none;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Фотография ложится ПОВЕРХ инициалов, а не рядом с ними: инициалы остаются
   под ней запасом — пока фото едет, если его нет вовсе и если оно оказалось
   битым. Без накладки оба содержимого встали бы в две строки сетки и сплющили
   кружок. Кольцо мессенджера рисует ::after, оно поверх картинки. */
.avatar > img { position: absolute; inset: 0; }
.avatar.sm { width: var(--size-avatar-sm); height: var(--size-avatar-sm); font-size: var(--fs-sm); }
.avatar.lg { width: var(--size-avatar-lg); height: var(--size-avatar-lg); font-size: var(--fs-xl); }

/* Оттенок аватара. Номер приходит из разметки (`tone-0` … `tone-5`) и всегда
   один и тот же у одного собеседника, поэтому список не «мигает» цветом при
   обновлении. Инициалы поверх заливки светлые: своего тона у них больше нет,
   иначе на глиняном и моховом фоне они тонули бы. */
.avatar[class*="tone-"] { color: var(--c-text); }
.avatar.tone-0 { background-image: var(--grad-tone-0); }
.avatar.tone-1 { background-image: var(--grad-tone-1); }
.avatar.tone-2 { background-image: var(--grad-tone-2); }
.avatar.tone-3 { background-image: var(--grad-tone-3); }
.avatar.tone-4 { background-image: var(--grad-tone-4); }
.avatar.tone-5 { background-image: var(--grad-tone-5); }

/* Фирменное кольцо мессенджера: вокруг аватара переписки и по краю включённого
   таба фильтра. Рядом в списке стоят Telegram и MAX, и по одному названию их не
   различить. Кольцо работает в паре с подписью в углу (.brand-tag): кольцо
   ловится боковым зрением при беге по списку, подпись отвечает, какой именно.

   Кольцо — накладка, а не рамка: border умеет один цвет, box-shadow не умеет
   градиент, а border ещё и сдвинул бы содержимое внутрь. Накладка лежит по
   самому краю поверх заливки, поэтому круг остаётся кругом и кольцо одинаково
   работает с инициалами, с иконкой группы и с фотографией. Оттенок приходит
   классом .brand-telegram / .brand-max через --grad-brand.

   Середину вырезает маска, поэтому правило закрыто @supports: браузер, который
   маску не умеет, залил бы аватар сплошным градиентом вместо кольца. Без
   кольца интерфейс работает как прежде — мессенджер остаётся в подписи. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .brand-ring::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: var(--ring-brand-w);
    background: var(--grad-brand, none);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    pointer-events: none;
  }
}
/* Оттенок мессенджера. Класс висит на ОБЁРТКЕ кружка, а не на нём самом:
   отсюда переменные достаются по наследованию и кольцу, и подписи в углу. */
.brand-telegram {
  --grad-brand: var(--grad-brand-tg);
  --c-brand: var(--c-brand-tg);
  --c-brand-deep: var(--c-brand-tg-deep);
  --c-brand-neon: var(--c-brand-tg-neon);
  --glow-brand: var(--glow-brand-tg);
}
.brand-max {
  --grad-brand: var(--grad-brand-max);
  --c-brand: var(--c-brand-max);
  --c-brand-deep: var(--c-brand-max-deep);
  --c-brand-neon: var(--c-brand-max-neon);
  --glow-brand: var(--glow-brand-max);
}
.brand-whatsapp {
  --grad-brand: var(--grad-brand-wa);
  --c-brand: var(--c-brand-wa);
  --c-brand-deep: var(--c-brand-wa-deep);
  --c-brand-neon: var(--c-brand-wa-neon);
  --glow-brand: var(--glow-brand-wa);
}

/* Неоновая подпись мессенджера в углу кружка — «TG» и «MAX». Стоит ВМЕСТЕ с
   кольцом: кольцо ловится боковым зрением, подпись отвечает, какой именно.

   Живёт в обёртке, а не внутри кружка: у того `overflow: hidden` ради
   фотографии, и подпись срезало бы по кругу. Тем же приёмом сделан знак
   состояния в списке аккаунтов.

   Своя тёмная подложка обязательна: подпись ложится поверх кружка, а он бывает
   и фотографией, и градиентом — на светлом снимке неон без подложки пропал бы.
   Наружу вынесена на два пикселя, чтобы закрывать собой поменьше лица. */
.brand-tag {
  position: absolute;
  left: -2px;
  bottom: -2px;
  padding: 1px 4px;
  border-radius: var(--r-pill);
  background: var(--c-bg);
  border: 1px solid var(--c-brand, transparent);
  color: var(--c-brand-neon, var(--c-text-2));
  font-size: var(--fs-tag);
  font-weight: var(--fw-bold);
  line-height: 1;
  letter-spacing: .04em;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: var(--glow-brand, none);
  text-shadow: var(--glow-brand, none);
}

/* Точка «в сети» — отдельным элементом рядом, чтобы не резалась overflow. */
.avatar-dot {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 10px;
  height: 10px;
  border-radius: var(--r-circle);
  background: var(--c-ok);
  box-shadow: var(--ring-dot);
}

/* ======================================================================
   Иконки (только инлайновый SVG)
   ====================================================================== */

.icon {
  width: var(--icon-md);
  height: var(--icon-md);
  flex: none;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon.xs { width: var(--icon-xs); height: var(--icon-xs); }
.icon.sm { width: var(--icon-sm); height: var(--icon-sm); }
.icon.lg { width: var(--icon-lg); height: var(--icon-lg); }
.icon.solid { fill: currentColor; stroke: none; }

/* Состояние — цветом самого значка, а не отдельной меткой рядом: зелёная
   галочка «подключён», красный минус «не подключён», оранжевые часики
   «ждёт подтверждения». Цвет тут не единственный носитель смысла — формы у
   трёх состояний разные, а состояние словами называет подсказка на обёртке
   значка. Обводка берёт currentColor, поэтому хватает одного свойства. */
.icon.ok { color: var(--c-ok); }
.icon.warn { color: var(--c-warn); }
.icon.error { color: var(--c-error); }

/* ======================================================================
   Строка списка
   ====================================================================== */

/* Строка переписки: плоская, объём появляется только под курсором.
   Выбранная — вдавленная, как нажатая клавиша. */
.row-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  text-align: left;
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}
.row-item:hover { background-color: var(--c-row-hover); }
.row-item.active {
  background: var(--c-bg);
  box-shadow: var(--neo-in-sm);
}
.row-item.unread .row-title { font-weight: var(--fw-semibold); color: var(--c-text); }

.row-body { flex: 1; min-width: 0; }
.row-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-sub {
  margin-top: 2px;
  font-size: var(--fs-sm);
  color: var(--c-text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-1);
  flex: none;
}

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

.sheet-back {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  background: var(--c-overlay);
  backdrop-filter: blur(6px);
  /* Тот же срок, что у самого окна: подложка, проявившаяся раньше окна на
     треть секунды, читается отдельной вспышкой. */
  animation: fade-in var(--dur-slow) var(--ease-out);
}

.sheet {
  width: 100%;
  max-width: 32rem;
  max-height: 85vh;
  overflow-y: auto;
  padding: var(--sp-6);
  border-radius: var(--r-card);
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out-lg), var(--shadow-modal);
  animation: rise-in var(--dur-slow) var(--ease-spring);
}

/* Окно уже на экране, идёт перерисовка, а не открытие. Разметка окна
   собирается заново на каждый ответ сервера, и без этого сброса подложка и
   само окно каждый раз проигрывали бы появление: гасли до нуля и всплывали
   снова — то самое «подмигивание». Класс ставит render(). */
.sheet-back.settled,
.sheet-back.settled .sheet { animation: none; }

/* Окно УХОДИТ. Пара к появлению: подложка гаснет, само окно оседает вниз и
   слегка уменьшается — то же движение, что при всплытии, только назад и без
   пружины (возвращаться с отскоком незачем).

   Узел в этот момент уже вынесен из #app в body (см. closeModal): перерисовка
   приложения его не тронет, и уход доигрывается целиком. Кликов он не ловит —
   под ним уже работающее приложение. */
.sheet-back.leaving {
  pointer-events: none;
  animation: fade-out var(--dur-base) var(--ease-in-out) both;
}
.sheet-back.leaving .sheet {
  animation: sink-out var(--dur-base) var(--ease-in-out) both;
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}

/* ======================================================================
   Просмотр картинки
   ====================================================================== */

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

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

   Наведение и нажатие меняют не заливку, а сам глиф: заливке здесь меняться
   некуда — светлее она спорит с фотографией, темнее не отличается от себя. */
.viewer-close {
  background: var(--c-overlay);
  color: var(--c-on-accent);
}
.viewer-close:hover,
.viewer-close:active {
  background: var(--c-overlay);
  color: #ffffff;
}

/* ======================================================================
   Контекстное меню
   ====================================================================== */

/* Ловчий слой на весь экран: он же ловит нажатие мимо меню. Своего вида у
   него нет — меню не гасит страницу, как модальное окно, оно всего лишь
   короткий список действий над тем, на что нажали. */
.menu-back {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  /* Прокрутка пальцем по слою не должна двигать то, что под ним: меню
     относится к конкретной строке, и уезжать от неё ему нельзя. */
  touch-action: none;
}

/* Пока палец держит строку или сообщение (и пока открытое этим удержанием
   меню на экране), выделять текст нельзя: Safari на iOS иначе подсвечивает
   слово и поднимает свою панель «Скопировать / Найти выбранное» поверх нашего
   меню. Класс живёт ровно на время удержания — обычным жестом кусок сообщения
   выделяется как прежде. Поля ввода не трогаем: набор текста в них не
   прекращается оттого, что кто-то держит палец на ленте. */
/* Ставим запрет на ДВА контейнера, а не на каждый элемент страницы.

   Было `body.holding :not(input):not(textarea)` — селектор, который совпадает
   со всем документом, поэтому появление и снятие класса заставляло браузер
   пересчитывать стили всех двух с половиной тысяч узлов. Класс снимается и
   ставится на каждом касании списка (палец коснулся — поехал), то есть это
   был двойной пересчёт всего документа в начале каждого свайпа.

   Здесь хватает двух контейнеров: `user-select` НАСЛЕДУЕТСЯ, и запрет сам
   доходит до строк и сообщений внутри. Поля ввода в эти контейнеры не входят
   (строка поиска и композер лежат рядом), поэтому исключать их больше не
   нужно — набор текста запрет не задевает. */
body.holding .side-list,
body.holding .feed {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Плавающая панель: та же выпуклая поверхность, что у окна, только мельче.
   Координаты ставит разметка — они считаются от точки нажатия. Фокус она
   принимает на себя (tabindex=-1), но кольца фокуса у неё нет: кольцо —
   примета того, по чему сейчас ходят с клавиатуры, а ходят по пунктам. */
.menu {
  position: fixed;
  min-width: 12rem;
  max-width: min(20rem, calc(100vw - var(--sp-5)));
  max-height: calc(100vh - var(--sp-5));
  overflow-y: auto;
  padding: var(--sp-2);
  border-radius: var(--r-card);
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out-lg), var(--shadow-modal);
  animation: rise-in var(--dur-base) var(--ease-out);
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--c-text);
  font: inherit;
  font-size: var(--fs-md);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.menu:focus,
.menu:focus-visible { outline: none; }

.menu-item .icon { color: var(--c-icon); }
.menu-item:hover,
.menu-item:focus-visible { background-color: var(--c-row-hover); }
.menu-item:hover .icon,
.menu-item:focus-visible .icon { color: var(--c-icon-strong); }
/* Под пальцем и под курсором — подсветка фона, и только она. Ни рамки, ни
   объёма: пункт меню не кнопка-пилюля, а строка списка действий, и вдавливать
   в панели нечего. Кольцо фокуса остаётся там, где оно нужно, — на обходе с
   клавиатуры (`:focus-visible`, общее правило темы). */
.menu-item:active { background-color: var(--c-accent-soft); }
.menu-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Пункт с подписью: название и под ним строка помельче — до какого времени
   переписка молчит. Столбец забирает всю ширину пункта, само название внутри
   него больше не тянется (иначе выросла бы высота строки). */
.menu-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.menu-text .menu-label { flex: none; }

.menu-note {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--c-text-3);
  font-size: var(--fs-sm);
}

/* ======================================================================
   Панель эмодзи
   ====================================================================== */

/* Стоит на том же слое, что и меню, и берёт от него поверхность с тенью —
   отсюда `menu` вторым классом. Своё здесь только содержимое: сетка знаков
   вместо строк-пунктов, и потолок высоты, иначе слой не сможет поднять её
   над кнопкой. */
.emoji-panel {
  width: min(21rem, calc(100vw - var(--sp-5)));
  max-height: min(50vh, 22rem);
}

.emoji-title {
  padding: var(--sp-2) var(--sp-1) var(--sp-1);
  color: var(--c-text-3);
}
.emoji-title:first-child { padding-top: 0; }

.emoji-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
}

/* Сам знак рисует шрифт системы — свой размер он берёт отсюда, а цвет ему не
   задаём вовсе: у цветных эмодзи он свой. */
.emoji-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.emoji-btn:hover,
.emoji-btn:focus-visible { background-color: var(--c-row-hover); }
.emoji-btn:active { background-color: var(--c-accent-soft); }

/* ======================================================================
   Пустое состояние
   ====================================================================== */

.empty-state {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--sp-3);
  min-height: 100%;
  padding: var(--sp-8) var(--sp-6);
  text-align: center;
}
.empty-state .empty-art {
  display: grid;
  place-items: center;
  width: var(--size-avatar-lg);
  height: var(--size-avatar-lg);
  margin-bottom: var(--sp-2);
  border-radius: var(--r-circle);
  background: var(--c-bg);
  box-shadow: var(--neo-in);
  color: var(--c-text-3);
}
.empty-state .empty-title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--c-text); }
.empty-state .empty-text { font-size: var(--fs-sm); color: var(--c-text-2); max-width: 34ch; }

/* ======================================================================
   Скелетоны загрузки
   ====================================================================== */

.skeleton {
  background: var(--c-surface-raised);
  background-image: var(--grad-skeleton);
  background-size: 220% 100%;
  border-radius: var(--r-sm);
  animation: shimmer 1.4s linear infinite;
  color: transparent;
  user-select: none;
  pointer-events: none;
}
.skeleton.line { height: 12px; width: 100%; }
.skeleton.line.short { width: 42%; }
.skeleton.line.mid { width: 68%; }
.skeleton.circle { border-radius: var(--r-circle); width: var(--size-avatar); height: var(--size-avatar); }
.skeleton.bubble { height: 44px; border-radius: var(--r-bubble); }
/* Заглушка длинного сообщения: в ленте их столько же, сколько коротких, и без
   разной высоты место под будущие сообщения считается неверно. */
.skeleton.bubble.tall { height: 72px; }

/* Заглушка на месте вложения, пока файл едет. Размеры РОВНО те же, что у
   того, что встанет на её место: картинка — окно превью, голосовое — полоса в
   высоту проигрывателя. Подстановка не двигает ленту вовсе, а не «почти». */
.skeleton.media-hold {
  width: var(--size-media-w);
  max-width: 100%;
  height: var(--size-media-h);
  /* Уже виденная картинка приходит из кэша браузера мгновенно, и мелькнувшая
     на миг заглушка читается как «качается заново». Задержка та же, что у
     заглушек списка и ленты. */
  animation:
    shimmer 1.4s linear infinite,
    fade-in var(--dur-base) var(--ease-out) var(--delay-skeleton) both;
}
/* Размытый предпросмотр на месте картинки, пока едет настоящая.
   Геометрия РОВНО та же, что у самой картинки и у мигающей заглушки: подстановка
   не должна двигать ленту ни на пиксель. Предпросмотр крошечный — растянутый до
   размера окна превью он и так мутный, и лёгкое размытие лишь убирает у него
   квадраты. Появляется сразу, без задержки: он не «ожидание», он уже картинка. */
.media-blur {
  width: var(--size-media-w);
  max-width: 100%;
  height: var(--size-media-h);
  border-radius: var(--r-sm);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  filter: blur(6px);
  /* Размытие тянет края внутрь и оставляет светлую кайму — обрезаем её. */
  transform: scale(1.06);
  overflow: hidden;
}

.skeleton.media-hold.audio {
  height: var(--size-control);
  border-radius: var(--r-pill);
}

/* Заглушка внутри своего пузыря. Общий градиент собран из тёмных оттенков
   поверхности: на алом фоне он темнеет пятном, и движения в нём не разглядеть.
   Здесь тот же приём, но светом. */
.bubble.out .skeleton {
  background: var(--c-skeleton-on-bubble-out);
  background-image: var(--grad-skeleton-on-bubble-out);
}

/* ======================================================================
   Тост
   ====================================================================== */

/* Стек стоит поверх всего и во всю свою ширину, поэтому нажатия он не ловит:
   иначе полоса над полем ввода на шесть секунд переставала бы нажиматься —
   и кнопка отправки под ней тоже. Ловят их только сами тосты: нажатие на
   тост его снимает. */
.toast-stack {
  position: fixed;
  left: 50%;
  bottom: calc(var(--sp-6) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(28rem, calc(100vw - var(--sp-8)));
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out), var(--shadow-modal);
  font-size: var(--fs-sm);
  color: var(--c-text);
  animation: rise-in var(--dur-base) var(--ease-out);
  /* Сам тост нажатия ловит — им он и снимается досрочно. */
  pointer-events: auto;
  cursor: pointer;
}
/* Текст по центру: тост — короткая строка на всю ширину полосы, и прижатая
   влево она читается как начало списка, а не как сообщение. */
.toast { justify-content: center; text-align: center; }
.toast.error { color: var(--c-error); }
/* Удачный исход не красим: зелёная строка спорит с алым акцентом продукта, а
   сообщать об успехе цветом незачем — человек только что сам это сделал. */
.toast.ok { color: var(--c-text); }
.toast.warn { color: var(--c-warn); }

/* ======================================================================
   Подсказка установки
   ====================================================================== */

/* Полоса внизу экрана: как поставить приложение на домашний экран айфона.
   Показывается один раз и только там, где кнопки установки нет вовсе, — см.
   4.15. Собрана как тост, но сама не гаснет: это не сообщение о случившемся,
   а объяснение, которое надо успеть прочитать. Радиус карточный, а не
   пилюля: текст занимает две строки, и пилюля на них разъезжается. */
.install-hint {
  position: fixed;
  left: 50%;
  bottom: calc(var(--sp-6) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: min(28rem, calc(100vw - var(--sp-8)));
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-card);
  background: var(--c-bg);
  background-image: var(--grad-surface);
  box-shadow: var(--neo-out), var(--shadow-modal);
  animation: rise-in var(--dur-base) var(--ease-out);
}

.install-hint-text {
  min-width: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--c-text-2);
}

/* Названия системных кнопок — то, что человек ищет глазами на своём экране,
   поэтому они выделены светлотой, а не вторым цветом. */
.install-hint-text b {
  color: var(--c-text);
  font-weight: var(--fw-semibold);
}

.install-hint-close { flex: none; }

/* ======================================================================
   Индикатор набора и загрузки
   ====================================================================== */

.typing { display: inline-flex; align-items: center; gap: 4px; }

/* Строка с отметкой набора — акцентом, а не вторым уровнем текста: это
   единственное место в списке, которое меняется само по себе, и взгляд должен
   цеплять его без поиска. Точки берут тот же цвет через currentColor, иначе
   они остались бы серыми посреди акцентной строки. */
.typing-line { color: var(--c-accent-text); }
.typing-line .typing i { background: currentColor; }
.typing i {
  width: 5px;
  height: 5px;
  border-radius: var(--r-circle);
  background: var(--c-text-3);
  animation: typing-bounce 1.2s var(--ease-in-out) infinite;
}
.typing i:nth-child(2) { animation-delay: .15s; }
.typing i:nth-child(3) { animation-delay: .3s; }

.spinner {
  width: var(--icon-md);
  height: var(--icon-md);
  border-radius: var(--r-circle);
  border: 2px solid var(--c-hairline);
  border-top-color: var(--c-accent-text);
  animation: spin .8s linear infinite;
}

/* ======================================================================
   Анимации
   ====================================================================== */

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes rise-in {
  from { opacity: 0; transform: translateY(12px) scale(.98); }
  to { opacity: 1; transform: none; }
}
@keyframes sink-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(8px) scale(.98); }
}
@keyframes shimmer {
  from { background-position: 140% 0; }
  to { background-position: -40% 0; }
}
@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: .5; }
  30% { transform: translateY(-4px); opacity: 1; }
}
@keyframes spin { to { transform: rotate(1turn); } }

/* Пользователь попросил меньше движения — оставляем только смену состояний. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ======================================================================
   Мелочи раскладки, нужные всем экранам
   ====================================================================== */

.hstack { display: flex; align-items: center; gap: var(--sp-3); }
.vstack { display: flex; flex-direction: column; gap: var(--sp-3); }
.spacer { flex: 1; min-width: 0; }
.safe-top { padding-top: env(safe-area-inset-top, 0px); }
.safe-bottom { padding-bottom: env(safe-area-inset-bottom, 0px); }
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
