  /* Светлая фиолетовая тема HeartChat — по прямому запросу пользователя
     ("Тоня очень любит фиолетовый светлый цвет, использовать по
     максимуму", 02.09.2026). Было — тёмная тема с синим акцентом
     (Herald Chat), полностью заменена: светлый лавандовый фон,
     насыщенный фиолетовый — акцент/кнопки/свои сообщения, светло-
     фиолетовые поверхности (чужие сообщения, чипы, разделители). Текст
     — тёмно-баклажановый (не чёрный), чтобы всё оставалось в одной
     фиолетовой гамме, а не просто "фиолетовые кнопки на сером". */
  :root{
    --bg:#F6F1FD; --surface:#FFFFFF; --surface2:#EDE1FB; --border:#DCC9F7;
    --primary:#8B5CF6; --primary-light:#EDE1FB; --text:#2C1B47; --muted:#8577A3;
    --error:#E15B64; --mine:#9B7AEF;
  }
  *{box-sizing:border-box;}
  html, body{height:100%;}
  body{margin:0; font-family:-apple-system,"Segoe UI",Arial,sans-serif; background:var(--bg); color:var(--text);
    display:flex; flex-direction:column; overflow:hidden;}
  /* overflow:hidden тут намеренно — скроллиться должен только .content/.chat-scroll
     внутри конкретного экрана, а не вся страница целиком (иначе шапка/композер
     уезжают за пределы видимой области при длинной истории). */
  #app{flex:1; display:flex; flex-direction:column; max-width:720px; width:100%; margin:0 auto;
    min-height:0; /* без этого flex-item не сжимается меньше контента, и overflow у детей не работает */
    overflow:hidden;}
  /* Версия приложения — прямой запрос 02.09.2026: "выводи в настройках,
     в остальных местах удали" — плавающий бейдж по всему приложению
     убран целиком (был сначала в правом нижнем углу, потом в левом —
     оба раза мешал/путал), версия теперь ТОЛЬКО текстом в самих
     настройках (см. loadAdminSettings/openOwnProfileModal). */

  /* position:relative + z-index — прямой запрос 02.09.2026: "шапка
     закреплённая не всегда поверх чата, пузырьки с аудио вылезают поверх
     неё". Причина: .topbar — обычный элемент flex-колонки БЕЗ своего
     z-index, а pinTopbarToVisualViewport() двигает её через CSS
     transform при появлении клавиатуры на iOS (компенсация панорамирования
     visualViewport) — transform смещает её ВИЗУАЛЬНО, не освобождая место
     в раскладке, и без явного z-index более поздние соседи по DOM
     (сообщения в .chat-scroll) красятся поверх неё, если экраны
     физически перекрылись. Даём шапке собственный слой поверх ленты. */
  .topbar{background:var(--surface); border-bottom:1px solid var(--border); padding:12px 16px;
    display:flex; align-items:center; gap:10px; flex-shrink:0; position:relative; z-index:5;}
  .topbar h1{font-size:16px; margin:0; font-weight:600; flex:1;}
  .topbar .sub{font-size:11.5px; color:var(--muted); margin-top:2px;}
  /* Шапка ЧАТА — по-настоящему прибита к верху экрана (position:fixed),
     не просто визуально сдвинута transform'ом — прямой запрос
     02.09.2026: "за Мамой Светой сверху видно все сообщения, надо
     сделать шапку до самого верха, растягивать". Порт решения ПЦ Спорт
     (.header-sticky{position:fixed}) — раньше шапка была обычным
     элементом flex-колонки: когда iOS Safari панорамирует страницу при
     появлении клавиатуры, под "визуально сдвинутой" transform'ом шапкой
     оказывалась видна лента сообщений, которая была ВЫШЕ текущей
     позиции скролла. .chat-body ниже отступает под неё ровно на
     измеренную высоту (--чат height через JS, см. syncChatHeaderHeight),
     не константой — у не всех каналов есть подзаголовок-description. */
  .chat-topbar{position:fixed; top:0; left:0; right:0; max-width:720px; margin:0 auto; z-index:30;}
  .chat-body{display:flex; flex-direction:column; flex:1; min-height:0; overflow:hidden;}
  /* "печатает…" — перенос фичи из ПЦ Спорт (прямой запрос 02.09.2026):
     три точки прыгают по очереди со сдвигом фазы, вместо статичного
     текста "•••". */
  .topnav-typing{display:flex; align-items:center; gap:4px; font-size:11.5px; color:var(--primary); font-weight:600; flex-shrink:0;}
  /* КРИТИЧНО: без этого правила точки были видны ВСЕГДА, даже когда
     никто не печатает — атрибут [hidden] (UA-стиль, display:none) и
     класс .topnav-typing{display:flex} имеют РАВНУЮ специфичность,
     класс из авторского <style> побеждает UA-дефолт. Тот же класс
     багов уже встречался в CardioRehub (см. память проекта) —
     явный [hidden]-селектор нужен для КАЖДОГО элемента, где display
     переключается через атрибут hidden, а не только через классы. */
  .topnav-typing[hidden]{display:none;}
  .topnav-typing i{width:4px; height:4px; border-radius:50%; background:currentColor; display:inline-block; animation:typingDotBounce 1s infinite;}
  .topnav-typing i:nth-child(2){animation-delay:.15s;}
  .topnav-typing i:nth-child(3){animation-delay:.3s;}
  @keyframes typingDotBounce{0%,60%,100%{transform:translateY(0);} 30%{transform:translateY(-3px);}}
  .topbar button.icon{background:none; border:none; color:var(--text); font-size:18px; cursor:pointer; padding:4px 6px; border-radius:8px;}
  .topbar button.icon:hover{background:var(--surface2);}

  /* скролл остаётся рабочим (колесо мыши/тачпад/клавиатура/тач), просто
     без самой полосы — так выглядит аккуратнее и меньше отвлекает. */
  .content, .chat-scroll{scrollbar-width:none; -ms-overflow-style:none;}
  .content::-webkit-scrollbar, .chat-scroll::-webkit-scrollbar{display:none; width:0; height:0;}
  .content{flex:1; min-height:0; overflow-y:auto; padding:12px 16px;}

  input, textarea, select{font-family:inherit; font-size:14px; border:1px solid var(--border);
    border-radius:8px; padding:9px 11px; width:100%; background:var(--surface2); color:var(--text);}
  input:focus, textarea:focus{outline:1.5px solid var(--primary);}
  button.btn{background:var(--primary); color:#fff; border:none; border-radius:8px; padding:10px 16px;
    font-size:14px; cursor:pointer; font-weight:500;}
  button.btn:hover{opacity:.92;}
  button.btn:disabled{opacity:.5; cursor:default;}
  button.btn.ghost{background:transparent; border:1px solid var(--border); color:var(--text);}
  button.btn.danger{background:var(--error); color:#1a0000;}
  button.btn.small{padding:6px 10px; font-size:12.5px;}
  .link-btn{background:none; border:none; color:var(--primary); font-size:13.5px; cursor:pointer; padding:4px;}
  .hint{font-size:12px; color:var(--muted); line-height:1.5;}
  .error{color:var(--error); font-size:12.5px; margin-top:6px;}

  /* центрированные формы (логин/админ-логин) */
  .center-wrap{flex:1; display:flex; align-items:center; justify-content:center; padding:24px;}
  .form-card{width:100%; max-width:340px; text-align:center;}
  /* Логотип (цветочек) на экране входа — картинка вместо эмодзи, крупнее
     (прямой запрос 02.09.2026: "этот цветочек сделай во всех местах
     крупнее"). */
  .app-logo{width:120px; height:120px; margin-bottom:10px; border-radius:26px;
    box-shadow:0 6px 20px rgba(139,92,246,.25);}
  .form-card h2{font-size:19px; margin:0 0 6px 0;}
  .form-card p{font-size:13.5px; color:var(--muted); margin:0 0 20px 0;}
  .code-input{font-size:21px; text-align:center; letter-spacing:4px; text-transform:uppercase;}
  .form-card .field{margin-bottom:12px; text-align:left;}
  .form-card label{display:block; font-size:12px; color:var(--muted); margin-bottom:4px;}

  /* список каналов */
  .channel-row{display:flex; align-items:center; gap:12px; padding:12px 4px; border-bottom:1px solid var(--border); cursor:pointer;}
  .channel-row:hover{background:var(--surface2);}
  .avatar{width:40px; height:40px; border-radius:50%; display:flex; align-items:center; justify-content:center;
    color:#fff; font-weight:700; font-size:15px; flex-shrink:0;}
  .avatar.sm{width:28px; height:28px; font-size:12px;}
  .channel-row .info{flex:1; min-width:0;}
  .channel-row .name{font-size:14.5px; font-weight:600; display:flex; align-items:center; gap:6px;}
  .channel-row .name.unread{font-weight:700;}
  .channel-row .preview{font-size:12.5px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .channel-row .meta{text-align:right; flex-shrink:0;}
  .channel-row .time{font-size:11px; color:var(--muted);}
  .unread-dot{width:9px; height:9px; border-radius:50%; background:var(--primary); display:inline-block;}
  .unread-badge{min-width:18px; height:18px; padding:0 5px; border-radius:9px; background:var(--primary);
    color:#fff; font-size:11px; font-weight:700; display:inline-flex; align-items:center; justify-content:center;}
  .empty-state{text-align:center; color:var(--muted); padding:40px 20px; font-size:13.5px;}
  /* Кнопка "Бронь" у личного канала в списке чатов админки — прямой
     запрос 02.09.2026: "сделай просто кнопкой красивой минималистичной"
     (было текстовой ссылкой в потоке текста), уточнение: "не кнопка с
     календариком, а кнопка с надписью Бронь" — текстовая "таблетка", не
     иконка. В правой колонке строки (.meta), рядом с временем/бейджем
     непрочитанного — не путается с "Профиль"/"Код"/"Отозвать" у самого
     участника (те — в основном блоке строки, слева). */
  .book-btn{margin-top:5px; padding:4px 10px; border-radius:12px; border:1px solid var(--primary);
    background:var(--surface2); color:var(--primary); font-size:11.5px; font-weight:600; cursor:pointer;
    white-space:nowrap;}
  .book-btn:hover{background:var(--primary); color:#fff;}

  /* чат */
  .chat-scroll{flex:1; min-height:0; overflow-y:auto; padding:10px 0; display:flex; flex-direction:column;}
  .load-more{text-align:center; padding:8px;}
  /* баннер закреплённого сообщения — над списком, всегда виден при скролле */
  .pinned-banner{display:flex; align-items:center; gap:8px; padding:8px 16px; margin-bottom:6px;
    background:var(--surface2); border-bottom:1px solid var(--border); cursor:pointer; flex-shrink:0;
    position:relative; z-index:5;}
  .pinned-banner .bar{width:3px; align-self:stretch; background:var(--primary); border-radius:2px; flex-shrink:0;}
  .pinned-banner .body{flex:1; min-width:0;}
  .pinned-banner .label{font-size:10.5px; color:var(--primary); font-weight:600;}
  .pinned-banner .text{font-size:12.5px; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .pinned-banner .unpin{color:var(--muted); font-size:16px; padding:2px 6px; cursor:pointer;}
  /* Плотная вёрстка внутри "пачки" подряд идущих сообщений одного автора
     (см. группировку в drawChat) — минимум зазора между ними; заметный
     отступ появляется только в НАЧАЛЕ новой пачки (.group-start), это и
     визуально отделяет разных отправителей друг от друга. */
  /* разделитель дня — прозрачный пузырёк по центру над первым сообщением
     нового дня, как в телеге ("Сегодня"/"Вчера"/дата). */
  .day-sep{display:flex; justify-content:center; margin:10px 0 6px 0;}
  .day-sep span{background:rgba(127,127,127,.22); color:var(--muted); font-size:11.5px;
    font-weight:600; padding:4px 12px; border-radius:12px;}
  .msg-row{position:relative; display:flex; gap:8px; margin-bottom:1px; padding:0; align-items:flex-end;}
  .msg-row.group-start{margin-top:6px;}
  .msg-row.mine{flex-direction:row-reverse;}
  .msg-col{display:flex; flex-direction:column; max-width:75%; position:relative;}
  .msg-row.mine .msg-col{align-items:flex-end;}
  /* Аватар показывается только у ПОСЛЕДНЕГО сообщения пачки (низ группы,
     как в телеграме) — у остальных сообщений пачки вместо него пустой
     спейсер той же ширины, чтобы пузыри не "съезжали" по горизонтали. */
  .avatar-spacer{width:28px; flex-shrink:0;}
  /* Имя отправителя — теперь ПЕРВОЙ строкой ВНУТРИ пузыря (как в
     телеграме), не отдельным элементом над ним; показывается у своих
     сообщений тоже — чтобы автор видел, как его имя выглядит у других. */
  .msg-sender-inline{font-size:12.5px; font-weight:700; margin-bottom:2px;}
  .bubble{padding:7px 8px 8px 10px; border-radius:14px; font-size:13.5px; line-height:1.35;
    cursor:pointer; user-select:text; min-width:0; position:relative;}
  .bubble.in{background:var(--surface2);}
  .bubble.out{background:var(--mine);}
  .bubble-text{white-space:pre-wrap; word-break:break-word; padding-right:2px;}
  /* время + галочки — «утоплены» в правый нижний угол пузыря, как в
     телеграме: последний span внутри текста, float:right заставляет
     текст обтекать его вместо того, чтобы занимать отдельную строку. */
  .msg-time-inline{float:right; margin:4px 0 -3px 8px; font-size:10.5px; line-height:1;
    white-space:nowrap; display:inline-flex; align-items:center; gap:3px;}
  /* Полупрозрачный БЕЛЫЙ текст был рассчитан на тёмную тему, где ОБА
     пузыря были тёмными — после перехода на светлую фиолетовую (02.09.2026)
     свой пузырь (.out) остался насыщенным фиолетовым, а чужой (.in) стал
     светло-сиреневым: один и тот же rgba(255,255,255,..) на чужом пузыре
     означал бы белый текст на светлом фоне — почти невидимый (жалоба
     пользователя "текст в цитате ответа вообще не видно"). Разводим по
     типу пузыря. */
  .bubble.out .msg-time-inline{color:rgba(255,255,255,.65);}
  .bubble.in .msg-time-inline{color:rgba(44,27,71,.5);}
  /* Галочки — как в телеге: 1 серая = отправлено (ушло с клиента), 2 серые
     = доставлено на сервер (никто ещё не прочитал), 1 синяя+1 серая = часть
     получателей прочитала, 2 синие = прочитали все. Каждая галочка — свой
     span, чтобы красить их независимо (для смешанного "прочитали не все").
     Показываются только на СВОИХ сообщениях (.bubble.out), поэтому им
     достаточно одного варианта под тёмный фиолетовый фон. */
  .msg-time-inline .ticks{font-size:12px; display:inline-flex; gap:0;}
  .msg-time-inline .ticks .tick.blue{color:#BFF0FF;}
  /* Не отправилось — красный кликабельный значок вместо времени/галочек,
     как красный "!" в телеге, нажатие повторяет отправку. */
  .msg-retry-badge{color:var(--error); font-weight:800; cursor:pointer; font-size:13px;}
  .msg-edited-mark{font-size:10px; font-style:italic; margin-right:2px;}
  .bubble.out .msg-edited-mark{color:rgba(255,255,255,.6);}
  .bubble.in .msg-edited-mark{color:rgba(44,27,71,.45);}
  /* цитата отвечаемого сообщения — внутри пузыря, над текстом самого
     ответа. Тоже разведена по типу пузыря — см. комментарий выше про
     .msg-time-inline, та же причина ("текст цитаты не видно"). */
  .reply-quote{border-left:3px solid var(--primary); padding:2px 8px; margin-bottom:5px;
    border-radius:4px; cursor:pointer; overflow:hidden;}
  .bubble.out .reply-quote{background:rgba(255,255,255,.14);}
  .bubble.in .reply-quote{background:rgba(44,27,71,.06);}
  .reply-quote-sender{font-size:11px; font-weight:600;}
  .bubble.out .reply-quote-sender{color:#fff;}
  .bubble.in .reply-quote-sender{color:var(--primary);}
  .reply-quote-text{font-size:12px; overflow:hidden; display:-webkit-box;
    -webkit-line-clamp:1; -webkit-box-orient:vertical; word-break:break-word;}
  .bubble.out .reply-quote-text{color:rgba(255,255,255,.85);}
  .bubble.in .reply-quote-text{color:var(--text); opacity:.75;}
  /* пузырёк "предложить дату встречи" (прямой запрос 02.09.2026) — блок
     под текстом сообщения внутри того же пузыря, статус/действия. */
  .meeting-box{margin-top:6px; padding:8px 10px; border-radius:8px; font-size:12.5px; font-weight:600;}
  .bubble.out .meeting-box{background:rgba(255,255,255,.14); color:#fff;}
  .bubble.in .meeting-box{background:var(--surface2); color:var(--text);}
  .meeting-box.confirmed{background:#dcf5e3; color:#2e9e52;}
  .meeting-box.countered{opacity:.75; font-weight:500;}
  .meeting-box.cancelled{opacity:.7; font-weight:500; text-decoration:line-through;}
  .meeting-box.awaiting-me{background:#fff4d6; color:#8a6300;}
  .meeting-note{margin-bottom:6px;}
  .meeting-actions{display:flex; gap:6px; flex-wrap:wrap;}
  .meeting-actions .btn{font-weight:600;}
  .meeting-box.confirmed .btn.ghost.small{border-color:#2e9e52; color:#2e9e52;}

  /* Бронирование сессии (прямой запрос 02.09.2026, по образцу
     скриншота стороннего сервиса — даты сверху, часовые слоты снизу).
     См. openBookingModal() в app.js. */
  .booking-days{display:flex; gap:6px; overflow-x:auto; padding-bottom:4px; margin-bottom:12px; position:relative;}
  .booking-day{flex-shrink:0; width:52px; padding:8px 4px; border-radius:10px; border:1px solid var(--border);
    background:var(--surface2); cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:2px;}
  .booking-day .wd{font-size:10.5px; color:var(--muted); text-transform:uppercase;}
  .booking-day .dd{font-size:16px; font-weight:700; color:var(--text);}
  .booking-day.active{background:var(--primary); border-color:var(--primary);}
  .booking-day.active .wd, .booking-day.active .dd{color:#fff;}
  .booking-day-calendar{font-size:18px; padding:8px 10px;}
  /* Прямой запрос 02.09.2026: "интервалы 10 и 15 минут каждого часа
     тоже" — слотов стало ~4x больше (104 против 25), без своей
     прокрутки сетка растягивала бы модалку выше экрана. */
  .booking-slots{min-height:60px; max-height:44vh; overflow-y:auto; padding-right:2px;}
  .booking-slot-grid{display:grid; grid-template-columns:repeat(4, 1fr); gap:6px;}
  .booking-slot{padding:8px 2px; border-radius:8px; border:1px solid var(--border); background:var(--surface2);
    color:var(--text); font-size:12.5px; font-weight:600; cursor:pointer;}
  .booking-slot:hover{border-color:var(--primary);}
  .booking-slot.taken{opacity:.4; text-decoration:line-through; cursor:not-allowed; background:var(--bg);}

  /* Полноэкранный просмотрщик фото/видео (прямой запрос 02.09.2026) —
     не системный браузер, свой оверлей поверх всего приложения, со
     свайпом/стрелками между вложениями текущего чата. См. openMediaViewer(). */
  .media-viewer-overlay{position:fixed; inset:0; z-index:500; background:rgba(0,0,0,.92);
    display:flex; align-items:center; justify-content:center;}
  .media-viewer-stage{max-width:94vw; max-height:88vh; display:flex; align-items:center; justify-content:center;}
  .media-viewer-stage img, .media-viewer-stage video{max-width:94vw; max-height:88vh; object-fit:contain; border-radius:4px;}
  .media-viewer-close{position:absolute; top:14px; right:14px; width:38px; height:38px; border-radius:50%;
    border:none; background:rgba(255,255,255,.12); color:#fff; font-size:18px; cursor:pointer;}
  .media-viewer-nav{position:absolute; top:50%; transform:translateY(-50%); width:44px; height:44px;
    border-radius:50%; border:none; background:rgba(255,255,255,.12); color:#fff; font-size:26px;
    cursor:pointer; line-height:1;}
  .media-viewer-nav.prev{left:10px;}
  .media-viewer-nav.next{right:10px;}
  @media (max-width:520px){ .media-viewer-nav{display:none;} } /* на телефоне — только свайп, стрелки лишние */

  /* вложения (фото/видео/файл/голосовое) — прямой запрос 02.09.2026.
     Плейсхолдер, пока не подтянулась presigned-ссылка (см.
     resolveAttachmentFor в app.js). */
  .attach-placeholder{display:flex; align-items:center; gap:6px; padding:14px 10px; border-radius:8px;
    font-size:12.5px; opacity:.7; min-width:120px;}
  .bubble.out .attach-placeholder{background:rgba(255,255,255,.12);}
  .bubble.in .attach-placeholder{background:var(--surface2);}
  /* Прямой запрос 02.09.2026: "предпросмотры фото и видео сделай сильно
     меньше" — было до 320px высотой на всю ширину пузыря, теперь
     компактный тамбнейл (макс. 200×200), полный размер — по тапу
     (см. openMediaViewer). */
  .attach-image, .attach-video{display:block; max-width:200px; max-height:200px; border-radius:10px; margin-bottom:2px;}
  .attach-video{background:#000;}
  /* Кастомный плеер голосового сообщения — прямой запрос 02.09.2026, по
     образцу референс-скриншота Телеграма (круглая кнопка play, "волна"
     из полосок, время). Один общий вид что для своих (bubble.out), что
     для чужих (bubble.in) сообщений — только цвет полосок/кнопки другой
     (наследуется от цвета текста пузыря). */
  .voice-player{display:flex; align-items:center; gap:8px; padding:6px 2px; min-width:200px;}
  .voice-play-btn{flex-shrink:0; width:34px; height:34px; border-radius:50%; border:none; cursor:pointer;
    font-size:13px; display:flex; align-items:center; justify-content:center; padding:0 0 0 2px;}
  .bubble.out .voice-play-btn{background:rgba(255,255,255,.22); color:#fff;}
  .bubble.in .voice-play-btn{background:var(--primary); color:#fff; padding:0;}
  .voice-wave{flex:1; min-width:0; height:24px; display:flex; align-items:center; gap:2px; cursor:pointer;}
  .voice-wave span{flex:1; min-width:1.5px; border-radius:2px; background:currentColor; opacity:.35;}
  .voice-wave span.played{opacity:1;}
  .bubble.out .voice-wave{color:#fff;}
  .bubble.in .voice-wave{color:var(--primary);}
  .voice-meta{flex-shrink:0; display:flex; flex-direction:column; align-items:flex-end; gap:2px;}
  .voice-time{font-size:10.5px; white-space:nowrap; opacity:.8;}
  .voice-transcribe-btn{border:none; background:none; cursor:pointer; font-size:10.5px; font-weight:700;
    padding:0; opacity:.75; text-decoration:underline; text-underline-offset:2px;}
  .bubble.out .voice-transcribe-btn{color:#fff;}
  .bubble.in .voice-transcribe-btn{color:var(--primary);}
  .attach-file{display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:8px; text-decoration:none;}
  .bubble.out .attach-file{background:rgba(255,255,255,.14); color:#fff;}
  .bubble.in .attach-file{background:var(--surface2); color:var(--text);}
  .attach-file-icon{font-size:20px; flex-shrink:0;}
  .attach-file-name{font-size:13px; word-break:break-word;}
  /* фото/видео без подписи — прямой запрос 02.09.2026, дизайн как у
     фото в чате ПЦ Спорт: сама картинка/видео БЕЗ рамки пузыря (фон,
     паддинги, тень пузыря убираются целиком), время+галочки — плашкой
     поверх неё (см. .attach-media-time ниже), а не отдельной строкой. */
  .bubble.media-only{padding:0; background:none !important; box-shadow:none;}
  .attach-media-wrap{position:relative; display:inline-block; line-height:0; cursor:pointer;}
  .attach-media-wrap .attach-image, .attach-media-wrap .attach-video{
    max-width:200px; max-height:200px; width:auto; border-radius:14px; margin-bottom:0;}
  /* Прямой запрос 02.09.2026: "когда присылаешь светлую картинку, время
     в пузырьке не видно — оно сливается" — 50% чёрного было недостаточно
     контрастно на ярких/белых фото; плашка теперь заметно непрозрачнее
     (плюс лёгкая тень для чёткого края поверх любого фона). */
  .attach-media-time{position:absolute; right:7px; bottom:7px; background:rgba(0,0,0,.72); color:#fff;
    font-size:11px; line-height:1; padding:4px 7px; border-radius:10px; display:flex; align-items:center;
    gap:3px; cursor:pointer; box-shadow:0 1px 4px rgba(0,0,0,.35);}
  .attach-media-time .tick{color:rgba(255,255,255,.85);}
  .attach-media-time .tick.blue{color:#8ecdfa;}
  /* время под вложением без подписи, когда рамка пузыря ОСТАЁТСЯ (аудио,
     файл — не фото/видео) — своя строка внизу пузыря, без наплыва на
     содержимое (в отличие от .msg-time-inline, который обычно "утоплен"
     в текст сообщения). */
  .attach-time-row{padding:3px 6px 1px; text-align:right;}
  .bubble.out .attach-time-row .msg-time-inline{color:rgba(255,255,255,.75);}
  .bubble.in .attach-time-row .msg-time-inline{color:var(--muted);}

  /* плашка прогресса загрузки вложения — над полем ввода */
  .upload-progress{padding:6px 16px; border-top:1px solid var(--border); background:var(--surface2);}
  .upload-progress-name{font-size:11.5px; color:var(--muted); margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .upload-progress-bar{height:4px; border-radius:2px; background:var(--border); overflow:hidden;}
  .upload-progress-bar div{height:100%; background:var(--primary); transition:width .15s ease;}

  /* панель записи голосового — заменяет обычный композер целиком
     (см. composerNormal/composerRecording в app.js), включая отсчёт
     "запись начнётся через 3с" перед стартом MediaRecorder. */
  .voice-recorder-bar{align-items:center;}
  .voice-recorder-status{flex:1; display:flex; align-items:center; gap:6px; font-size:13px; color:var(--text); min-width:0;}
  .voice-rec-dot{width:9px; height:9px; border-radius:50%; background:var(--error); flex-shrink:0;
    animation:voice-rec-pulse 1.1s ease-in-out infinite;}
  @keyframes voice-rec-pulse{0%,100%{opacity:1;} 50%{opacity:.25;}}
  /* Живой эквалайзер во время записи — прямой запрос 02.09.2026: "как на
     скриншоте — красиво, в виде эквалайзера" (вместо просто пульсирующей
     точки). Реальные уровни сигнала — см. _startVoiceEqualizer() в app.js. */
  .voice-eq{flex:1; min-width:0; height:22px; display:flex; align-items:center; gap:2px;}
  .voice-eq span{flex:1; min-width:1.5px; height:15%; border-radius:2px; background:var(--primary);
    transition:height .08s ease-out;}
  /* Предпрослушка перед отправкой (прямой запрос 02.09.2026: "как в
     телеграме — можешь послушать перед отправкой") — тот же .voice-player,
     что и у уже отправленных голосовых, просто локальный blob:-URL. */
  .voice-preview-bar{align-items:center;}
  .voice-preview-player{flex:1; min-width:0;}
  .voice-preview-player .voice-player{padding:0;}
  /* .voice-player вне пузыря сообщения (здесь — в композере, не в
     .bubble.in/.bubble.out) — свой набор цветов по умолчанию. */
  .voice-preview-player .voice-play-btn{background:var(--primary); color:#fff;}
  .voice-preview-player .voice-wave{color:var(--primary);}
  .voice-preview-player .voice-time{color:var(--muted);}
  .voice-preview-player .voice-transcribe-btn{color:var(--primary);}
  /* реакции «прижаты» к нижнему краю пузыря — отрицательный margin-top
     заводит их под бабл внахлёст, как чипсы в телеграме. */
  .reactions{display:flex; flex-wrap:wrap; gap:4px; margin-top:-9px; padding:0 2px; position:relative; z-index:2;}
  .reaction-chip{display:flex; align-items:center; gap:3px; padding:2px 7px; border-radius:11px; font-size:11.5px;
    border:2px solid var(--bg); background:var(--surface2); cursor:pointer;}
  .reaction-chip.mine{border-color:var(--bg); background:var(--primary-light);}
  /* тултип "кто поставил реакцию" — по долгому нажатию/наведению на чип */
  .reactors-tooltip{position:fixed; z-index:150; background:var(--surface); border:1px solid var(--border);
    border-radius:8px; padding:6px 10px; font-size:12px; color:var(--text); max-width:220px;
    box-shadow:0 4px 14px rgba(0,0,0,.4); pointer-events:none;}
  /* панель быстрых реакций по наведению мышью — ТОЛЬКО десктоп (hover).
     На тач-устройствах эта панель никогда не показывается сама по себе —
     раньше здесь было исключение для hover:none, из-за которого на
     телефоне реакции+"⋯" висели под каждым сообщением постоянно. Теперь
     на тач-устройствах единственные пути к действиям — зажатие (long
     press, см. touchstart-обработчик ниже) и маленькая кнопка "⋯"
     (.msg-more-btn), которая тоже видна и на десктопе как альтернатива
     наведению/ПКМ. */
  .quick-react-bar{position:absolute; top:-16px; right:8px; display:none; gap:2px; z-index:5;
    background:var(--surface2); border:1px solid var(--border); border-radius:16px; padding:2px 4px;
    box-shadow:0 2px 8px rgba(0,0,0,.35);}
  .msg-row.mine .quick-react-bar{right:auto; left:8px;}
  @media (hover:hover){ .msg-row:hover .quick-react-bar{display:flex;} }
  .quick-react-bar span{cursor:pointer; padding:2px 5px; font-size:14px; border-radius:50%; line-height:1.3;}
  .quick-react-bar span:hover{background:var(--surface);}
  .quick-react-bar .qr-more{font-size:16px; font-weight:700;}
  /* кнопка "⋯" — всегда видимый, дискретный вход в меню действий сообщения
     (на десктопе — дублирует ПКМ/наведение, на тач-устройствах — основной
     явный путь наравне с зажатием). */
  .msg-more-btn{position:absolute; top:2px; right:3px; width:18px; height:18px; border-radius:50%;
    border:none; background:rgba(0,0,0,.18); color:#fff; font-size:11px; font-weight:700;
    line-height:18px; text-align:center; padding:0; cursor:pointer; opacity:.7;}
  .bubble.in .msg-more-btn{background:rgba(255,255,255,.12); color:var(--text);}
  .msg-more-btn:hover{opacity:1;}

  /* контекстное меню по ПКМ — всплывает прямо в точке клика (см.
     openMessageContextMenu), а не общим нижним листом. */
  .ctx-menu{position:fixed; z-index:200; background:var(--surface); border:1px solid var(--border);
    border-radius:10px; padding:6px; box-shadow:0 8px 24px rgba(0,0,0,.5); min-width:200px;}
  .ctx-menu-item{padding:9px 12px; font-size:13.5px; cursor:pointer; border-radius:6px;
    display:flex; align-items:center; gap:8px; white-space:nowrap;}
  .ctx-menu-item:hover{background:var(--surface2);}
  .ctx-menu-emojis{display:flex; flex-wrap:wrap; max-width:260px; gap:2px; padding:4px 2px 8px 2px;
    border-bottom:1px solid var(--border); margin-bottom:4px;}
  .ctx-menu-emojis span{cursor:pointer; padding:5px 7px; font-size:18px; border-radius:50%;}
  .ctx-menu-emojis span:hover{background:var(--surface2);}

  /* Отступ снизу под системный safe-area (домашний индикатор iPhone) —
     прямой запрос 02.09.2026: "обрати внимание как в ПЦ Спорт
     гармонично — есть отступ снизу, а у тебя нет, поле ввода просто в
     самом низу". Порт того же приёма (.chat-bottom{padding-bottom:
     env(safe-area-inset-bottom)} у ПЦ Спорт) — здесь на весь блок
     композера, чтобы reserved-место было физическим (флекс), а не
     наложением, и сообщения не заезжали под него. */
  .composer-wrap{position:relative; flex-shrink:0; padding-bottom:max(6px, env(safe-area-inset-bottom, 0px));}
  .composer{border-top:1px solid var(--border); padding:8px 16px; display:flex; gap:8px; align-items:flex-end;}
  /* тот же класс бага, что уже чинили для индикатора печати (.topnav-typing) —
     у класса .composer и атрибута [hidden] одинаковая специфичность,
     побеждает то, что позже в каскаде, поэтому оба композера (обычный и
     "запись голоса") показывались одновременно, пока не задан явный
     override ниже. */
  .composer[hidden]{display:none;}
  .composer textarea{resize:none; min-height:20px; max-height:120px; line-height:1.4;}
  .composer button{flex-shrink:0;}
  .composer .attach-btn{background:none; border:1px solid var(--border); color:var(--primary); font-size:15px;
    cursor:pointer; padding:0; width:36px; height:36px; border-radius:50%;
    display:inline-flex; align-items:center; justify-content:center;}
  .composer .attach-btn:hover{background:var(--surface2);}
  /* Панель эмодзи для вставки в текст — по образцу ПЦ Спорт: всплывает
     НАД композером (bottom:100%), сетка 8 в ряд, без "часто используемых". */
  .emoji-picker{position:absolute; left:8px; right:8px; bottom:100%; margin-bottom:6px; box-sizing:border-box;
    z-index:56; background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:8px;
    box-shadow:0 -4px 20px rgba(0,0,0,.18); max-height:240px; overflow-y:auto;
    display:grid; grid-template-columns:repeat(8, 1fr); gap:2px;}
  /* КРИТИЧНО, тот же класс бага, что уже чинили у .composer/.topnav-typing
     в этой же сессии: атрибут [hidden] и класс .emoji-picker{display:grid}
     имеют РАВНУЮ специфичность — класс, будучи ниже в каскаде, побеждает,
     и панель остаётся видимой (пустой прямоугольник над полем ввода)
     ДАЖЕ когда JS выставил hidden=true. Отсюда обе жалобы разом: "не
     могу закрыть" (JS честно переключал hidden, просто CSS его
     игнорировал) и "бесполезное пустое поле" (сама пустая сетка, пока
     ничего не выбрано). */
  .emoji-picker[hidden]{display:none;}
  .emoji-picker button{background:transparent; border:0; font-size:22px; line-height:1; padding:6px 0;
    cursor:pointer; border-radius:8px;}
  .emoji-picker button:hover{background:var(--surface2);}
  /* цитата отвечаемого сообщения — над полем ввода, пока не отправлено/отменено */
  .reply-preview{display:flex; align-items:center; gap:8px; padding:6px 16px; border-top:1px solid var(--border); background:var(--surface2);}
  .reply-preview-bar{width:3px; align-self:stretch; background:var(--primary); border-radius:2px; flex-shrink:0;}
  .reply-preview-body{flex:1; min-width:0;}
  .reply-preview-sender{font-size:11.5px; font-weight:600; color:var(--primary);}
  .reply-preview-text{font-size:12.5px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .reply-preview .edit-label{color:#ffb74d;}

  /* меню действий над сообщением */
  .overlay{position:fixed; inset:0; background:rgba(0,0,0,.5); display:none; align-items:flex-end; justify-content:center; z-index:100;}
  .overlay.show{display:flex;}
  .sheet{background:var(--surface); border-radius:16px 16px 0 0; padding:8px 0 20px 0; width:100%; max-width:480px;}
  .sheet .handle{width:36px; height:4px; background:var(--border); border-radius:2px; margin:6px auto 10px auto;}
  .sheet-item{padding:12px 20px; font-size:14px; cursor:pointer; display:flex; align-items:center; gap:10px;}
  .sheet-item:hover{background:var(--surface2);}
  .sheet-item[data-do="delete"], .ctx-menu-item[data-do="delete"]{color:var(--error);}
  .emoji-row{display:flex; justify-content:center; gap:6px; padding:10px 0; flex-wrap:wrap;}
  .emoji-row .e{font-size:28px; padding:8px; cursor:pointer; border-radius:50%;}
  .emoji-row .e:hover{background:var(--surface2);}

  .modal-overlay{position:fixed; inset:0; background:rgba(0,0,0,.5); display:none; align-items:center; justify-content:center; z-index:100;}
  .modal-overlay.show{display:flex;}
  .modal{background:var(--surface); border-radius:14px; padding:22px; width:340px; max-width:90vw;}
  .modal h2{margin:0 0 14px 0; font-size:16px;}
  .modal .field{margin-bottom:12px; text-align:left;}
  .modal .field label{display:block; font-size:12px; color:var(--muted); margin-bottom:4px;}
  /* та же карточка полей, но вне модалки — вкладка "Настройки" в
     админ-панели (см. loadAdminSettings в app.js). */
  .content .field{margin-bottom:14px; text-align:left; max-width:420px;}
  .content .field label{display:block; font-size:12px; color:var(--muted); margin-bottom:4px;}
  .section-title{font-size:14px; font-weight:700; margin:4px 0 12px 0; color:var(--text);}

  /* Диагностика сервера (место на диске/память + доступность сервисов) —
     прямой запрос 02.09.2026: "по сервисам чуть более кратко: названия,
     галочки и мини описание". */
  .diag-resources{display:flex; flex-direction:column; gap:12px; max-width:420px; margin-bottom:16px;}
  .diag-resource-label{display:flex; justify-content:space-between; font-size:12.5px; color:var(--muted); margin-bottom:4px;}
  .diag-resource-label span{color:var(--text); font-weight:600;}
  .diag-bar{height:7px; border-radius:4px; background:var(--surface2); overflow:hidden;}
  .diag-bar-fill{height:100%; background:var(--primary); border-radius:4px;}
  .diag-bar-fill.low{background:var(--error);}
  .diag-checks{display:flex; flex-direction:column; gap:2px; max-width:480px;}
  .diag-check{display:flex; align-items:flex-start; gap:10px; padding:8px 0; border-bottom:1px solid var(--border);}
  .diag-check:last-child{border-bottom:none;}
  .diag-check-icon{flex-shrink:0; width:20px; height:20px; border-radius:50%; display:flex; align-items:center;
    justify-content:center; font-size:12px; font-weight:700; margin-top:1px;}
  .diag-check.ok .diag-check-icon{background:#dcf5e3; color:#2e9e52;}
  .diag-check.fail .diag-check-icon{background:#fbdadd; color:var(--error);}
  .diag-check-name{font-size:13.5px; font-weight:600; color:var(--text);}
  .diag-check-desc{font-size:12px; color:var(--muted); margin-top:1px;}
  .diag-check.fail .diag-check-desc{color:var(--error);}

  /* Календарь встреч (прямой запрос 02.09.2026) — простая месячная
     сетка, заполняется по пузырькам встреч (pending+confirmed). */
  .cal-header{display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; max-width:420px;}
  .cal-grid{display:grid; grid-template-columns:repeat(7, 1fr); gap:3px; max-width:420px;}
  .cal-cell{aspect-ratio:1; display:flex; flex-direction:column; align-items:center; justify-content:center;
    border-radius:8px; font-size:12px; cursor:pointer; position:relative; background:var(--surface2);}
  .cal-cell.empty{background:none; cursor:default;}
  .cal-cell.cal-weekday{background:none; color:var(--muted); font-weight:600; aspect-ratio:auto; cursor:default;}
  .cal-cell.has-meetings{background:var(--primary-light); font-weight:700;}
  .cal-cell.selected{outline:2px solid var(--primary);}
  .cal-dot{width:5px; height:5px; border-radius:50%; background:var(--primary); margin-top:2px;}
  .cal-dot.confirmed{background:#2e9e52;}

  /* Почасовая схема выбранного дня + список конфликтов (прямой запрос
     02.09.2026: "внизу отображай почасовую схему... на 17:00 сразу 3
     клиента — конфликт"). */
  .hour-schedule{display:grid; grid-template-columns:repeat(12, 1fr); gap:3px; max-width:520px;}
  .hour-cell{aspect-ratio:1; display:flex; flex-direction:column; align-items:center; justify-content:center;
    border-radius:6px; font-size:10.5px; background:var(--surface2); color:var(--muted); position:relative;}
  .hour-cell.has-items{background:var(--primary-light); color:var(--text); font-weight:600;}
  .hour-cell.conflict{background:var(--error); color:#fff;}
  .hour-cell-count{font-size:9px; opacity:.85;}
  .conflict-row .info .title{color:var(--error); font-weight:600;}

  .modal-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:16px;}
  .code-display{font-size:24px; letter-spacing:3px; text-align:center; font-weight:700; margin:10px 0; word-break:break-all;}

  .tabs{display:flex; border-bottom:1px solid var(--border);}
  .tabs button{flex:1; background:none; border:none; color:var(--muted); padding:12px; font-size:13.5px; cursor:pointer; border-bottom:2px solid transparent;}
  .tabs button.active{color:var(--primary); border-color:var(--primary); font-weight:600;}
  .list-item{display:flex; align-items:center; gap:10px; padding:12px 4px; border-bottom:1px solid var(--border);}
  .list-item .info{flex:1; min-width:0;}
  .list-item .title{font-size:14px; font-weight:600;}
  .list-item .sub{font-size:12px; color:var(--muted);}
  /* Список каналов в админке — переиспользует тот же .channel-row, что и
     собственный список чатов участника (превью последнего сообщения,
     время, бейдж непрочитанных — прямой запрос 02.09.2026: "видимость
     кусочка последнего сообщения как в телеграме"), целиком кликабелен
     ("убери кнопку Написать, просто нажимаешь"). Групповой (не личный,
     не системный) канал выделяется цветной полосой слева + бейджем
     "Канал" — чтобы сразу отличался от переписок 1:1. */
  .channel-row.is-group-channel{border-left:3px solid var(--primary); padding-left:8px; background:var(--primary-light);}
  .group-channel-badge{display:inline-block; font-size:10px; font-weight:700; color:#fff; background:var(--primary);
    border-radius:5px; padding:1px 6px; margin-left:4px; vertical-align:middle;}
  .fab{position:fixed; bottom:24px; right:calc(50% - 360px + 24px); background:var(--primary); color:#fff; border:none;
    width:52px; height:52px; border-radius:50%; font-size:24px; cursor:pointer; box-shadow:0 4px 14px rgba(0,0,0,.4);}
  @media (max-width:760px){ .fab{right:24px;} }
