
/* ── 00-base.css ── */
/* БАЗА — оголошення шарів, @property, reset, типографіка, каркас, шапка
 *
 * Оголошення `@layer tokens, base, layout, components, utilities, overrides;`
 * мусить бути ПЕРШИМ рядком зшитого аркуша: воно фіксує порядок шарів.
 * Якщо шар уперше зустрінеться нижче, він стане останнім і латки
 * почнуть вигравати не там, де очікується.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

/*
 * BESIDA — 2026 CSS Layers refactor
 * Layer order: base < layout < components < utilities < overrides
 * A declaration in a later layer wins over an earlier one (same specificity).
 * !important in an earlier layer wins over !important in a later layer.
 */

@layer tokens, base, layout, components, utilities, overrides;

/* Анімований прогрес меню. Звичайну кастом-проперті транзишенити не можна
   — браузер бачить її як рядок; `@property` з типом `<number>` робить її
   інтерполяційною. Це і дає прийом Reactiive: ОДНА величина 0→1 на
   контейнер, а кожен пункт рахує свою позицію від неї і власного `--i`. */
@property --menu-p {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

@layer base {
/*
 * BESIDA — design tokens now live in Rust:
 *   - dark `:root` + light `[data-theme="light"]` blocks → beseda::css::tokens_css()
 *   - served at /static/tokens.css (loaded before this file in index.html)
 * Component rules below still reference --bg / --fg / --space-* / etc.
 */

/* Theme toggle buttons */
.theme-toggle,
.model-toggle {
  display: flex;
  gap: var(--ds-space-2);
  margin-bottom: var(--ds-space-3);
}

.theme-toggle .btn--small,
.model-toggle .btn--small {
  flex: 1;
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  border: none;
  border-radius: var(--ds-radius-sm);
}

.theme-toggle .btn--small:hover,
.model-toggle .btn--small:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
}

.theme-toggle .btn--small.active,
.model-toggle .btn--small.active {
  background: var(--ds-accent);
  color: var(--ds-on-accent);
}

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

html, body {
  overflow: hidden;
  height: 100%;
  /* Modern dvh keeps height sane on mobile browsers that hide the
     address bar on scroll — 100vh overflowed by the bar height,
     100dvh tracks the actually-visible area. */
  height: 100dvh;
}

html {
  font-size: 1rem; /* Prevents iOS auto-zoom on inputs */
  -webkit-font-smoothing: antialiased;
  text-rendering: geometricPrecision;
  scrollbar-gutter: stable;
}

body {
  font-family: var(--ds-font);
  background: var(--ds-surface-base);
  color: var(--ds-text-primary);
  line-height: 1.4;
  letter-spacing: -0.01em;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  touch-action: pan-y;
  overscroll-behavior: none;
  /* Safe area insets for notch/dynamic island */
  padding-top: env(safe-area-inset-top);
  padding-inline-start: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  transition:
    background-color 0.3s ease,
    color 0.3s ease;
}

/* color: on-accent, не літерал --p-neutral-0 — 06.09.2026 Ф1b: з
   монохромним акцентом (білий на dark) білий текст на білому виділенні
   ставав невидимим; той самий фікс у theme-toggle нижче й у send-кнопці
   (40-composer.css). */
::selection {
  background: var(--ds-accent);
  color: var(--ds-on-accent);
}

/* Видимий, на бренд focus-ring (a11y + «чіткість» à la Linear). Ф2a:
   раніше жив у @layer overrides; тут — глобальний дефолт у base, тож
   БУДЬ-ЯКЕ конкретніше `:focus-visible` у components/utilities (напр.
   .btn:focus-visible, .input:focus-visible) все одно виграє шаром. */
:focus-visible {
  outline: 0.125rem solid var(--ds-accent);
  outline-offset: 0.0625rem;
}

/* ═══════════════════════════════════════════
   LAYOUT - Mobile first
   ═══════════════════════════════════════════ */

/* Mobile-first refactor (Ben Frain Ch.3-4): the default rules describe
   the DESKTOP 2-column grid; mobile additions live in one `@media
   (max-width: var(--bp-md))` block below. This eliminates the old
   `!important` cluster that was resolving specificity fights between
   a mobile default and a desktop override.
   Sidebar track is fluid: `clamp(15rem, 22vw, 24rem)` reads at 240-384px
   across typical screens, scaling with viewport instead of a 320px
   hardcode. */
.layout {
  display: grid;
  grid-template-columns: clamp(15rem, 22vw, 24rem) 1fr;
  grid-template-rows: 3.5rem 1fr;
  grid-template-areas:
    "header header"
    "sidebar main";
  height: 100vh;
  height: 100dvh;
  position: relative;
  overflow: hidden;
}

/* ═══════════════════════════════════════════
   HEADER
   ═══════════════════════════════════════════ */

.header {
  grid-area: header;
  display: flex;
  align-items: center;
  padding: 0 var(--ds-space-3);
  gap: var(--ds-space-3);
  background: var(--ds-surface-base);
  z-index: 100;
}

/* Ф1 (план §2, п.8): крапка-статус у лівому верхньому куті шапки — банений
   ШІ-патерн (пам'ять ai_ui_patterns_banned, no_status_dots). Ховаємо до
   Ф2, де стан з'єднання отримає текстовий/іконковий індикатор замість
   кольорової точки. */
.status {
  display: none;
}

.title {
  font-size: var(--t2);
  font-weight: 600;
  letter-spacing: -0.04em;
  flex: 1;
}

/* Account switcher — header chrome, same flat language as the rest of the
   bar: no pill, no drop shadow, no blue circle. Sits left of the logo and
   only opens its menu on click. */
.acct {
  position: relative;
  flex-shrink: 0;
}

.acct__chip {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  max-width: 12rem;
  min-height: 2.25rem;
  padding: var(--ds-space-1) var(--ds-space-2);
  background: none;
  border: 0.0625rem solid transparent;
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-secondary);
  font: inherit;
  font-size: var(--ds-label-size);
  cursor: pointer;
  transition: transform 100ms var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}

.acct__chip:hover {
  background: var(--ds-surface-raised);
  color: var(--ds-text-primary);
}

/* Answers on press, not on release. */
.acct__chip:active { transform: scale(0.97); }

.acct__ava {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  flex: 0 0 auto;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  font-size: var(--ds-meta-size);
  font-weight: 600;
}

.acct__ava--ghost {
  color: var(--ds-text-muted);
}

.acct__lbl {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.acct__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--ds-space-1));
  min-width: 12rem;
  padding: var(--ds-space-1);
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: var(--ds-radius-sm);
  z-index: 150;
}

.acct__item {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  width: 100%;
  padding: var(--ds-space-2);
  background: none;
  border: 0;
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  font: inherit;
  font-size: var(--ds-label-size);
  text-align: left;
  cursor: pointer;
}

.acct__item:hover { background: var(--ds-surface-raised); }
.acct__item:active { background: var(--ds-surface-raised); transform: scale(0.99); }

/* Log out carries no avatar, so indent its label to the same text column
   as the rows above it — an avatar-width plus the row gap. */
.acct__item--danger {
  color: var(--ds-feedback-danger);
  padding-left: calc(var(--ds-space-2) * 2 + 1.5rem);
}

/* Narrow header: the nickname goes, the initial stays clickable.
   CHIP only — the dropdown items keep their labels, otherwise the menu
   collapses to a bare "+" and a blank logout row. */
@media (max-width: 40em) {
  .acct__chip .acct__lbl { display: none; }
}

.logo {
  display: flex;
  align-items: center;
  flex: 1;
  color: var(--ds-text-primary);
  text-decoration: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
}
.logo:hover svg { color: var(--ds-accent); }

.logo svg {
  width: 1.5rem;
  height: 1.5rem;
}

}

/* Поза шарами навмисно: атрибут `hidden` має перемагати будь-яке
   `display:flex` з шарів (JS ховає кнопки дзвінка/форми через `.hidden=true`,
   а `.btn--icon{display:flex}` у layer'і його глушив — 05.09.2026). Нешаровий
   рядок старший за всі @layer без !important. */
[hidden] { display: none; }

/* ── 10-layout.css ── */
/* КАРКАС — dynamic island, бічна панель
 *
 * Шар layout тримає лише розкладку поверхонь: де що стоїть і скільки
 * займає. Кольори й стани компонентів — у шарі components нижче.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer layout {
/* ═══════════════════════════════════════════
   DYNAMIC ISLAND — push/enable notification prompt
   ═══════════════════════════════════════════ */

.dynamic-island {
  position: fixed;
  top: var(--ds-space-3);
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: 1.5rem;
  /* Найближчий існуючий токен тіні — --ds-shadow-3 (рівень overlay/modal,
     точно цей контекст): 0 8px 28px rgba(0,0,0,.5) замість колишнього
     0 8px 32px rgba(0,0,0,.4) — блюр на 4px вужчий, альфа трохи темніша,
     різниця непомітна на око. */
  box-shadow: var(--ds-shadow-3);
  font-size: var(--ds-label-size);
  color: var(--ds-text-primary);
  max-width: min(28rem, calc(100vw - var(--ds-space-4)));
  animation: islandSlideIn 0.4s cubic-bezier(0.32, 0.23, 0.4, 0.9);
}

@keyframes islandSlideIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-1rem); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

.dynamic-island__icon {
  display: flex;
  align-items: center;
  color: var(--ds-accent);
  flex-shrink: 0;
}

.dynamic-island__text {
  flex: 1;
  line-height: 1.3;
}

.dynamic-island__action {
  flex-shrink: 0;
  padding: var(--ds-space-1) var(--ds-space-3);
  border: none;
  border-radius: var(--ds-radius-sm);
  background: var(--ds-accent);
  color: var(--ds-on-accent);
  font-size: var(--ds-meta-size);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--duration-fast);
}
.dynamic-island__action:hover {
  background: var(--ds-accent-hover);
}

.dynamic-island__close {
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  border: none;
  background: transparent;
  color: var(--ds-text-muted);
  font-size: 0.75rem;
  cursor: pointer;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color var(--duration-fast), background var(--duration-fast);
}
.dynamic-island__close:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
}

.btn--icon {
  /* Apple HIG: min 44x44 pt touch target. SVG inside can be smaller —
     the padding carries the hit area. */
  min-width: 2.75rem;
  min-height: 2.75rem;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 0;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
}

.btn svg {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}

/* Вигляд (прозорість, колір, hover) — у 40-composer.css поруч із .btn,
   у тому ж шарі components; тут лише геометрія. */

/* ═══════════════════════════════════════════
   SIDEBAR
   ═══════════════════════════════════════════ */

.sidebar {
  grid-area: sidebar;
  display: flex;
  flex-direction: column;
  background: var(--ds-surface-raised);
  border-right: 0.0625rem solid var(--ds-border-strong);
  min-height: 0;
  overflow: hidden;
  /* Enable container queries so dialog-items can adapt to sidebar
     width, not viewport width (Ch.5 — "components respond to space
     they occupy, not the window"). */
  container-type: inline-size;
  container-name: side;
}

/* Mobile state machine: data-view="list" | "chat" — only meaningful
   below the breakpoint, where sidebar and main overlap as sliding
   overlays. On desktop both are grid children and no `data-view`
   dance is needed. */
@media (max-width: 48em) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "content";
  }

  .sidebar {
    grid-area: content;
    position: absolute;
    /* `inset: 0` positions within the grid-area containing block, which
       is row 2 of the layout grid (starting at y = 3.5rem). Adding an
       explicit `top: 3.5rem` would double-offset — a 56px gap above
       the dialog-search input the user spotted. */
    inset: 0;
    z-index: 50;
    border-right: none;
    transition: transform 0.3s var(--ds-ease-in-out);
  }

  .layout[data-view="chat"] .sidebar { transform: translateX(-100%); visibility: hidden; }
  .layout[data-view="chat"] .main { transform: translateX(0); }
  .layout[data-view="chat"] .header { display: none; }
  .layout[data-view="chat"] { grid-template-rows: 1fr; grid-template-areas: "content"; }
  .layout[data-view="list"] .sidebar { transform: translateX(0); }
  .layout[data-view="list"] .main { transform: translateX(100%); }
}

/* ── Chat column (Stepanov: minimal vocabulary) ─────────────────────
   `.main` is a flex column whose ONLY scrolling child is `.messages`.
   Default (desktop): a grid child that sits next to the sidebar.
   Mobile adds absolute+translate for the slide-in overlay — the base
   transform lives here in the same layer as the data-view rules so
   specificity (not layer precedence) resolves the cascade. */
.main {
  grid-area: main;
  display: flex;
  flex-direction: column;
  background: var(--ds-surface-base);
  min-height: 0;
  overflow: hidden;
}
@media (max-width: 48em) {
  .main {
    grid-area: content;
    position: absolute;
    inset: 0;
    z-index: 40;
    transform: translateX(100%);
    transition: transform 0.3s var(--ds-ease-in-out);
  }
}

.sidebar.hidden {
  transform: translateX(-100%);
  pointer-events: none;
}

.sidebar__header {
  display: none;
}

/* Mobile backdrop when chat is open */
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--p-neutral-1000) 50%, transparent);
  z-index: 49;
  opacity: 0;
  transition: opacity 0.3s;
}

.sidebar-backdrop.visible {
  display: block;
  opacity: 1;
}

.sidebar__title {
  display: none;
}

}

/* ── 20-sidebar-dialogs.css ── */
/* СПИСОК ДІАЛОГІВ — пошук і рядки діалогів
 *
 * Еталонна поверхня системи (DESIGN_SYSTEM.md §8): саме тут спершу
 * з'являється вагова ієрархія і хайрлайн-розділювачі.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   DIALOG SEARCH
   ═══════════════════════════════════════════ */

.dialog-search {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
  padding: var(--ds-space-2) var(--ds-space-2);
  background: var(--ds-surface-raised);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  flex-shrink: 0;
}
/* Header slot — quiet inline field between logo and controls.
   DS: inputs use radius-sm + border-subtle + UI font. The old look — a fat
   1rem pill with the base input's monospace placeholder — read as a foreign
   widget in the header. Focus is carried by the CONTAINER (focus-within),
   so the whole field lights up, not a stray inner edge. */
.dialog-search--header {
  flex: 1;
  min-width: 0;
  height: 2.125rem;
  border: 0.0625rem solid transparent;
  /* radius-md (12px) — playlist-bot `.search-box` :1808, не radius-sm
     (Ф1b план §6: капсула пошуку з мови плеєра). */
  border-radius: var(--ds-radius-md);
  background-color: var(--ds-surface-overlay);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2380808a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 0.625rem center;
  background-size: 0.875rem 0.875rem;
  padding: 0 var(--ds-space-2) 0 2rem;
  margin: 0 var(--ds-space-2);
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}
.dialog-search--header:hover { border-color: var(--ds-border-subtle); }
/* focus-ring — playlist-bot `.search-box:focus` :1817-1821: рамка акцентом
   + 3px тінь акцент-тінтом навколо, не лише зміна кольору рамки. */
.dialog-search--header:focus-within {
  border-color: var(--ds-accent);
  background-color: var(--ds-surface-base);
  box-shadow: 0 0 0 0.1875rem var(--ds-accent-subtle);
}
/* The container carries the focus ring — the inner input's own outline
   would draw a second box inside the field. */
.dialog-search--header .dialog-search__input:focus,
.dialog-search--header .dialog-search__input:focus-visible {
  outline: none;
}
.dialog-search--header .dialog-search__input {
  min-height: 0;
  height: 100%;
  padding: 0;
  border: none;
  font-size: var(--ds-label-size);
  font-family: var(--ds-font);
}

.dialog-search__icon {
  color: var(--ds-text-muted);
  flex-shrink: 0;
}

.dialog-search__input {
  flex: 1;
  width: 100%;
  min-height: 2.75rem;  /* HIG 44pt */
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 0;
  padding: 0.625rem var(--ds-space-2);
  font-size: var(--ds-label-size);
  font-family: var(--ds-font-mono);
  color: var(--ds-text-primary);
  outline: none;
  transition: border-color var(--duration-fast) var(--ease-out);
  min-width: 0;
}

.dialog-search__input:focus {
  border-color: var(--ds-accent);
}

.dialog-search__input::placeholder {
  color: var(--ds-text-subtle);
}

.dialog-search__clear {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  background: none;
  border: none;
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-muted);
  cursor: pointer;
  flex-shrink: 0;
  padding: 0;
  transition: color var(--duration-fast);
}

.dialog-search__clear:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-overlay);
}

/* ═══════════════════════════════════════════
   DIALOG LIST - Unified (P2P + Telegram)
   ═══════════════════════════════════════════ */

.dialog-list {
  list-style: none;
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* Зверху 0: перший елемент — липка смуга вкладок, і 8px над нею
     їхали б під неї при прокрутці як «щілина». Свій відступ смуга має. */
  padding: 0 0 var(--ds-space-2);
  /* Hide scrollbar — iOS-style ghost scroll, only appears during active
     drag (native behavior on WebKit). Desktop keeps it hidden too. */
  scrollbar-width: none;
}
.dialog-list::-webkit-scrollbar { display: none; }

.dialog-item {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  padding: var(--ds-space-2) var(--ds-space-3);
  /* Вагова ієрархія списку (DESIGN_SYSTEM §8): роомніший вертикальний ритм
     + hairline розділювач. Ф2a, з @layer overrides. */
  padding-block: var(--ds-space-3);
  /* Рядок за метриками Telegram (Артем 05.09.2026): ~72px на мобільному —
     avatar 50px (3.125rem) + 2×space-3 (12px) padding ≈ 74px, округлення
     в межах ока. Стягується до ~64px на десктопі нижче. */
  min-height: 4.5rem;
  position: relative;
  margin: 0;
  cursor: pointer;
  /* Ф1 §4 «Правила»: без 300ms tap-delay на дотику. */
  touch-action: manipulation;
  /* transition: background+transform живе в 95-utilities.css (шар
     utilities > components, там же й :active { scale(.99) } — інакше
     цей запис тут ніколи не рендериться, шар вищий все одно виграє). */
  /* Perf: offscreen items skip layout/paint until scrolled into view.
     Оголошення стояло ДВІЧІ в цьому ж блоці з різними значеннями
     (`auto 3rem` і `3.5rem 0`) — браузер зводив їх у безглузде
     `auto 56px auto 0px`. Один рядок, одна висота. */
  /* 07.09.2026, скрін Артема з iOS: перший рядок під смугою вкладок
     обрізаний зверху рівно на 17pt = 74 − 56. Safari малював рядок у
     плейсхолдерній висоті `3.5rem` (для 56px-рядків старої верстки), а
     справжній рядок — 4.5rem (min-height вище). Для ста рядків
     content-visibility не економить нічого помітного, а на WebKit дає
     стрибки й обрізання під час прокрутки — прибрано зовсім. */
  /* Entry animation lives in one place — see `dialogFadeIn` below in the
     micro-interactions section. The old cascade lived here AND there, and the
     later one silently won; keeping two was how the blur version survived
     unnoticed. */
}

/* Desktop: тісніший ритм — 64px замість 72px мобільних (той самий avatar
   50px, менший вертикальний паддінг). */
@media (width > 48em) {
  .dialog-item { min-height: 4rem; padding-block: 0.4375rem; }
}

/* Роздільник — hairline ВІД КРАЮ АВАТАРА (Telegram-манер, Артем 05.09.2026),
   не в усю ширину рядка: псевдоелемент замість border-bottom на всьому
   `.dialog-item`, зсунутий на padding + ширину аватара + gap. Останній
   рядок групи теж отримує лінію — так само, як робив border-bottom раніше;
   лишень зникає під час hover/selected нема сенсу, тож простий static offset. */
.dialog-item::after {
  content: "";
  position: absolute;
  left: calc(var(--ds-space-3) + 3.125rem + var(--ds-space-3));
  right: var(--ds-space-3);
  bottom: 0;
  height: 0.0625rem;
  background: var(--ds-border-subtle);
}

/* Re-rendered items (same data-dialog-id) skip the animation. */
.dialog-item--seen { animation: none; will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  .dialog-item { animation: none; will-change: auto; }
}

/* View Transitions — smooth cross-fade between list states. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
}

/* Ф2a: overrides перебивав "transparent" на var(--ds-surface-hover) —
   без цього hover не давав жодного відгуку. */
.dialog-item:hover {
  background: var(--ds-surface-hover);
}

/* Selected dialog — subtle background tint + weight bump on the
   name. No left-edge bar (the "AI-template" cliche). The mono palette
   means accent==fg, so the cue lives in the row's surface, not in a
   color shift on text. */
.dialog-item--selected {
  background: var(--ds-row-selected-bg);
}

.dialog-item--selected .dialog-name {
  font-weight: 600;
}

.dialog-item--selected:hover {
  background: color-mix(in srgb, var(--p-neutral-0) 9%, transparent);
}

/* Container queries — dialog-item adapts to sidebar width, not viewport
   (Ben Frain Ch.5 + Ch.11). When `smart-ui--collapsed` narrows the
   sidebar, items shed the secondary text first, then align to centre
   as a compact icon strip. No JS, no viewport lookup — the component
   responds to the space its parent actually gives it.
   Container is `side` (see `.sidebar` block above). */
@container side (max-width: 14em) {
  /* First casualty: the 2nd-line preview text. Avatar + name still fit. */
  .dialog-preview { display: none; }
  .dialog-item { gap: var(--ds-space-2); padding: var(--ds-space-2); }
}
@container side (max-width: 10em) {
  /* Name line also drops. Dialog reduces to avatar with absolute badge. */
  .dialog-info { display: none; }
  .dialog-item {
    justify-content: center;
    padding: var(--ds-space-2) 0;
  }
  .dialog-badge {
    position: absolute;
    top: 0.25rem;
    right: 0.5rem;
  }
}
@container side (max-width: 7em) {
  /* Ultra-compact: kill the badge too. Unread count lives on avatar
     ring colour via `.dialog-item--selected` / incoming status. */
  .dialog-badge { display: none; }
}

/* Search input + tabs also adapt — at narrow widths the search bar
   collapses to an icon-only placeholder and tabs drop their labels. */
@container side (max-width: 12em) {
  .dialog-search__input { text-align: center; padding: var(--ds-space-2); }
  .dialog-search__input::placeholder { font-size: 0; }   /* hide placeholder, focus still works */
  .dialog-search__input:focus { padding: var(--ds-space-2) var(--ds-space-3); }
  .dialog-tabs { gap: 0; }
  .dialog-tab { padding: var(--ds-space-1); font-size: var(--ds-eyebrow-size); }
}

/* Avatar — 50px коло (Telegram-метрика, Артем 05.09.2026); кімнати лишаються
   квадратом зі скругленням --ds-radius-sm через .dialog-avatar--room нижче. */
.dialog-avatar {
  width: 3.125rem;
  height: 3.125rem;
  min-width: 3.125rem;
  min-height: 3.125rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.125rem;
  font-weight: 600;
  text-transform: uppercase;
  flex-shrink: 0;
  color: var(--ds-text-secondary);
  background: transparent;
  border: 0.0625rem solid var(--ds-border-strong);
  position: relative;
}

/* Малий гліф каналу в правому нижньому куті аватара (TG-літак / конверт /
   решітка кімнати) — не для p2p/ai/case, сама фігура вже каже "хто це". */
.dialog-avatar__glyph {
  position: absolute;
  right: -0.125rem;
  bottom: -0.125rem;
  width: 1.0625rem;
  height: 1.0625rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-surface-base);
  color: var(--ds-text-secondary);
}
.dialog-avatar__glyph svg { width: 0.625rem; height: 0.625rem; }
.dialog-avatar__glyph--tg { color: var(--tg); }
.dialog-avatar__glyph--email { color: var(--ds-feedback-warning); }

/* Avatars — monochrome: surface colour varies by elevation, not hue.
   Type distinction lives in the initial letter/badge, not a coloured
   swatch. Matches Linear/Vercel/Arc's "identity via letter" pattern. */
.dialog-avatar--ai,
.dialog-avatar--p2p,
.dialog-avatar--tg,
.dialog-avatar--tg-group,
.dialog-avatar--tg-channel,
.dialog-avatar--email {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
}

/* Room row: square tile, not a circle — a room list mixes several
   unrelated projects (unlike a single contact/channel), so it gets the
   hue cue (`[data-hue]`, below) plus a squared shape to read as a distinct
   "container" kind at a glance, the way Telegram's forum groups do.
   beseda 05.09.2026: previously fell back to the plain monochrome circle
   with no hue attribute, reading as a bare "P" tile. */
.dialog-avatar--room {
  border-radius: var(--ds-radius-sm);
}

/* Language switch button — sits next to logo in header */
.btn--lang {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.5rem;
  height: 1.875rem;
  padding: 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ds-text-secondary);
  text-decoration: none;
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: 0.375rem;
  background: var(--ds-surface-overlay);
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.btn--lang:hover {
  color: var(--ds-text-primary);
  border-color: var(--ds-text-secondary);
  background: var(--ds-surface-hover);
}

/* Avatar images for Telegram */
.dialog-avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* Icon tab bar for dialog sections */
.dialog-tabs {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  padding: 0.375rem 0.5rem;
  background: var(--ds-surface-raised);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  overflow-x: auto;
  scrollbar-width: none;
  flex-shrink: 0;
}
.dialog-tabs::-webkit-scrollbar { display: none; }

/* Пілюля — playlist-bot `.genre-chip` :2015-2034 (Ф1b план §6): рамка +
   поверхня-raised, не гола підпис-кнопка. Тач-ціль лишається 44pt (HIG) —
   плеєр сам чіп нижчий (~28px), бо не мусить бути дотиковою ціллю
   першого ряду; тут ціль важливіша за буквальну висоту еталона. */
.dialog-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;  /* Apple HIG 44pt */
  min-width: 2.75rem;
  height: 2.75rem;
  padding: 0 var(--ds-space-3);
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-default);
  color: var(--ds-text-muted);
  /* Дефект №11 (план §2): eyebrow-ярлики секцій/вкладок — Inter, не mono.
     Мono лишається лише для коду. 12/600/uppercase/+0.04em. */
  font-size: var(--ds-meta-size);
  font-family: var(--ds-font);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
  flex-shrink: 0;
}
.dialog-tab:hover { color: var(--ds-text-primary); background: var(--ds-surface-hover); border-color: var(--ds-border-strong); }
/* Активний таб = тональний крок поверхні + вага тексту + рамка strong
   (playlist-bot `.genre-chip.active` :2030-2034). Підкреслення акцентом
   заборонено: це AI-патерн, як і кольорові кромки. */
.dialog-tab--active {
  background: var(--ds-surface-active);
  border-color: var(--ds-border-strong);
  color: var(--ds-text-primary);
  font-weight: 600;
}

.dialog-tab svg { width: 1.125rem; height: 1.125rem; }

.dialog-tab__badge {
  position: absolute;
  top: 0.125rem;
  right: 0.125rem;
  min-width: 0.875rem;
  height: 0.875rem;
  border-radius: 0.4375rem;
  background: var(--ds-accent);
  color: var(--ds-on-accent);
  font-size: 0.5625rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 0.1875rem;
  line-height: 1;
}

.dialog-tab--add {
  color: var(--ds-text-subtle);
  border: 0.0625rem dashed var(--ds-border-strong);
  width: 2rem;
  height: 1.75rem;
}
.dialog-tab--add:hover { color: var(--ds-text-secondary); border-color: var(--ds-text-muted); }
.dialog-tab--add svg { width: 0.875rem; height: 0.875rem; }

/* Custom folder tab with name */
.dialog-tab--folder {
  width: auto;
  padding: 0 0.5rem;
  gap: 0.25rem;
  font-size: 0.6875rem;
  font-weight: 500;
}
.dialog-tab--folder span { max-width: 3rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Section headers — hidden when tabs are active */
.dialog-section-header {
  display: none;
}

/* ── Combined filter row ──
   Дефект №4 (план §2). Дві попередні спроби відхилені супервізором
   06.09.2026: (v1) `flex-wrap` на комбінованому рядку саму лише
   «Групувати» зносив вниз, а `min-width:max-content` на вкладках все
   одно вилазив за `.sidebar{overflow:hidden}` — той самий рефлекс, інший
   рівень; (v2) колонка (вкладки згори + дропдаун під ними) не різала
   нічого, але на 1280 давала ТРИ рядки шапки списку (150px) — це вже не
   компакт-полиця, а власна панель.

   v3 — Telegram-метрика: один рядок, вкладки скролляться горизонтально
   (обрізання/скрол — ок, план явно дозволяє це для «tabs», не для
   тексту всередині), «Групувати» завжди праворуч у ТОМУ Ж рядку.
   `.dialog-tabs` (базовий клас, він же на `.dialog-tabs--types`) вже мав
   `overflow-x:auto` + прихований скролбар — саме ця пара тепер працює
   як задумано, бо тут вона застосована СВІДОМО (не auto-min-0 побічний
   ефект): вкладки `flex:0 0 auto` не стискаються, контейнер вкладок
   `flex:1 1 0; min-width:0` забирає весь вільний простір МІЖ логотипом
   списку й «Групувати», а не свій natural-content розмір. */
.dialog-tabs-combined {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-2);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  flex-shrink: 0;
  /* 07.09.2026 (скрін Артема, iOS): смуга вкладок жила всередині
     прокрутки списку і їхала вгору разом з рядками — щоб перемкнути
     MAIL→TG після прокрутки, треба було вертатись на початок. Як у
     Telegram: смуга липне до верху списку. sticky теж позиційований —
     containing block для .dimension-dropdown-menu лишається тут. */
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--ds-surface-raised);
}
/* Еталон 07.09.2026 — перемикач режимів камери iOS (скрін Артема):
   ОДНА капсула-доріжка, режими — голий текст, активний у підсвіченій
   капсулі, краї доріжки тануть, праворуч кругла кнопка-іконка.
   Доріжку малює псевдоелемент батька, а не сам скролер: mask на скролері
   ховала б і його фон на краях. Геометрія детермінована: відступ смуги
   (space-2) + кругла кнопка (2.75rem) + gap (space-2). */
.dialog-tabs-combined::before {
  content: "";
  position: absolute;
  top: var(--ds-space-2);
  bottom: var(--ds-space-2);
  left: var(--ds-space-2);
  right: calc(var(--ds-space-2) + 2.75rem + var(--ds-space-2));
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-border-subtle);
  pointer-events: none;
}
.dialog-tabs-combined .dialog-tabs--types {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  gap: 0;
  /* 2px повітря між рамкою доріжки і активною капсулою; висота
     доріжки 2.75rem = 2×2px + вкладка 2.5rem. */
  padding: 0.125rem 0.125rem;
  scroll-snap-type: x proximity;
  border-bottom: none;
  background: transparent;
  /* Обрізання «R|» під кнопкою було глухим; тепер краї доріжки тануть
     в обидва боки (mask, не gradient-фон — нічого не домальовується),
     а padding дає крайнім чипам вийти з-під тіні на кінцях прокрутки. */
  scroll-padding-inline: 1.25rem;
  mask-image: linear-gradient(to right, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent);
}
.dialog-tabs-combined .dialog-tabs--types .dialog-tab {
  flex: 0 0 auto;
  scroll-snap-align: center;
  min-height: 2.5rem;
  height: 2.5rem;
  min-width: 0;
  padding: 0 var(--ds-space-3);
  background: transparent;
  border-color: transparent;
}
.dialog-tabs-combined .dialog-tabs--types .dialog-tab:hover {
  background: transparent;
  border-color: transparent;
  color: var(--ds-text-secondary);
}
.dialog-tabs-combined .dialog-tabs--types .dialog-tab--active,
.dialog-tabs-combined .dialog-tabs--types .dialog-tab--active:hover {
  background: var(--ds-surface-active);
  border-color: var(--ds-border-strong);
  color: var(--ds-text-primary);
}
.dialog-tabs-combined .dimension-dropdown {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 0;
}

/* ── Dimension dropdown (right-aligned, secondary control) ──
   Ф2a: панель відкриття (.dimension-dropdown-menu нижче) вирісши з
   вузького 8rem-попапу в картки на всю ширину смуги табів — контейнер
   більше не позиціонує сам себе (position:static), позиціонує
   .dialog-tabs-combined вище. Було з @layer overrides. */
.dimension-dropdown {
  position: static;
  flex-shrink: 0;
  padding: 0 0.375rem;
}
.dimension-dropdown-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-base);
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.dimension-dropdown-btn svg { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }
/* Назва «Групувати» лишається для читачів екрана й у title, з екрана
   зникає — кнопка кругла, як у камери iOS (Артем, 07.09.2026). */
.dimension-dropdown-label {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.dimension-dropdown-btn:hover {
  color: var(--ds-text-primary);
  border-color: var(--ds-border-default);
  background: var(--ds-surface-hover);
}
.dimension-dropdown--open .dimension-dropdown-btn {
  color: var(--ds-text-primary);
  border-color: var(--ds-border-strong);
  background: var(--ds-surface-active);
}

/* Було: сірий 8rem-попап із чотирма рядками тексту, що висів збоку від
   кнопки. Стало: панель на всю ширину смуги табів — картки «як
   перегрупувати список» (.dimension-card нижче): схема-гліф + назва +
   підказка. Ф2a, з @layer overrides. */
.dimension-dropdown-menu {
  display: none;
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0.375rem;
  right: 0.375rem;
  min-width: 0;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.125rem;
  padding: 0.1875rem;
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  /* Найближчий існуючий токен — --ds-shadow-3 (елевація overlay/modal):
     0 8px 28px rgba(0,0,0,.5) замість колишнього 0 12px 32px rgba(0,0,0,.55)
     — трохи компактніша й світліша тінь, різниця непомітна. */
  box-shadow: var(--ds-shadow-3);
  z-index: 20;
  list-style: none;
  margin: 0;
  transform-origin: top center;
}
.dimension-dropdown--open .dimension-dropdown-menu {
  display: grid;
  animation: dim-menu-in 160ms var(--ease-out) both;
}

/* Активна лінза одна, підсвічена акцентом; відкриття — короткий підйом
   зі стаггером на картках. */
.dimension-card {
  display: grid;
  grid-template-areas: "glyph name" "glyph hint";
  grid-template-columns: auto 1fr;
  column-gap: 0.625rem;
  row-gap: 0.0625rem;
  align-items: center;
  padding: 0.5rem 0.625rem;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-xs);
  cursor: pointer;
  color: var(--ds-text-secondary);
  transition: background 0.12s, color 0.12s;
}
.dimension-dropdown--open .dimension-card {
  animation: dim-card-in 180ms var(--ease-out) both;
}
.dimension-dropdown--open .dimension-card:nth-child(2) { animation-delay: 30ms; }
.dimension-dropdown--open .dimension-card:nth-child(3) { animation-delay: 60ms; }
.dimension-dropdown--open .dimension-card:nth-child(4) { animation-delay: 90ms; }

.dimension-card:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
}
.dimension-card__glyph {
  grid-area: glyph;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--ds-radius-xs);
  background: color-mix(in srgb, var(--p-neutral-0) 4%, transparent);
  color: var(--ds-text-muted);
  transition: color 0.12s, background 0.12s;
}
.dimension-card__name {
  grid-area: name;
  font-size: var(--ds-meta-size);
  font-weight: 600;
  line-height: 1.2;
}
.dimension-card__hint {
  grid-area: hint;
  font-size: 0.6875rem;
  line-height: 1.25;
  color: var(--ds-text-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dimension-card--active {
  color: var(--ds-text-primary);
  background: var(--ds-accent-subtle);
}
.dimension-card--active .dimension-card__glyph {
  color: var(--ds-accent);
  background: color-mix(in srgb, var(--ds-accent) 14%, transparent);
}
.dimension-card--active .dimension-card__name { color: var(--ds-accent); }

@keyframes dim-menu-in {
  from { opacity: 0; transform: translateY(-0.25rem) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
@keyframes dim-card-in {
  from { opacity: 0; transform: translateY(-0.1875rem); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .dimension-dropdown--open .dimension-dropdown-menu,
  .dimension-dropdown--open .dimension-card { animation: none; }
}

.dimension-dropdown-item {
  padding: 0.375rem 0.625rem;
  border-radius: var(--ds-radius-xs);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
.dimension-dropdown-item:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
}
.dimension-dropdown-item--active {
  background: var(--ds-accent-subtle);
  color: var(--ds-accent);
  font-weight: 600;
}

/* click-outside: clicking outside closes the dropdown */
.dimension-dropdown--open::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 10;
}

/* ── Type sub-tabs (All | P2P | TG | Mail | AI) ── */
.dialog-tabs--types {
  border-bottom: none;
  background: var(--ds-surface-raised);
}
.dialog-tabs--types .dialog-tab--active {
  background: var(--ds-surface-overlay);
  font-weight: 600;
}
/* Той самий принцип, що вище: тональний крок, без акцентної смуги. */

/* ── Group headers ──
   Run-done cues on a messenger: mono bracket-eyebrow — один рядок, де
   моно/uppercase/letter-spacing читається як run-done-хром, не чіпаючи
   читабельність повідомлень. Лічильник — акцентним праворуч. Ф2a,
   з @layer overrides. Без bracket-псевдоелементів: рядок full-width
   justified (лейбл ліворуч, лічильник праворуч) — `[`…`]` тягнувся б
   до країв і читався зламаним; дужки лишились лише на заголовках
   налаштувань, де короткий лейбл без лічильника. */
.dialog-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.75rem 0.25rem;
  /* Дефект №11: eyebrow = Inter, не mono. */
  font-family: var(--ds-font);
  font-size: var(--ds-eyebrow-size);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  background: var(--ds-surface-raised);
}
.dialog-group-header:first-child {
  border-top: none;
}
/* Плоский вимір «recent» віддає одну групу без назви: заголовок
   рендерився як порожня смуга 13px між двома лініями (скрін Артема,
   07.09.2026). Організм теж перестає його віддавати; це правило —
   для бінаря, що ще не перезібраний. */
.dialog-group-header:has(> span:first-child:empty) {
  display: none;
}
/* Ф1 (план §2, п.8): зелений mono-лічильник «1689» над списком — шумова
   хромка без сенсу для юзера (загальна кількість рядків у базі). Ховаємо
   до Ф2, де заголовок групи або зникне, або отримає осмислений підпис. */
.dialog-group-count {
  display: none;
}

/* Folder toggle on dialog items */
.dialog-folder-toggle {
  position: absolute;
  right: 0.25rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  border: 0.0625rem solid var(--ds-border-strong);
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  font-size: 0.875rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.15s;
}
.dialog-item:hover .dialog-folder-toggle { opacity: 1; }

/* Dynamic avatar colors. НЕ токенізовано навмисно (Ф2b): --avatar-hue —
   довільне число 0-360 на контакт, а не роль; колір рахується з нього
   функцією hsl(), тому лінтер бачить "hsl(" і рахує це color-literal.
   Замінити на var() неможливо без css.rs (поза межами цього агента) або
   filter:hue-rotate() на фіксованому токені — останнє змінило б рендер
   (зачепило б і вкладене фото аватара) всупереч правилу «візуал незмінний».
   Лишається як задокументований виняток. */
.dialog-avatar[data-hue] {
  background: hsl(var(--avatar-hue, 220), 25%, 25%);
  color: hsl(var(--avatar-hue, 220), 30%, 70%);
}

[data-theme="light"] .dialog-avatar[data-hue] {
  background: hsl(var(--avatar-hue, 220), 30%, 92%);
  color: hsl(var(--avatar-hue, 220), 40%, 40%);
}

/* Content */
.dialog-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

/* Вагова вісь (DESIGN_SYSTEM §8): ім'я — сильніше й на крок більше;
   прев'ю падає до приглушеного regular — чіткий 2-й рівень. Ф2a,
   з @layer overrides. */
.dialog-name {
  /* 16px/600 — метрика Telegram (Артем 05.09.2026). Немає готового
     --ds-*-size токена на 16 (шкала §2 стрибає 15→14→13); rem-літерал тут —
     задокументований виняток, не нова довільна одиниця (лінтер забороняє
     лише px/hex-літерали, не rem). */
  font-size: 1rem;
  font-weight: var(--ds-weight-strong);
  color: var(--ds-text-primary);
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dialog-preview {
  font-size: var(--ds-body-size);   /* 14px, muted — один рядок */
  font-weight: var(--ds-weight-regular);
  color: var(--ds-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dialog-preview--locked {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  color: var(--ds-text-muted);
  font-size: var(--ds-meta-size);
}

.dialog-preview--locked::before {
  content: "";
  width: 1rem;
  height: 1rem;
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E")
    center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* Виділений рядок: колір беремо з токенів, а не білим по-жорсткому.
   Підсвітка виділення — напівпрозорий АКЦЕНТ (rgba зелений 0.10-0.14), а
   не темна плашка, тож на світлій темі білий текст лягав білим по блідо-
   мʼятному: час у виділеному рядку читався приблизно 1.1:1, тобто ніяк. */
.dialog-item--selected .dialog-preview {
  color: var(--ds-text-secondary);
}

/* Right column of a row: last-message time over the unread badge.
   `margin-left:auto` pins it to the edge; `min-width:0` on the text block
   next to it is what lets a long name ellipsize instead of shoving the
   time off the row. */
.dialog-meta {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.1875rem;
  flex-shrink: 0;
}

.dialog-info {
  flex: 1 1 auto;
  min-width: 0;
}

.dialog-time {
  /* Дефект №11: Inter tabular-nums замість mono — mono лишається лише
     для коду. */
  font-family: var(--ds-font);
  font-size: var(--ds-meta-size);  /* 12px точно (--font-size-mono=12.8px) */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  color: var(--ds-text-muted);
  white-space: nowrap;
}

.dialog-item--selected .dialog-time {
  color: var(--ds-text-muted);
}

/* Narrow sidebar drops the time before it drops the preview: the row
   still answers "who wrote", which is what a 10rem rail is for. */
@container side (max-width: 14em) {
  .dialog-time { display: none; }
}

/* Badge — minimal mono pill. No shadow (editorial flat); mono digits
   so multi-digit counts don't shift width as numbers change. */
.dialog-badge {
  /* Пілюля 20px, --ds-accent/--ds-on-accent (Артем 05.09.2026, Telegram-
     метрика) — замінює попередній warning-варіант: тепер це єдиний
     "непрочитане" індикатор у списку, і акцент саме тут читається як стан
     ("є що прочитати"), а не декорація. */
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.375rem;
  background: var(--ds-accent);
  color: var(--ds-on-accent);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-meta-size);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  border-radius: var(--ds-radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: transform 0.2s var(--ease-spring);
}

.dialog-item:hover .dialog-badge {
  transform: scale(1.1);
}

.dialog-item--selected .dialog-badge {
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
}

/* Status dot for P2P contacts */
.dialog-status-dot {
  display: none; /* крапка-статус = ШІ-патерн №2 (06.09.2026) */
  position: absolute;
  bottom: -0.0625rem;
  right: -0.0625rem;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 50%;
  /* --text-3 не існує ніде (ні в семантиці, ні в мості) — завжди падало на
     fallback #999. Найближча роль — text-muted, майже той самий сірий. */
  background: var(--ds-text-muted);
  border: 0.125rem solid var(--ds-surface-base);
  transition: background 0.3s ease;
}
.dialog-status-dot--online {
  /* Apple-зелений #34c759 не був жодним токеном системи; найближча роль
     за призначенням («онлайн» = success/accent) — run-done зелений
     (#2ea66e темна тема / #0d7a6a світла), трохи глибший за Apple-відтінок. */
  background: var(--ds-feedback-success);
}

/* Chat header status labels — 12px muted (Telegram-метрика, Артем
   05.09.2026): статус більше не сидить в одному рядку з іменем
   (margin-inline-start прибрано), а власним рядком під ним у
   `.chat-header__text` (ui-organisms/chat.rs). Онлайн-стан несе презенс-
   крапка (`.chat-header__dot`), не колір тексту — сам напис лишається
   одного тону в обох станах. */
.chat-header__status {
  font-size: var(--ds-meta-size);
  font-weight: 400;
  color: var(--ds-text-muted);
}

.chat-header__badge {
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.0625rem 0.3125rem;
  border-radius: var(--ds-radius-xs);
  margin-inline-start: 0.375rem;
}
.chat-header__badge--tg { background: var(--tg); color: var(--p-neutral-0); }
/* email-бренд #e65100 (глибокий оранж) не мав жодного токена; найближча
   існуюча роль за відтінком — feedback-warning (#f0883e, світліший амбер).
   Той самий компроміс нижче в .dialog-source--email. */
.chat-header__badge--email { background: var(--ds-feedback-warning); color: var(--p-neutral-0); }
.chat-header__badge--ai { background: var(--ds-accent); color: var(--ds-on-accent); }

/* Source indicator (tiny) */
.dialog-source {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.dialog-source--tg {
  color: var(--tg);
}

.dialog-source--email {
  color: var(--ds-feedback-warning);
  font-weight: 700;
}

.dialog-item--selected .dialog-source {
  color: var(--ds-text-muted);
}

/* «Показати ще» — хвіст списку діалогів. Стилю не було зовсім: кнопка
   падала на дефолт браузера посеред рядків. Тепер це рядок-у-рядок зі
   списком, з лічильником решти праворуч, щоб натиск був усвідомлений.
   Ф2a, з @layer overrides. */
.dialog-show-more-wrapper { padding: var(--ds-space-2) var(--ds-space-3) var(--ds-space-4); }
.dialog-show-more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  width: 100%;
  min-height: 2.75rem;               /* палець, не курсор */
  padding: 0 var(--ds-space-3);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 0;
  background: var(--ds-surface-raised);
  color: var(--ds-text-secondary);
  font: inherit;
  font-size: var(--ds-label-size);
  cursor: pointer;
  transition:
    background var(--ds-motion-fast) var(--ds-ease-out),
    color var(--ds-motion-fast) var(--ds-ease-out);
}
.dialog-show-more:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }
.dialog-show-more:active { transform: scale(0.99); }
.dialog-show-more__rest {
  font-size: var(--ds-meta-size);
  font-variant-numeric: tabular-nums;
  color: var(--ds-text-muted);
}
.dialog-show-more.htmx-request { color: var(--ds-text-muted); pointer-events: none; }

/* Дотяг до реалтайму: P2P-діалог показує, ЯК ідуть повідомлення —
   прямий datachannel (P2P, акцент) чи через сервер (тихо). */
.transport-badge {
  display: inline-flex;
  align-items: center;
  align-self: center;
  flex: 0 0 auto;
  margin-right: 0.25rem;
  padding: 0.125rem 0.375rem;
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-xs);
  font-family: var(--ds-font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ds-text-subtle);
  vertical-align: middle;
}
.transport-badge--direct {
  color: var(--ds-accent);
  border-color: color-mix(in srgb, var(--ds-accent) 45%, transparent);
  background: color-mix(in srgb, var(--ds-accent) 10%, transparent);
}
/* Рядок діалогу з живим прямим каналом — тонке акцентне кільце аватара. */
.dialog-item--direct .dialog-avatar {
  outline: 0.0625rem solid color-mix(in srgb, var(--ds-accent) 60%, transparent);
  outline-offset: 0.125rem;
}

/* ── Mute/Pin ── */
.dialog-item--muted .dialog-name { color: var(--ds-text-muted); }
/* Ф2b: !important прибрано без переписування в інший !important (§9.3).
   Розмітка завжди ставить ОБИДВА класи разом (dialog_list.rs:220,257) —
   `.dialog-item--muted .dialog-badge--muted`, той самий шар components,
   специфічність (0,3,0), бере гору над `.dialog-item--selected .dialog-badge`
   (0,2,0) навіть коли рядок і muted, і selected одночасно. */
.dialog-item--muted .dialog-badge--muted {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-subtle);
}
.dialog-ctx {
  position: fixed;
  z-index: 300;
  display: flex;
  flex-direction: column;
  min-width: 11rem;
  padding: 0.1875rem;
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  /* --ds-shadow-3: найближчий токен тіні для overlay/modal, див. пояснення
     біля .dimension-dropdown-menu вище. */
  box-shadow: var(--ds-shadow-3);
  /* Один --menu-p веде і відкриття, і закриття (@property у 00-base.css
     робить його інтерпольовним): перерваний на півдорозі рух їде назад
     з поточної точки, а не смикається. */
  --menu-p: 0;
  transform-origin: top left;
  transition: --menu-p 220ms linear(0, 0.26 8%, 0.72 20%, 0.96 30%, 1.04 42%, 1);
  opacity: var(--menu-p);
  transform: scale(calc(0.96 + 0.04 * var(--menu-p)));
}
.dialog-ctx[data-open] { --menu-p: 1; }

/* Сходинка живе в `--i`, а не в `transition-delay`: ланцюжок затримок при
   закритті спершу дограв би чергу і лише потім розвернувся б; спільний
   прогрес веде всі пункти назад однією кривою з їхніх поточних місць. */
.dialog-ctx button {
  transform: translateY(calc((1 - var(--menu-p)) * var(--i, 0) * 0.4375rem));
  opacity: calc(1 - (1 - var(--menu-p)) * (1 + var(--i, 0) * 0.35));
}
@media (prefers-reduced-motion: reduce) {
  .dialog-ctx { transition-duration: 90ms; transform: none; }
  .dialog-ctx button { transform: none; }
}
.dialog-ctx button {
  padding: 0.5rem 0.75rem;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-secondary);
  font-size: var(--ds-meta-size);
  cursor: pointer;
}
.dialog-ctx button:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }

.ptr-spinner {
  padding: 0.5rem;
  text-align: center;
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
}
}

/* ── 30-chat-messages.css ── */
/* ЧАТ І ПОВІДОМЛЕННЯ — шапка чату, стрічка, бабли, контекстне меню, панелі відповіді/редагування
 *
 * Найгустіша поверхня застосунку. Тримаємо її одним файлом, бо бабл,
 * його меню і панель відповіді ділять ті самі стани наведення.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   MAIN CHAT
   ═══════════════════════════════════════════ */

/* ── Chat body — flex child layout ────────────────────────────────── */
.chat-body {
  position: relative;
  flex: 1 1 0;              /* take all leftover vertical space */
  min-height: 0;            /* allow flex child to shrink below content */
  overflow: hidden;         /* pure clip; real scroll on .messages */
  display: flex;
  flex-direction: column;
}

/* Legacy .active class — no longer used, state via data-view */

.chat-header {
  display: grid;
  /* `auto` for the back button — when `display:none` on desktop, the
     track still exists. A fixed 2.25rem reserved the slot for content
     (since grid tracks are positional) and shoved the name into the
     tiny track. `auto` collapses to 0 when back is hidden. */
  grid-template-columns: auto 1fr auto;
  align-items: center;
  padding: 0.375rem var(--ds-space-2);
  /* Гап: назад-кнопка тулилась впритул до заголовка (0px). Ф2a: overrides. */
  gap: var(--ds-space-2);
  background: var(--ds-surface-base);
  min-height: 2.5rem;
  flex-shrink: 0;
  transition: transform 0.2s var(--ease-out);
}
.chat-header > span { margin-inline-start: var(--ds-space-1); }

/* Header stays put. The previous "auto-hide on scroll" trick reflowed
   `.chat-body` every time it toggled absolute positioning, which
   caused the jumpy-scroll the user was complaining about. Removed
   entirely — real messengers keep the chat header static. */

/* Glyph buttons: < and > as text characters */

.btn--back {
  width: 2.75rem;
  height: 2.75rem;
  font-size: 1.375rem;
  font-weight: 700;
  background: transparent;
  border-color: transparent;
  color: var(--ds-text-secondary);
  margin-inline-start: calc(var(--ds-space-1) * -1);
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.3s var(--ease-spring), color 0.15s;
}
/* Ф2a: overrides мав окреме .btn--back:hover{background,color} поверх
   цього translateX-ефекту — злито в одне правило (transform лишається,
   background/color з override перебивають старий колір-на-hover). */
.btn--back:hover { transform: translateX(-0.1875rem); background: var(--ds-surface-hover); color: var(--ds-text-primary); }
.btn--back:active { transform: translateX(-0.375rem) scale(0.9); transition-duration: 0.08s; }

#chat-header-content {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.375rem;
  min-width: 0;
  width: 100%;
  /* Explicit grid placement — back button (col 1) may be display:none
     on desktop, which otherwise makes this span the first auto-flow item
     and end up in the tiny col 1. Pin to middle track. */
  grid-column: 2;
}
.chat-header__calls { grid-column: 3; }
.chat-header__name {
  /* Let the name eat all free space inside the header, shrinking with
     ellipsis only when the column genuinely can't fit it. Without this
     the span's intrinsic width collapses to content, then overflow-hidden
     paints "ar..." at a narrow intrinsic size. */
  flex: 1 1 auto;
  min-width: 0;
}
.chat-header__calls {
  display: flex;
  gap: 0.125rem;
}
.chat-header__calls .btn--icon {
  width: 2rem;
  height: 2rem;
  /* color/hover: спільне .btn--icon (10-layout.css) вже несе
     ds-text-secondary/hover-tile — Ф2a видалив тутешній дубль
     (fg-secondary/accent), який @layer overrides вже глушив повністю. */
}
/* Аватар 36px + ім'я 16/600 + текстовий статус 12px muted (Telegram-
   метрика, Артем 05.09.2026) — `ChatHeader` раніше не мала аватара взагалі,
   а `.chat-header__name`/`__status` існували в CSS без жодної розмітки, що
   їх емітить. */
.chat-header__id {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-width: 0;
}
.chat-header__avatar {
  width: 2.25rem;
  height: 2.25rem;
  min-width: 2.25rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ds-eyebrow-size);
  font-weight: 600;
  text-transform: uppercase;
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  flex-shrink: 0;
  overflow: hidden;
}
.chat-header__avatar img { width: 100%; height: 100%; object-fit: cover; }
/* Той самий hsl(var(--avatar-hue)) прийом, що й у `.dialog-avatar[data-hue]`
   / `.message__avatar[style*="--avatar-hue"]` — колір рахується з числа на
   контакт, лишається задокументованим винятком (§8, 20-sidebar-dialogs.css). */
.chat-header__avatar[style*="--avatar-hue"] {
  background: hsl(var(--avatar-hue, 220), 20%, 25%);
  color: hsl(var(--avatar-hue, 220), 30%, 65%);
}
.chat-header__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 0.0625rem;
}
.chat-header__name-row {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}
.chat-header__dot {
  /* крапка-презенс = ШІ-патерн №2; стан несе напис «онлайн» (06.09.2026) */
  display: none;
}
.chat-header__name {
  font-size: 1rem;      /* 16px — та сама Telegram-метрика, що й .dialog-name */
  font-weight: 600;
  letter-spacing: -0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#chat-target {
  flex: 1;
  font-size: var(--t3);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#chat-target strong {
  color: var(--ds-text-primary);
}

/* ═══════════════════════════════════════════
   MESSAGES
   ═══════════════════════════════════════════ */

/* Emoji are user content; the font stack supplies OS emoji fallbacks. */
.message__text,
.message__quote,
.message__translation { font-variant-emoji: emoji; }

/* ── The one scroll root ────────────────────────────────────────────
   Stepanov §Concept: this is the `Scroller`. Everything UI-layer in
   `./scroll.js` talks to it via a tiny API — `atBottom()`,
   `toBottom()`, `preserveAnchor(fn)` — and nothing else needs to
   know it's scrollable. */
.messages {
  flex: 1 1 0;
  min-width: 0;                 /* forbid flex item from widening parent */
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain; /* stop scroll-chain up to document */
  padding: var(--ds-space-1) var(--ds-space-2) var(--ds-space-4);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;          /* between sender groups (Telegram-like rhythm) */
  position: relative;
  scroll-padding-bottom: var(--space-6);

  /* Native sticky-bottom: when new siblings are inserted while user
     is scrolled up, the browser preserves scroll anchor instead of
     forcing content up. Combined with scroll-to-bottom-on-send in JS
     gives proper messenger feel without per-insert scroll math. */
  overflow-anchor: auto;

  /* Allow native text selection for copy-paste. The swipe-to-reply
     gesture runs from message bubbles, not the scroll container. */
  -webkit-user-select: text;
  user-select: text;
}

/* Defence against horizontal overflow. Any descendant — image,
   `<audio>`, `<pre>`, long URLs, table, embedded player — is clamped
   to the column width. Fixes the horizontal scrollbar that appeared
   in the list when a message carried wide media. */
.messages * {
  min-width: 0;
  max-width: 100%;
}
.messages img,
.messages video,
.messages audio,
.messages pre,
.messages table,
.messages iframe {
  max-width: 100%;
  height: auto;
}
.messages pre,
.messages code {
  overflow-x: auto;             /* long code scrolls inside, not list */
  white-space: pre-wrap;
  word-break: break-word;
}

.message {
  content-visibility: auto;
  /* auto 56px (не 3rem=48px): Ф2a — з @layer overrides (там перемагала
     independent від шару компонентів). Off-screen бабли пропускають
     layout/paint; 56px — реальна середня висота рядка. */
  contain-intrinsic-size: auto 3.5rem;
  position: relative;
  display: flex;
  gap: var(--ds-space-2);
  /* 75% мобільний / 60% десктоп (Telegram-метрика, Артем 05.09.2026) —
     раніше 85% скрізь, бабл розтягувався майже на всю ширину екрана. */
  max-width: min(75%, 40rem);
  line-height: 1.4;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* Base state: no transition, no will-change. The earlier rule kept
     a GPU layer active per-message forever — compositor churn that
     showed up as visible scroll jitter once the list got long.
     Transitions are opted into by interaction states below. */
}
/* Hint the compositor only while an interaction is actually in
   flight, not for every idle bubble in the list. */
.message.swiping,
.message.pressing,
.message.swipe-return {
  will-change: transform;
  transition: transform 0.15s var(--ease-out), opacity 0.15s;
}
/* Long-press feedback — stronger than subtle scale so user feels the
   press register even when Vibration API is unavailable (iOS Safari).
   Scales down + brightens + ring expands outward like an ink drop. */
.message.pressing .message__bubble {
  transform: scale(0.94);
  opacity: 0.92;
  filter: brightness(1.15);
  box-shadow: 0 0 0 0.1875rem color-mix(in srgb, var(--ds-accent) 40%, transparent);
  transition:
    transform 0.2s var(--ease-spring),
    opacity 0.2s,
    filter 0.2s,
    box-shadow 0.3s ease-out;
}
/* Swipe-to-reply visual */
.message.swiping {
  transition: none;
}
.message.swiping .message__bubble {
  transition: none;
}
/* Spring-back when released */
.message.swipe-return {
  transition: transform 0.5s var(--ease-spring);
}
/* Reply arrow indicator that reveals during swipe */
.message .swipe-reply-hint {
  position: absolute;
  left: -2.5rem;
  top: 50%;
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translateY(-50%) scale(0.4) rotate(-30deg);
  opacity: 0;
  color: var(--ds-text-muted);
  transition: transform 0.25s var(--ease-spring), opacity 0.15s, color 0.15s;
  pointer-events: none;
}
/* Incoming: hint on left */
.message--incoming.swiping .swipe-reply-hint {
  opacity: 1;
  transform: translateY(-50%) scale(1) rotate(0);
}
.message--incoming.swipe-triggered .swipe-reply-hint {
  color: var(--ds-accent);
  transform: translateY(-50%) scale(1.2) rotate(0);
}
/* Outgoing: hint on right, mirrored */
.message--outgoing.swiping .swipe-reply-hint {
  opacity: 1;
  transform: translateY(-50%) scale(1) rotate(0) scaleX(-1);
}
.message--outgoing.swipe-triggered .swipe-reply-hint {
  color: var(--ds-accent);
  transform: translateY(-50%) scale(1.2) rotate(0) scaleX(-1);
}
/* Copy-feedback: spring micro-pulse + expanding accent halo. The old
   `inset` box-shadow animation read as a weird internal glow fighting
   the text. New version keeps the bubble legible — a 1.02× scale
   acknowledges the action, a single clean outer ring provides the
   "got it" pulse, then everything settles back in 420ms. */
@keyframes copyPulse {
  0%   { transform: scale(1);      box-shadow: 0 0 0 0 var(--ds-accent-border); }
  40%  { transform: scale(1.02);   box-shadow: 0 0 0 0.375rem var(--ds-accent-border); }
  70%  { transform: scale(1);      box-shadow: 0 0 0 0.625rem transparent; }
  100% { transform: scale(1);      box-shadow: 0 0 0 0 transparent; }
}
.message.copied .message__bubble {
  animation: copyPulse 0.42s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Respect reduced-motion: replace animation with a brief border tint. */
@media (prefers-reduced-motion: reduce) {
  .message.copied .message__bubble {
    animation: none;
    outline: 0.125rem solid var(--ds-accent);
    outline-offset: -0.125rem;
    transition: outline-color 0.5s ease-out;
  }
}
/* Allow text selection inside bubble text only */
.message__text {
  -webkit-user-select: text;
  user-select: text;
}
/* On touch devices, text selection steals the long-press gesture — iOS
   shows selection handles + callout before our 500ms context-menu timer
   fires. Disable selection for touch; desktop mouse still gets it. */
@media (hover: none) and (pointer: coarse) {
  .message__text {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
}

/* Дефект №7 (план §2): час повідомлення був видимий лише на
   hover/`.show-time` (`opacity:0; max-width:0`) — на мобільному, де
   hover не існує, час не показувався взагалі. Тепер видимий завжди,
   без hover-трюку; нижче (не-roompost/roompost) лише позиціонують і
   стилізують той самий завжди-видимий елемент. */
.message__time {
  opacity: 1;
  max-width: none;
  overflow: visible;
}

.message.message--new {
  animation: msgIn 0.4s var(--ease-spring);
}

@media (width > 48em) {
  .message { max-width: min(60%, 40rem); }
}

.message--incoming {
  align-self: flex-start;
}

.message--outgoing {
  align-self: flex-end;
  flex-direction: row-reverse;
}

.message__avatar {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  display: none; /* Hidden in 1:1 chats */
  align-items: center;
  justify-content: center;
  font-size: 0.625rem;
  font-weight: 600;
  flex-shrink: 0;
  text-transform: uppercase;
  align-self: flex-end;
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
}
/* Show avatars in group/email/AI chats */
.messages--group .message__avatar { display: flex; }

.message--incoming .message__avatar {
  background: hsl(var(--avatar-hue, 220), 20%, 25%);
  color: hsl(var(--avatar-hue, 220), 30%, 65%);
}

.message--outgoing .message__avatar {
  background: var(--ds-accent-subtle);
  color: var(--ds-accent);
}

.message__avatar[style*="--avatar-hue"] {
  background: hsl(var(--avatar-hue), 20%, 25%);
  color: hsl(var(--avatar-hue), 30%, 65%);
}

/* Bubble geometry — messenger-standard asymmetric radius.
   Standalone / first-in-group: full radius on three corners, a small
   "tail" radius on bottom-speaker-side (incoming→bottom-left,
   outgoing→bottom-right). The tail replaces a full SVG chat-tail —
   cheaper to render, reads the same ("this bubble points at me").
   Grouped (consecutive same-sender, not first): also tightens the
   TOP-speaker-side corner so consecutive bubbles visually join. */
.message__bubble {
  position: relative;
  padding: var(--ds-space-2) var(--ds-space-3);   /* 8px / 12px per Awwwards chat-UI research */
  border-radius: var(--ds-radius-md);
  font-size: var(--t3);
  line-height: 1.5;
  min-width: 3rem;  /* prevent "Tt"-narrow bubbles from looking like badges */
  overflow: hidden;
  overflow-wrap: break-word;
  word-break: break-word;
  /* Мінімалізм швидкості (Superhuman): без тіні, без `transition: all` —
     будь-яка тінь чи blur перемальовується на кожному кадрі скролу.
     Ф2a: раніше жило в @layer overrides. */
  box-shadow: none;
}

/* Дефект №10 (план §2): вхідний бабл читає компонентний токен
   `--ds-bubble-in-bg` (Ф1 перевизначає його на surface-overlay в
   00-tokens-next.css) — сам файл більше не вирішує, ЯКА це поверхня. */
.message--incoming .message__bubble {
  background: var(--ds-bubble-in-bg);
  color: var(--ds-bubble-in-fg);
  border-bottom-left-radius: var(--ds-radius-xs);
}
/* Дефект №10: вихідний бабл — синій tint БЕЗ рамки (не зелений tint +
   зелена рамка, як було). Рамка прибрана: на tint-фоні 1px border того ж
   кольору тільки заважав, а лінк усередині раніше йшов у --ds-accent
   (зелений на зеленому — низький контраст); нижче лінк отримує
   --ds-text-primary + підкреслення. Обидва читають tier-3 токени
   (`--ds-bubble-out-*`, перевизначені в 00-tokens-next.css), а не
   `--ds-accent-*` напряму — той самий токен, що описаний у DESIGN_SYSTEM
   §9.1 "Bubble", тепер справді використовується. */
.message--outgoing .message__bubble {
  background: var(--ds-bubble-out-bg);
  color: var(--ds-bubble-out-fg);
  border: 0.0625rem solid var(--ds-bubble-out-border);
  border-bottom-right-radius: var(--ds-radius-xs);
}

/* Лінк усередині вихідного бабла: --ds-text-primary з підкресленням
   замість --ds-accent (був низький контраст зеленого на зеленому tint-і,
   а на новому синьому tint-і акцентний лінк все одно зливався б з
   фоном-tint-ом того ж кольору). */
.message--outgoing .message__text a {
  color: var(--ds-text-primary);
  border-bottom: none;
  text-decoration: underline;
  text-underline-offset: 0.125em;
}
.message--outgoing .message__text a:hover {
  color: var(--ds-text-primary);
  opacity: 0.85;
}

/* Grouped-bubble adaptive radius (Facebook Messenger technique via
   CSS logical properties — auto-flips for RTL). Consecutive messages
   from same sender "stack" with sharp corners facing each other and
   rounded corners outward, without needing per-position classes. */
.message--grouped .message__bubble {
  border-start-start-radius: var(--ds-radius-xs);
}
.message--outgoing.message--grouped .message__bubble {
  border-start-end-radius: var(--ds-radius-xs);
  border-start-start-radius: var(--ds-radius-md);  /* reset incoming's */
}
/* When this bubble has a grouped follower, its "tail" corner becomes
   sharp — the next bubble continues the thread visually. */
.message:not(:last-child).message--incoming:has(+ .message--incoming.message--grouped) .message__bubble {
  border-end-start-radius: var(--ds-radius-xs);
}
.message:not(:last-child).message--outgoing:has(+ .message--outgoing.message--grouped) .message__bubble {
  border-end-end-radius: var(--ds-radius-xs);
}

/* Grouped — tighter vertical rhythm only. We deliberately do NOT change
   the border-radius of grouped bubbles: doing so used to produce a
   visual mismatch where the first-of-group had 1 sharp corner while the
   rest had 2, reading as "half round / half square". Uniform radii +
   compressed gap gives the same "thought cluster" feel with zero
   corner inconsistency. */
.messages .message.message--grouped {
  margin-top: -0.25rem;    /* collapses the 8px group gap to 4px within a group */
}

/* [data-theme="light"] .message--outgoing .message__bubble { background: rgba(0,0,0,.04); color: var(--ds-text-primary) }
   видалено Ф2a: з @layer overrides `.message--outgoing .message__bubble`
   (вище) перебивав background/color для ОБОХ тем незалежно від
   специфічності (шар вищий за будь-яку специфічність) — це правило не
   рендерилось НІ В ОДНІЙ темі й до цієї правки. accent-light/accent-glow
   у мерджі вище — токени per-theme, тож світла тема адаптується сама. */

/* Grouped messages — hide repeated avatar/name */
.message--grouped .message__avatar {
  visibility: hidden;
}

/* Згруповані бабли (дефект №7, друга половина; 06.09.2026): інлайн
   `display:none` на header у message_list.rs прибрано, стан несе клас
   `.message--grouped`. У групі ховаємо лише ім'я відправника — час і
   статус лишаються в кутку кожного бабла, як у Telegram. */
.message--grouped .message__sender,
.message--grouped .message__sender-badge {
  display: none;
}

/* Legacy flat-radius + extra margin-top overrides on grouped bubbles
   removed — they were the actual source of "half round / half square":
   first bubble kept the asymmetric tail (--radius + --radius-xs), every
   grouped one got reset to a uniform --radius-sm with no tail. Now all
   bubbles share geometry; the margin-top collapse for grouping is
   defined once, in the `.messages .message.message--grouped` rule above. */

@keyframes msgIn {
  from {
    opacity: 0;
    transform: translateY(0.625rem) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.message__header {
  display: none; /* Hidden by default in 1:1 chats */
  align-items: center;
  gap: var(--ds-space-2);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  margin-bottom: 0.125rem;
}

/* Show header only in group/email/AI chats */
.messages--group .message__header { display: flex; }

/* Telegram-метрика meta-рядок (Артем 05.09.2026) — реальний баг: `.messages
   --group` НІКОЛИ не ставиться жодним контейнером (лишається порожнім
   класом-заглушкою, живим лише в декларації вище), а `.message__header`
   ховається за замовчуванням — тому час НІКОЛИ не показувався у звичайних
   1:1/AI/email повідомленнях, навіть на hover (`.message__time` чекав на
   видимий header, а той лишався display:none назавжди). Роумпости мають
   власний header (72-rooms.css) — виключаємо їх тут, щоб не малювати
   header двічі. Час (+галочка статусу для вихідних) тепер завжди в кутку
   бабла, як у Telegram. */
.message:not(.message--roompost) .message__header {
  display: flex;
  position: absolute;
  right: var(--ds-space-2);
  bottom: 0.3125rem;
  gap: 0.25rem;
  align-items: center;
  margin: 0;
  pointer-events: none;
  /* Короткий текст («Тут ?») робив бабл вужчим за мета-рядок, і час
     ламався на два рядки, наїжджаючи на текст (скрін Артема 07.09.2026).
     Мета ніколи не переноситься, а бабл тримає під неї ширину нижче. */
  white-space: nowrap;
}
.message:not(.message--roompost) .message__sender { display: none; }
.message:not(.message--roompost) .message__time {
  opacity: 0.82;
  max-width: none;
  overflow: visible;
  font-size: 0.6875rem;  /* 11px — усередині бабла, поруч із текстом */
}
.message:not(.message--roompost) .message__bubble {
  padding-bottom: 1.375rem;  /* місце під meta-рядок знизу */
  /* Ширина під час + галочки (11px «19:20» + 14px значок + гапи + падінги):
     менше — і абсолютна мета вилазить за край бабла. */
  min-width: 5rem;
}

.message--outgoing .message__header {
  justify-content: flex-end;
}

.message--system {
  align-self: center;
  max-width: 90%;
}

.message--system .message__bubble {
  background: var(--ds-surface-hover);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-md);
  text-align: center;
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  padding: var(--ds-space-2) var(--ds-space-4);
}

.message--system .message__avatar {
  display: none;
}

/* Бабл із черги: написане без мережі. Приглушений, щоб не плутати з тим,
   що вже пішло, але видимий — саме цього бракувало, коли повідомлення
   просто зникало разом із текстом у полі. Ф2a: раніше @layer overrides.
   Порядок: .message__time--pending мусить лишатись ПЕРЕД загальним
   .message__time нижче — той пізніший і виграє color для елементів з
   обома класами (той самий трейд-оф, що був в overrides). */
.message--pending .message__bubble {
  opacity: 0.7;
  background: transparent;
  border: 0.0625rem dashed var(--ds-border-strong);
}
.message__time--pending {
  opacity: 1;                        /* час у баблі ховається до hover, підпис черги — ні */
  color: var(--ds-text-muted);
  font-style: italic;
}

.message__sender {
  font-weight: var(--ds-weight-strong);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
}

.message__time {
  /* Дефект №11: Inter tabular-nums, не mono — mono лишається тільки для
     code/pre (95-utilities.css .message__text pre/code). */
  font-family: var(--ds-font);
  font-size: var(--font-size-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ds-text-muted);
  letter-spacing: 0.01em;
}

.message__subject {
  font-weight: 600;
  font-size: 0.85em;
  opacity: 0.7;
  margin-bottom: 0.25rem;
  font-style: italic;
}

.message__status {
  font-size: 0.625rem;
  opacity: 0.6;
}

.message__status--sent,
.message__status--delivered,
.message__status--read {
  display: inline-flex;
  width: 1rem;
  height: 1rem;
}

.message__status--sent {
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
}

.message__status--delivered {
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
}

.message__status--read {
  background: var(--ds-text-primary);  /* monochrome: read = brighter than delivered */
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z'/%3E%3C/svg%3E")
    center/contain no-repeat;
}

.message__lock {
  display: inline-flex;
  opacity: 0.5;
}

/* Email signature pinned footer */
.email-signatures {
  display: flex;
  gap: var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-3);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  background: var(--ds-surface-raised);
  max-height: 30vh;
  overflow-y: auto;
  flex-shrink: 0;
}

.email-sig {
  flex: 1;
  padding: var(--ds-space-1) var(--ds-space-2);
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-label-size);
  line-height: 1.3;
  color: var(--ds-text-secondary);
  background: var(--ds-surface-base);
  cursor: pointer;
  max-height: 5em;
  overflow: hidden;
  transition: max-height 0.2s;
  white-space: pre-line;
}

.email-sig.expanded {
  max-height: none;
}

.email-sig__label {
  font-weight: 600;
  font-size: var(--ds-meta-size);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--ds-space-1);
  color: var(--ds-text-muted);
}

.email-sig--incoming {
  color: var(--ds-text-muted);
}

.email-sig--outgoing {
  /* кольорова кромка прибрана (ШІ-патерн №1, 06.09.2026) */
  color: var(--ds-text-muted);
}

/* Typing indicator — wave style */
.typing-indicator {
  display: none;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-3) var(--ds-space-4);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  flex-shrink: 0;
}

.typing-indicator.visible {
  display: flex;
  animation: fadeIn 0.2s ease-out;
}

.typing-indicator__dots {
  display: flex;
  gap: 0.375rem;
}

.typing-indicator__dots span {
  width: 0.375rem;
  height: 0.375rem;
  background: var(--ds-accent);
  border-radius: 50%;
  animation: typingWave 1.2s ease-in-out infinite;
}

.typing-indicator__dots span:nth-child(2) {
  animation-delay: 0.15s;
}

.typing-indicator__dots span:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes typingWave {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.4;
  }
  30% {
    transform: translateY(-0.25rem);
    opacity: 1;
  }
}

/* ═══════════════════════════════════════════
   LAZY LOAD SPINNER
   ═══════════════════════════════════════════ */
.lazy-load-spinner {
  text-align: center;
  padding: 0.5rem;
  font-size: 0.75rem;
  color: var(--ds-text-secondary);
  opacity: 0.7;
}

/* ═══════════════════════════════════════════
   MESSAGE CONTEXT MENU
   ═══════════════════════════════════════════ */
/* iOS-style context menu — full-screen backdrop blur, pressed bubble
   scales up, menu pops with overshoot spring. Mirrors Telegram iOS +
   UIKit `UIContextMenuInteraction` pattern (rockyshikoku medium). */
/* Z-order stack: toasts live at 9999+, so the long-press UI sits above
   the page but below critical system toasts. */
.msg-context-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9996;
  background: transparent;
  pointer-events: none;
  transition: background 0.2s ease;
}
.msg-context-backdrop--active {
  background: color-mix(in srgb, var(--p-neutral-1000) 45%, transparent);
  pointer-events: auto;
}

/* Pressed bubble scales up and lifts ABOVE the backdrop. The
   `isolation: isolate` forces a new stacking context so the bubble's
   z-index actually wins against siblings in `.messages`. */
.message--menu-open {
  position: relative;
  z-index: 9997;
  isolation: isolate;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform: scale(1.03);
  filter: drop-shadow(0 0.5rem 1.5rem color-mix(in srgb, var(--p-neutral-1000) 35%, transparent));
}

.msg-context-menu {
  position: fixed;
  z-index: 9998;
  flex-direction: column;
  min-width: 11rem;
  max-width: calc(100vw - 1rem);
  /* No overflow — 4 actions (reply/copy/edit/delete) always fit. A
     scrollbar inside the menu looked ugly and clipped the corner radius. */
  overflow: visible;
  /* glass (ШІ-патерн №7) прибрано 06.09.2026: суцільна поверхня + межа */
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: 0.75rem;
  box-shadow: 0 0.75rem 2rem color-mix(in srgb, var(--p-neutral-1000) 45%, transparent);
  padding: 0.375rem;
  gap: 0.125rem;
  opacity: 0;
  transform: scale(0.85);
  transform-origin: top left;  /* overridden inline per msg alignment */
  transition: opacity 0.18s ease-out,
              transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
}
.msg-context-menu--visible {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .message--menu-open,
  .msg-context-menu { transition-duration: 0s; }
}
.msg-context-menu__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.625rem 0.875rem;
  background: none;
  border: none;
  color: var(--ds-text-primary);
  font-size: var(--ds-label-size);
  cursor: pointer;
  border-radius: 0.125rem;
  text-align: left;
  transition: background 0.15s, transform 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.msg-context-menu__item:active {
  transform: scale(0.96);
}
.msg-context-menu__item:hover {
  background: var(--ds-surface-hover);
}
.msg-context-menu__item--danger {
  color: var(--ds-feedback-danger);
}
.msg-context-menu__item--danger:hover {
  background: color-mix(in srgb, var(--ds-feedback-danger) 18%, transparent);
}

/* ═══════════════════════════════════════════
   EDIT BAR
   ═══════════════════════════════════════════ */
.edit-bar {
  display: flex;
  align-items: center;
  padding: 0.375rem 0.75rem;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  gap: 0.5rem;
}
.edit-bar__content {
  flex: 1;
  min-width: 0;
}
.edit-bar__label {
  font-size: 0.7rem;
  color: var(--ds-accent);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03125rem;
}
.edit-bar__text {
  font-size: 0.8rem;
  color: var(--ds-text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.edit-bar__close {
  background: none;
  border: none;
  color: var(--ds-text-secondary);
  cursor: pointer;
  padding: 0.25rem;
  border-radius: 0.25rem;
  flex-shrink: 0;
}
.edit-bar__close:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
}

/* Edited message indicator */
.message__edited {
  font-size: 0.65rem;
  color: var(--ds-text-secondary);
  opacity: 0.7;
  margin-inline-start: 0.25rem;
}

/* ═══════════════════════════════════════════
   DROP OVERLAY
   ═══════════════════════════════════════════ */

.drop-overlay {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--p-neutral-1000) 70%, transparent);
  align-items: center;
  justify-content: center;
}

.drop-overlay.visible {
  display: flex;
}

.drop-overlay__content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-3);
  color: var(--ds-text-primary);
  padding: var(--space-8);
  border: 0.125rem dashed var(--ds-accent);
  border-radius: var(--ds-radius-md);
  animation: fadeIn 0.15s ease-out;
}

.drop-overlay__text {
  font-size: var(--t2);
  font-weight: 500;
}

/* ═══════════════════════════════════════════
   REPLY BAR
   ═══════════════════════════════════════════ */

.reply-bar {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-2);
  background: var(--ds-surface-overlay);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  flex-shrink: 0;
}

.reply-bar__content {
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.reply-bar__sender {
  font-size: var(--ds-meta-size);
  font-weight: 600;
  color: var(--ds-accent);
}

.reply-bar__text {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.reply-bar__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  background: none;
  border: none;
  color: var(--ds-text-muted);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  border-radius: var(--ds-radius-xs);
}

.reply-bar__close:hover {
  color: var(--ds-text-primary);
}

/* Quoted text in messages */
.message__text blockquote,
.message__text .quote {
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  padding: var(--ds-space-1) var(--ds-space-2);
  margin: 0 0 var(--ds-space-1);
  color: var(--ds-text-secondary);
  font-size: var(--ds-label-size);
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(0.25rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message__text {
  color: var(--ds-text-primary);
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* Links in message text: single definition in the LINKIFIED TEXT block
   (utilities layer) — two older shadowed copies removed. */

/* Email formatting */
.email-content {
  display: grid;
  gap: var(--ds-space-2);
}

/* Email stays a messenger message, but its own header follows mail
   hierarchy: sender and time above subject and body. The wrapper emitted by
   both email formatters keeps these rules away from every other channel. */
.message:has(.email-content) {
  width: min(90%, 48rem);
  max-width: none;
}

@media (width > 48em) {
  .message:has(.email-content) {
    width: min(78%, 48rem);
  }
}

.message:has(.email-content) .message__header {
  position: static;
  display: flex;
  gap: var(--ds-space-2);
  margin: 0 0 var(--ds-space-2);
  pointer-events: auto;
}

.message:has(.email-content) .message__sender {
  display: inline;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ds-text-secondary);
}

.message:has(.email-content) .message__time {
  flex: 0 0 auto;
  opacity: 1;
}

.message:has(.email-content) .message__bubble {
  width: 100%;
  padding-bottom: var(--ds-space-3);
}

.messages .message.message--grouped:has(.email-content) {
  margin-top: 0;
}

.email-subject {
  font-weight: var(--ds-weight-strong);
  line-height: 1.35;
  padding-bottom: var(--ds-space-2);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.email-body {
  line-height: 1.55;
  overflow-wrap: anywhere;
  color: var(--ds-text-primary);
}

.email-body :where(p, div, span, td, th, li, font) {
  color: inherit;
}

.email-history {
  margin-top: var(--ds-space-3);
  color: var(--ds-text-secondary);
}

.email-history__summary {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: 2.25rem;
  padding: var(--ds-space-1) var(--ds-space-2);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  font-size: var(--ds-label-size);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.email-history__summary::-webkit-details-marker {
  display: none;
}

.email-history__summary::before {
  content: "›";
  flex: 0 0 auto;
  color: var(--ds-text-subtle);
  transform: rotate(0);
  transition: transform 0.18s var(--ease-out);
}

.email-history[open] > .email-history__summary::before {
  transform: rotate(90deg);
}

.email-history__content {
  display: grid;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2);
  font-size: var(--ds-label-size);
  overflow-wrap: anywhere;
}

.email-history__content > .email-quote,
.email-history__content > blockquote {
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.email-history__content table,
.email-history__content img {
  max-width: 100%;
  height: auto;
}

.email-quote {
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  padding: var(--ds-space-1) var(--ds-space-2);
  margin: var(--ds-space-1) 0;
  color: var(--ds-text-secondary);
  font-size: var(--ds-label-size);
}

.email-signature {
  margin-top: var(--ds-space-2);
  padding-top: var(--ds-space-1);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  color: var(--ds-text-secondary);
  font-size: var(--ds-label-size);
}

.message__original {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  margin-top: var(--ds-space-2);
  padding-top: var(--ds-space-2);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  font-style: italic;
  opacity: 0.7;
}

/* Дочитування історії впало. Раніше помилка йшла тільки в консоль, і решта
   треда мовчки ставала недосяжною до перевідкриття. Ф2a: раніше overrides. */
.lazy-load-retry {
  display: block;
  width: 100%;
  min-height: 2.75rem;
  margin: 0 0 var(--ds-space-2);
  border: 0.0625rem dashed var(--ds-border-default);
  background: transparent;
  color: var(--ds-text-secondary);
  font: inherit;
  font-size: var(--ds-label-size);
  cursor: pointer;
}
.lazy-load-retry:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }
}

/* У черзі P2P (rtc.js ChatLink): ще не пішло, лежить у відправника
   до появи прямого каналу. Годинник замість галочки. */
.message__status--queued {
  display: inline-flex;
  width: 1rem;
  height: 1rem;
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 110-16 8 8 0 010 16zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 110-16 8 8 0 010 16zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7z'/%3E%3C/svg%3E")
    center/contain no-repeat;
}

/* Шифровані повідомлення: поки текст не підставлено з локальної IDB
 * (E2E.v3.fillStubs), бабл читається як службовий напис. */
.message--e2e:not([data-filled="1"]) .message__text {
  font-style: italic;
  opacity: 0.65;
}

/* ── 40-composer.css ── */
/* ПОЛЕ ВВОДУ І КНОПКИ
 *
 * Секції INPUT і BUTTONS стоять поруч у вихідному аркуші й посилаються
 * одна на одну (кнопки відправки живуть усередині поля вводу).
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   INPUT
   ═══════════════════════════════════════════ */

/* ── Composer ───────────────────────────────────────────────────────
   Pinned to the bottom of the chat column by virtue of DOM order
   (last child of `.main`). `flex-shrink: 0` keeps it from collapsing
   when the keyboard or virtual viewport shortens height. Grid uses
   implicit track sizing — children define their own widths via
   `auto` — so adding or removing an icon button doesn't require
   rewriting column-count arithmetic. */
/* Floating "scroll to newest" button. Hidden by default; scroll.js
   toggles `--visible` when user scrolls up from the bottom. Lives
   inside `.chat-body` (position: relative), anchored bottom-right. */
.scroll-bottom-fab {
  position: absolute;
  right: var(--ds-space-3);
  bottom: var(--ds-space-3);
  width: 2.75rem;
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ds-surface-active);
  color: var(--ds-text-primary);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 50%;
  box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, var(--p-neutral-1000) 25%, transparent);
  cursor: pointer;
  opacity: 0;
  transform: translateY(0.75rem);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 5;
}
.scroll-bottom-fab--visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.scroll-bottom-fab:hover { background: var(--ds-surface-hover); border-color: var(--ds-accent); }
.scroll-bottom-fab svg { width: 1.25rem; height: 1.25rem; stroke: currentColor; }
.scroll-bottom-fab__badge {
  position: absolute;
  top: -0.25rem;
  right: -0.25rem;
  min-width: 1.125rem;
  height: 1.125rem;
  padding: 0 0.375rem;
  background: var(--ds-accent);
  color: var(--ds-on-accent);
  font-size: var(--ds-eyebrow-size);
  font-weight: 600;
  line-height: 1.125rem;
  border-radius: 0.5625rem;
  text-align: center;
}
.scroll-bottom-fab__badge[hidden] { display: none; }

/* Visually hidden but accessible to screen readers (a11y convention) —
   absolute positioning takes it out of grid flow, otherwise the <label>
   becomes a visible empty cell that breaks the 4-column input-area grid. */
.sr-only {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  padding: 0;
  margin: -0.0625rem;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.email-meta {
  padding: var(--ds-space-2) var(--ds-space-3);
  font-size: 0.75rem;
  color: var(--ds-text-subtle);
  display: grid;
  grid-template-columns: minmax(12rem, 0.8fr) minmax(16rem, 1.5fr) auto;
  gap: var(--ds-space-2) var(--ds-space-3);
  align-items: center;
  border-top: 0.0625rem solid var(--ds-border-subtle);
}
.email-meta__field {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--ds-space-2);
  align-items: center;
  min-width: 0;
}
.email-meta__label {
  color: var(--ds-text-subtle);
  font-size: var(--ds-meta-size);
  white-space: nowrap;
}
.email-meta__from {
  font-family: monospace;
  font-size: inherit;
  color: var(--ds-text-primary);
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 0;
  padding: 0.15rem 0;
  width: 100%;
  min-width: 0;
  outline: none;
}
.email-meta__from:focus {
  border-bottom-color: var(--ds-text-primary);
}
.email-meta__subject {
  width: 100%;
  min-width: 0;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--ds-text-primary);
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 0;
  padding: 0.15rem 0.25rem;
  outline: none;
}
.email-meta__subject:focus {
  border-bottom-color: var(--ds-text-primary);
}
.email-meta__sig {
  justify-self: end;
  font-style: italic;
  opacity: 0.7;
  white-space: nowrap;
}

@media (max-width: 48em) {
  .email-meta {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .email-meta__field--from {
    grid-column: 1;
    grid-row: 1;
  }
  .email-meta__field--subject {
    grid-column: 1 / -1;
    grid-row: 2;
    grid-template-columns: 1fr;
    gap: 0;
  }
  .email-meta__sig {
    grid-column: 2;
    grid-row: 1;
  }
}

.input-area {
  display: grid;
  grid-template-columns: auto auto 1fr auto;  /* attach | voice | text | send */
  align-items: end;
  gap: 0;
  /* iOS screen corners are radius ~44-48pt — keep buttons inside the
     safe viewing area. Horizontal padding scales with safe-area-inset
     so landscape-notch phones keep buttons off the camera housing. */
  padding: var(--ds-space-2);
  padding-inline-start: max(var(--ds-space-3), env(safe-area-inset-left));
  padding-inline-end: max(var(--ds-space-3), env(safe-area-inset-right));
  padding-bottom: max(var(--ds-space-2), env(safe-area-inset-bottom));
  /* iOS-style elevation + rounded top corners matching device curve.
     Top border-radius so the composer "hugs" the bottom of the screen
     instead of meeting it at a 90° angle, which looks clipped against
     iPhone's rounded display. */
  /* Solid surface, нуль backdrop-blur (Ф2a, з @layer overrides): фрости
     перемальовують blur на кожному кейстроці й кадрі скролу за собою —
     найгірший ворог швидкості вводу. У нас реальні токени поверхонь,
     фейкова глибина через blur не потрібна. */
  background: var(--ds-surface-raised);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-top: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 1rem 1rem 0 0;
  box-shadow:
    0 -0.25rem 0.75rem color-mix(in srgb, var(--p-neutral-1000) 12%, transparent),
    0 -0.75rem 2rem color-mix(in srgb, var(--p-neutral-1000) 6%, transparent);
  flex-shrink: 0;
  position: relative;
  z-index: 2;  /* above messages list so shadow shows */
}

/* ── Voice recording overlay ───────────────────────────────────────────
   rtc.js builds `.voice-rec` inside `.input-area` while holding the mic.
   It had NO css, so the inline mic <svg> (no width) fell back to the
   replaced-element default (~300px) and a giant mic swallowed the screen.
   Style it as a compact Telegram-style bar over the composer. */
.voice-rec {
  position: absolute;
  inset: 0;
  display: none;                 /* shown via --visible */
  align-items: center;
  gap: var(--ds-space-2);
  padding-inline: var(--ds-space-4);
  background: var(--ds-surface-active);
  border-radius: inherit;
  z-index: 3;
}
.voice-rec--visible { display: flex; }

.voice-rec__mic {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--ds-feedback-danger);
  animation: voice-rec-pulse 1.2s ease-in-out infinite;
}
.voice-rec__mic svg { width: 100%; height: 100%; display: block; }

@keyframes voice-rec-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

.voice-rec__wf {
  flex: 0 0 auto;
  width: 6rem;
  height: 1.75rem;
  opacity: 0.8;
}

.voice-rec__timer {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: var(--ds-label-size);
  color: var(--ds-text-primary);
}

.voice-rec__live {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
}

.voice-rec__hint {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
}

/* Slide far enough left and the release will cancel — make that state loud. */
.voice-rec--cancel-intent .voice-rec__mic { color: var(--ds-feedback-danger); }
.voice-rec--cancel-intent .voice-rec__hint {
  color: var(--ds-feedback-danger);
  font-weight: 600;
}

/* Floating pill composer — uses a CONTAINER query on `.main` so it
   kicks in when the chat column itself is narrow (split-screen, DevTools
   open, sidebar visible), not just on small viewports. Viewport-based
   media query missed the common case of a narrow chat column on desktop. */
.main { container-type: inline-size; container-name: chat; }
@container chat (max-width: 48em) {
  .input-area {
    margin-inline: max(var(--ds-space-2), env(safe-area-inset-left));
    margin-inline-end: max(var(--ds-space-2), env(safe-area-inset-right));
    margin-bottom: max(var(--ds-space-2), env(safe-area-inset-bottom));
    padding-bottom: var(--ds-space-2);
    border-radius: 1.25rem;
    border: 0.0625rem solid var(--ds-border-subtle);
  }
}
@media (max-width: 48em) {
  .input-area {
    margin-inline: max(var(--ds-space-2), env(safe-area-inset-left));
    margin-inline-end: max(var(--ds-space-2), env(safe-area-inset-right));
    margin-bottom: max(var(--ds-space-2), env(safe-area-inset-bottom));
    padding-bottom: var(--ds-space-2);
    border-radius: 1.25rem;
    border: 0.0625rem solid var(--ds-border-subtle);
  }
}

.input-autocomplete {
  bottom: 100%;
  top: auto;
  margin-bottom: 0.25rem;
  margin-top: 0;
  box-shadow: 0 -0.25rem 0.75rem color-mix(in srgb, var(--p-neutral-1000) 30%, transparent);
}

/* Випадний список малює операційна система, і на Linux вона бере свою
   світлу тему навіть при `color-scheme: dark` — а текст пунктів
   успадковує наш майже білий колір, тож список виходив білим по білому
   (знімок 10.09.2026). Колір пунктів задаємо явно: Chrome і Firefox
   поважають `background-color` на `option`, macOS малює нативно і
   слухає `color-scheme`. */
select.input { cursor: pointer; }
select.input option,
select.input optgroup {
  background-color: var(--ds-surface-raised);
  color: var(--ds-text-primary);
}

.input {
  flex: 1;
  padding: var(--ds-space-2) var(--ds-space-3);
  font-size: 1rem;
  font-family: var(--ds-font);
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 0;
  color: var(--ds-text-primary);
  outline: none;
  min-height: 2.75rem;  /* HIG 44pt */
  max-height: clamp(6.25rem, 25vh, 12.5rem);
  resize: none;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  line-height: 1.4;
  field-sizing: content;
  transition: background var(--duration-fast) var(--ease-out);
}
.input:focus,
.input:focus-visible {
  background: transparent;
  border-bottom-color: var(--ds-accent);
  box-shadow: none;
  outline: none;     /* kill UA rectangular ring (incompatible with the
                        rounded `.input-area` container — the user
                        called this out: "квадратное на круглом") */
}

.input::placeholder {
  color: var(--ds-text-subtle);
}

/* ═══════════════════════════════════════════
   BUTTONS
   ═══════════════════════════════════════════ */

/* Base .btn + .btn--primary live further down (single definition).
   A second full copy used to sit here and silently lost every conflicting
   property to it — same-layer shadowing, the search-field bug class. */
.btn:focus-visible {
  outline: 0.125rem solid var(--ds-accent);
  outline-offset: 0.125rem;
}

.btn.btn--send {
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--ds-text-muted);
  border: none;
  border-radius: var(--ds-radius-full);
  cursor: pointer;
  transition:
    background 0.2s ease,
    transform 0.3s var(--ease-spring),
    color 0.2s;
  position: relative;
  /* Long-press to record — iOS defaults cancel pointerdown mid-press:
     - tap-highlight-color shows a grey flash
     - touch-callout shows the system "copy/look up" menu after ~500ms
     - user-select triggers text selection
     - touch-action: manipulation prevents double-tap zoom from eating the press
     All four must be off for the hold gesture to survive to pointerup. */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}
.send-icon {
  position: absolute;
  transition: opacity 0.25s, transform 0.35s var(--ease-spring);
}
.send-icon--arrow { opacity: 0; transform: scale(0.3) rotate(-90deg); font-size: 1.375rem; font-weight: 700; }
.send-icon--mic { opacity: 1; transform: scale(1); }
/* When typing -- show arrow, hide mic. Send: тихий mic доки не набрано
   текст, тоді ЗАПОВНЕНЕ акцентне коло — найзрозуміліший афорданс "це
   надсилає" (Telegram/Superhuman). Ф2a: раніше @layer overrides. */
/* .btn.btn--send…: специфічність 0,3,0, бо `.btn.btn--glyph{background:transparent;color:…}`
   нижче у файлі глушив стан has-text (06.09.2026: кнопка відправки «зникла»). */
.btn.btn--send.has-text {
  background: var(--ds-accent);
  border-color: transparent;
  color: var(--ds-on-accent);
}
.btn.btn--send.has-text .send-icon--arrow { opacity: 1; transform: scale(1.1) rotate(0); color: var(--ds-on-accent); }
.btn.btn--send.has-text .send-icon--mic { opacity: 0; transform: scale(0.3) rotate(90deg); }

/* Attach / іконки: приглушені → акцент на hover (без важких заливок). */
.btn--attach {
  width: 2.75rem;
  height: 2.75rem;
  font-size: 1.375rem;
  color: var(--ds-text-muted);
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-full);
  transition: transform 0.3s var(--ease-spring), color 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.btn--attach:hover { color: var(--ds-accent); transform: rotate(45deg); }

.btn--send:hover { color: var(--ds-text-primary); }
/* color: on-accent (не accent) — 06.09.2026 Ф1b: з монохромним акцентом
   (білий на dark/чорний на light) `color:accent` на `background:accent-
   hover` малював майже той самий тон на тон — гліф зникав. З блакитним
   акцентом Ф1 різниця відтінків це маскувала, з --p-neutral-0/--p-zinc-950
   вже ні. */
.btn.btn--send.has-text:hover {
  color: var(--ds-on-accent);
  background: var(--ds-accent-hover);
  transform: scale(1.15);
}
.btn.btn--send.has-text:active {
  transform: scale(0.9);
  transition-duration: 0.08s;
}
/* Send pulse on submit */
.btn--send.sending {
  animation: sendPulse 0.4s var(--ease-spring);
}
@keyframes sendPulse {
  0% { transform: scale(1); }
  30% { transform: scale(0.75); }
  60% { transform: scale(1.2); }
  100% { transform: scale(1); }
}

.btn.btn--send.recording {
  color: var(--ds-feedback-danger);
  animation: recPulse 1s ease-in-out infinite;
}
.btn--send.recording .send-icon {
  transform: scale(1.2);
}
@keyframes recPulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(1.1); }
}

/* Base `.btn` — was missing entirely. Settings/modal buttons carry
   just `class="btn btn--small"`, so without this rule they rendered
   as padding-only naked text. Modifiers below specialise shape/colour;
   `.btn--icon` still overrides background+border to transparent via
   its own !important declarations. */
/* Ф2a: базовий вигляд (background/color/border/radius/font-weight) і
   :hover/:active нижче перенесені з @layer overrides (там перемагали
   ці ж легасі-значення незалежно від шару). Одна карта — тихий hairline,
   акцент лише на active/primary (Linear-стриманість). */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  font-family: var(--ds-font);
  font-size: var(--ds-body-size);
  font-weight: var(--ds-weight-medium);
  line-height: 1.2;
  cursor: pointer;
  /* Ф1 §4 «Правила»: без 300ms tap-delay на дотику. */
  touch-action: manipulation;
  transition:
    background 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease,
    transform 0.05s ease;
  text-decoration: none;
  white-space: nowrap;
}
.btn:hover {
  background: var(--ds-surface-hover);
  border-color: var(--ds-border-default);
  color: var(--ds-text-primary);
}
/* :active лишається в 95-utilities.css — там же вже свій .btn:active
   (шар utilities вищий за components; перенесене мусить перебивати
   там-таки, інакше програє пружному scale(.94)). */
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Активний сегмент (settings-тумблери theme/input-position/sound) —
   акцентний tint, не суцільна заливка. */
.btn.active,
.btn[aria-pressed="true"],
[role="tab"][aria-selected="true"] {
  background: var(--ds-accent-subtle);
  border-color: var(--ds-accent-border);
  color: var(--ds-accent);
  font-weight: 600;
}
/* color: on-accent — 06.09.2026 Ф1b: без цього `.btn.active` лишав
   color:accent (білий на dark) на background:accent-hover (теж майже
   білий) — той самий «тон-на-тон» бага, що й у send-кнопки вище. */
.btn.active:hover {
  background: var(--ds-accent-hover);
  border-color: var(--ds-accent-hover);
  color: var(--ds-on-accent);
}

/* Primary — single dominant action (e.g. "save passkey"). */
.btn--primary {
  background: var(--ds-accent);
  border-color: transparent;
  color: var(--ds-on-accent);
  font-weight: 600;
}
.btn--primary:hover {
  background: var(--ds-accent-hover);
  border-color: var(--ds-accent-hover);
  color: var(--ds-on-accent);
}

.btn--small {
  padding: var(--ds-space-1) var(--ds-space-2);
  font-size: var(--ds-meta-size);
  border-radius: var(--ds-radius-xs);
}

/* Glyph buttons — text characters (< > + ×) used as icon substitutes.
   Compound .btn.btn--glyph beats plain .btn on specificity without !important. */
.btn.btn--glyph {
  font-family: var(--font-mono, "SF Mono", "Fira Code", monospace);
  font-size: 1.5rem;
  font-weight: 300;
  line-height: 1;
  /* Ф2a: у проді overrides-.btn (ds-text-secondary) перебивав тутешній
     var(--ds-text-primary); зберігаємо фактичний вигляд. */
  color: var(--ds-text-secondary);
  background: transparent;
  border: none;
  box-shadow: none;
  transition: transform 0.15s var(--ease-spring), color 0.15s;
}
.btn.btn--glyph:hover,
.btn.btn--glyph:active,
.btn.btn--glyph:focus {
  background: transparent;
  box-shadow: none;
}
/* Standalone glyph buttons (Rust-generated close buttons without .btn parent) */
.btn--glyph {
  background: transparent;
  /* 1px прозора, не none: геометрія як у .btn (у проді її тримав overrides). */
  border: 0.0625rem solid transparent;
  box-shadow: none;
  cursor: pointer;
  touch-action: manipulation;
}

/* Ф1 §4 «Правила»: усі кнопки-іконки ≥44×44px на ≤48em. `.btn--icon` вже
   44px безумовно (10-layout.css); тут — «голі» `.btn--glyph` без
   `.btn--icon` (напр. "x" у media.rs::ai-reply-box, settings.rs::
   accounts-box), які на десктопі лишаються компактними за задумом. */
@media (max-width: 48em) {
  .btn--glyph {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}

/* Кнопки-іконки (назад, дзвінок, «×» у шапках) — прозорі, лише гліф.
   Мусять жити в ТОМУ Ж шарі components, що й .btn: .btn--icon у шарі
   layout програє .btn за шаром незалежно від специфічності (Ф2a-баг:
   «×» шторки отримав рамку). Стоїть ПІСЛЯ .btn.btn--glyph і має `.btn.btn--icon`,
   бо .btn.btn--glyph{color:var(--ds-text-primary)} специфічніший за голий .btn--icon. */
.btn--icon, .btn.btn--icon, .btn--back, .chat-header__calls .btn {
  background: transparent;
  border-color: transparent;
  color: var(--ds-text-secondary);
}
.btn--icon:hover, .btn.btn--icon:hover, .btn--back:hover, .chat-header__calls .btn:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
}


.btn--danger {
  background: var(--ds-feedback-danger);
  border-color: var(--ds-border-subtle);
  color: var(--ds-feedback-danger);
}
.btn--danger:hover {
  background: var(--ds-feedback-danger);
  border-color: transparent;
  color: var(--p-neutral-0);
}

/* ── Запис екрана (static/screen-rec.js) ──
   Кнопка з'являється лише там, де браузер пише екран, — тоді в сітці
   композера на одну колонку більше: attach | screen | voice | text | send.
   Прихована кнопка комірки не займає, тож без неї сітка лишається старою. */
.input-area:has(#screen-rec-btn:not([hidden])) {
  grid-template-columns: auto auto auto minmax(0, 1fr) auto;
}
.screen-rec__stop,
.screen-rec__timer { display: none; }
.screen-rec[data-state="recording"] {
  width: auto;
  gap: var(--ds-space-1);
  padding-inline: var(--ds-space-2);
  color: var(--ds-feedback-danger);
}
/* Під курсором .btn:hover перефарбовує в основний колір — а під курсором
   кнопка під час запису буває завжди: нею ж і зупиняють. */
.screen-rec[data-state="recording"]:hover { color: var(--ds-feedback-danger); }
.screen-rec[data-state="recording"] .screen-rec__icon { display: none; }
.screen-rec[data-state="recording"] .screen-rec__stop,
.screen-rec[data-state="recording"] .screen-rec__timer { display: inline-block; }
.screen-rec__timer {
  /* Не моно: у JetBrains Mono двокрапка займає повну клітинку, і
     таймер читався як «0 : 0 3». Рівну ширину цифр дає tabular-nums. */
  font-size: var(--ds-meta-size);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  min-width: 2.5em;
}

/* ── Composer attach chip + sending indicator ── */
.attach-chip {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-2);
  margin-bottom: var(--ds-space-1);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-raised);
}
.attach-chip[hidden] { display: none; }
.attach-chip__thumb {
  width: 2.25rem;
  height: 2.25rem;
  object-fit: cover;
  border-radius: var(--ds-radius-xs);
  flex-shrink: 0;
}
.attach-chip__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
}
.attach-chip__x {
  background: none;
  border: none;
  color: var(--ds-text-muted);
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  padding: var(--ds-space-1);
  flex-shrink: 0;
}
.attach-chip__x:hover { color: var(--ds-text-primary); }

/* Позиція/колір індикатора — звичайний токен-компонент, лишається у шарі. */
#send-indicator {
  position: absolute;
  top: -1.5rem;
  right: var(--ds-space-3);
  font-size: var(--ds-meta-size);
  color: var(--ds-accent);
  pointer-events: none;
}
}

/* ── ESCAPE HATCH з @layer components — НАВМИСНО поза шаром ──
   Перевірено Playwright + getComputedStyle (06.09.2026), звіт нижче.
   htmx (`includeIndicatorStyles: true`) інжектить СВІЙ `<style>` у
   <head> — НЕШАРОВАНИЙ (htmx не використовує @layer). За специфікацією
   каскаду нешаровані правила виграють у БУДЬ-якого шарованого правила
   НЕЗАЛЕЖНО ВІД СПЕЦИФІЧНОСТІ: `#send-indicator{opacity:0}` (1,0,0)
   усередині `@layer components` програвав htmx-івському нешарованому
   `.htmx-request .htmx-indicator{opacity:1}` (0,2,0) — це і є причина
   бага, не порядок файлів і не специфічність. `.htmx-request` на
   БУДЬ-якому предку (типово `<body>` — дефолтний індикатор htmx, коли
   запит не називає власний `hx-indicator`) підсвічував «Надсилання…»
   від чужих запитів (зміна виміру списку тощо).

   Проба (Playwright, getComputedStyle):
     document.body.classList.add('htmx-request') → opacity ДО фіксу: 1
     те саме                                      → opacity ПІСЛЯ:    0
     #send-indicator.classList.add('htmx-request') (справжній кейс
       власної відправки, той самий клас, що htmx ставить на елемент,
       named у hx-indicator="#send-indicator")     → opacity:          1

   Тому цей, і ЛИШЕ цей, override навмисно винесений з @layer
   components — це єдиний спосіб перебити нешаровану CSS без
   `!important` (заборонено лінтером і планом). */
#send-indicator {
  opacity: 0;
}
#send-indicator.htmx-request {
  opacity: 1;
}

/* ── Смуга вибору каналу у злитій розмові ────────────────────────
   Оскар у бесіді й Оскар у тг — одна стрічка, і композер мусить
   показувати, куди піде відповідь.

   Смуга — це ВЕРХ композера, а не окрема плита над ним. Перша спроба
   малювала її власною панеллю тієї ж поверхні з прямими кутами: під нею
   стояв композер зі скругленим верхом і тінню вгору, тінь малювала між
   ними лінію, а прямі кути читались як обрубок. Тепер скруглення, рамка
   й тінь належать смузі, а композер їх віддає, поки вона видима, —
   виходить один предмет, а не два.

   Чип — той самий організм, що `.dialog-tab` (playlist-bot
   `.genre-chip`): рамка + поверхня, активний = крок поверхні й
   border-strong. Підкреслення акцентом заборонене — це ШІ-патерн. */
.merge-meta {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding-block: var(--ds-space-2);
  /* Горизонталь — рівно як у композера, щоб мітка стала над «+». */
  padding-inline-start: max(var(--ds-space-3), env(safe-area-inset-left));
  padding-inline-end: max(var(--ds-space-3), env(safe-area-inset-right));
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 1rem 1rem 0 0;
  box-shadow:
    0 -0.25rem 0.75rem color-mix(in srgb, var(--p-neutral-1000) 12%, transparent),
    0 -0.75rem 2rem color-mix(in srgb, var(--p-neutral-1000) 6%, transparent);
  position: relative;
  z-index: 2;
  flex-shrink: 0;
}
.merge-meta[hidden] { display: none; }

/* Смуга видима — композер більше не окрема панель: без свого верху,
   без скруглення, без тіні. Сусідній селектор, бо `#merge-meta` стоїть
   у розмітці рівно перед `#message-form` (body_shell.html). */
.merge-meta:not([hidden]) ~ .input-area {
  border-top: none;
  border-radius: 0;
  box-shadow: none;
}

.merge-meta__label {
  font-size: var(--ds-meta-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ds-text-subtle);
  white-space: nowrap;
}

.merge-meta__chips { display: flex; gap: var(--ds-space-2); }

.merge-chip {
  /* Нижче за 44pt свідомо: чип стоїть у щільному рядку композера і не є
     дією першого ряду — головна ціль тут поле вводу. Ціль лишається
     натискною (36px + проміжок 8px). */
  min-height: 2.25rem;
  padding: 0 var(--ds-space-3);
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-base);
  color: var(--ds-text-muted);
  font-family: var(--ds-font);
  font-size: var(--ds-meta-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.merge-chip:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
  border-color: var(--ds-border-strong);
}
.merge-chip--active {
  background: var(--ds-surface-active);
  border-color: var(--ds-border-strong);
  color: var(--ds-text-primary);
}

/* Композер має ДВА режими геометрії (див. вище): широка колонка — панель
   на всю ширину зі скругленим верхом, вузька (`@container chat`) — плавуча
   картка з відступом і скругленням з усіх боків. Смуга вибору каналу
   мусить іти за обома, інакше в картковому режимі вона лишалась би
   прямокутною плитою над круглою карткою — рівно те, що впадало в око.

   Разом вони складають ОДНУ картку: верхні кути в смуги, нижні в
   композера, спільна рамка без шва посередині. */
@container chat (max-width: 48em) {
  .merge-meta:not([hidden]) {
    margin-inline: max(var(--ds-space-2), env(safe-area-inset-left));
    margin-inline-end: max(var(--ds-space-2), env(safe-area-inset-right));
    border: 0.0625rem solid var(--ds-border-subtle);
    border-bottom: none;
    border-radius: 1.25rem 1.25rem 0 0;
  }
  .merge-meta:not([hidden]) ~ .input-area {
    border-top: none;
    border-radius: 0 0 1.25rem 1.25rem;
    /* Тінь композера б'є ВГОРУ — під смугою вона малювала б шов
       усередині картки. Тінь тепер лише в смуги, вона верхня. */
    box-shadow: none;
  }
}
@media (max-width: 48em) {
  .merge-meta:not([hidden]) {
    margin-inline: max(var(--ds-space-2), env(safe-area-inset-left));
    margin-inline-end: max(var(--ds-space-2), env(safe-area-inset-right));
    border: 0.0625rem solid var(--ds-border-subtle);
    border-bottom: none;
    border-radius: 1.25rem 1.25rem 0 0;
  }
  .merge-meta:not([hidden]) ~ .input-area {
    border-top: none;
    border-radius: 0 0 1.25rem 1.25rem;
    /* Тінь композера б'є ВГОРУ — під смугою вона малювала б шов
       усередині картки. Тінь тепер лише в смуги, вона верхня. */
    box-shadow: none;
  }
}

/* Смуга черги P2P — той самий організм, що .merge-meta; мітка може
   бути довшою, тож на вузькому екрані переноситься, а чипи лишаються. */
.p2p-queue { flex-wrap: wrap; }
.p2p-queue__label { white-space: normal; text-transform: none; letter-spacing: 0; }

/* ── 41-composer-state.css ── */
/* Ф2.1c — композер (.input-area/#message-form) видимий лише коли діалог
   справді відкритий. `.dialog-open` ставлять openDialog() і htmx:afterSwap
   за маркером #open-dialog-meta (static/transport.js), знімає «назад»
   (static/ui.js). Без цього форма відправки висіла порожньою на першому
   екрані (жодного діалогу ще не обрано) — саме той стан, де індикатор
   #send-indicator раніше й «застрягав» видимим (06.09.2026, Ф2.0/Ф2.1). */
.layout:not(.dialog-open) .input-area,
.layout:not(.dialog-open) #message-form {
  display: none;
}

/* ── 45-motion.css ── */
/* 45-MOTION — анімовані стани іконок (Ф1b, Артем 06.09.2026 «анимированные
 * иконки?», docs/plans/2026-09-06-uipro-redesign.md §6): CSS/SVG-переходи
 * лише через --ds-motion-*/--ds-ease-*/--ease-spring токени, без
 * бібліотек, без декоративних анімацій — усе прив'язане до реального
 * стану (press/send/record/badge/hover), усе вимикається під
 * prefers-reduced-motion нижче.
 *
 * Порядок файлів = порядок зшивки /static/css/style.css за іменем; 45
 * стоїть після 41 (composer-state) і перед 50 (call) — суто позиційно,
 * конфліктів імен з жодним із них нема.
 */

@layer components {

/* ── 1. Кнопка відправки — стрілка після успішного сабміту ──
   .sending ставить transport.js:383-391 на .btn--send і знімає на
   animationend; та кнопка вже грає sendPulse на собі (40-composer.css:
   scale 1→0.75→1.2→1). Тут — додатково стрілка зсувається вгору-вправо
   й згасає, потім повертається на місце тим самим transform, яким її
   тримає стан `.has-text` (scale(1.1) rotate(0)) — інакше по кінцю
   анімації вона «стрибнула» б назад без переходу. Увесь цикл — 250ms. */
@keyframes send-arrow-away {
  0%   { opacity: 1; transform: scale(1.1) rotate(0) translate(0, 0); }
  45%  { opacity: 0; transform: scale(1.1) rotate(0) translate(0.35rem, -0.35rem); }
  55%  { opacity: 0; transform: scale(1.1) rotate(0) translate(-0.2rem, 0.2rem); }
  100% { opacity: 1; transform: scale(1.1) rotate(0) translate(0, 0); }
}
.btn--send.sending .send-icon--arrow {
  animation: send-arrow-away 250ms var(--ds-ease-out);
}

/* ── 2. Мікрофон під час запису — пульс-кільце ──
   playlist-bot `.header-logo.glow` :1680 — там статичний box-shadow;
   тут дихаюча версія того самого accent-subtle тону, 1.2s. Окремий
   ::after (не сама кнопка) — та вже грає recPulse (opacity+scale,
   40-composer.css); інша властивість, інший бокс, нуль конфлікту з
   існуючою анімацією. `.btn--send{position:relative}` вже є
   (40-composer.css), тому ::after коректно позиціюється навколо кнопки. */
/* Кільце-світіння прибрано 06.09.2026 (ШІ-патерн №6): запис показує
   суцільна accent-заливка кнопки + recPulse іконки з 40-composer.css. */
.btn.btn--send.recording {
  background: var(--ds-accent);
  color: var(--ds-on-accent);
}

/* ── 3. Індикатор набору тексту — вже існує, не дублюємо ──
   `.typing-dots` (95-utilities.css:640-648, три крапки з затримками
   0/0.2s/0.4s) і `.typing-indicator__dots` (30-chat-messages.css:
   770-787) — обидва вже роблять почерговий підйом крапок. ui.js:195-200
   створює `<span class="typing-indicator msg-appear">` з
   `.typing-dots` усередині на подію "typing" (transport.js:1102/1508).
   Ф1b нічого сюди не додає — задокументовано на прохання Артема, щоб
   ніхто не написав другу версію тієї самої фічі. */

/* ── 4. Пілюля непрочитаного — поява ──
   Кожен рендер рядка діалогу — свіжий DOM-вузол (htmx innerHTML на
   список), тож CSS-анімація на маунті програється сама, без JS-тригера.
   `.dialog-badge` (20-sidebar-dialogs.css:875) лишає свій
   `transition: transform` (hover-збільшення) — інша властивість, обидві
   діють одночасно. */
@keyframes badge-pop-in {
  from { opacity: 0; transform: scale(0.8); }
  to   { opacity: 1; transform: scale(1); }
}
.dialog-badge {
  animation: badge-pop-in var(--ds-motion) var(--ds-ease-out);
}

} /* @layer components */

/* ── 5. Кнопки-іконки — hover/press ──
   playlist-bot `.track-act` :1923-1938: приглушені → акцент-тінт на
   hover (не суцільний surface-hover), пружний press. МУСИТЬ жити в
   `@layer utilities`, не `components`: `.btn:active { transform:
   translateY(...) }` живе в 95-utilities.css, а шар сильніший за
   специфічність незалежно від порядку файлів (той самий урок, що вже
   задокументований у 95-utilities.css над .btn:active) — компонентне
   правило тут програло б, скільки б класів у селекторі не додавай. */
@layer utilities {

.btn--icon:hover, .btn.btn--icon:hover, .btn--back:hover, .chat-header__calls .btn:hover {
  background: var(--ds-accent-subtle);
  color: var(--ds-text-primary);
}
.btn--icon:active, .btn.btn--icon:active, .btn--back:active, .chat-header__calls .btn:active {
  transform: scale(0.9);
  transition: transform var(--ds-motion-fast) var(--ease-spring);
}

} /* @layer utilities */

/* Вимикач — неспозиційований (без @layer), тому переб'є будь-яке з двох
   правил вище незалежно від шару (той самий прийом, що вже в
   20-sidebar-dialogs.css над `.dialog-item{animation:none}`). */
@media (prefers-reduced-motion: reduce) {
  .btn--send.sending .send-icon--arrow,
  .btn.btn--send.recording::after,
  .dialog-badge {
    animation: none;
  }
  .btn--icon:active, .btn.btn--icon:active, .btn--back:active, .chat-header__calls .btn:active {
    transition: none;
  }
}

/* ── 50-call.css ── */
/* ДЗВІНКИ — повноекранна сцена і оверлей аудіо/відео
 *
 * Поверхня вмикається рідко, але важить багато рядків; окремий файл
 * дає змогу правити її, не тримаючи в голові решту чату.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   CALL UI - Fullscreen WebGL Scene
   ═══════════════════════════════════════════ */

/* Incoming call — fullscreen, safe area aware */
/* Dial pad */
.dialpad-overlay {
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: color-mix(in srgb, black 85%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
}
.dialpad {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-4);
  padding: var(--ds-space-5);
  max-width: 17.5rem;
  width: 100%;
}
.dialpad__input {
  width: 100%;
  font-size: 1.5rem;
  text-align: center;
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--fg-2);
  color: var(--ds-text-primary);
  padding: var(--ds-space-3) 0;
  font-family: var(--font-mono);
  letter-spacing: 0.125rem;
}
.dialpad__input:focus { outline: none; border-color: var(--ds-text-primary); }
.dialpad__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--ds-space-2);
  width: 100%;
}
.dialpad__key {
  aspect-ratio: 1;
  font-size: 1.25rem;
  background: var(--bg-2);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-primary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.dialpad__key:active { background: var(--fg-2); }
.dialpad__actions {
  display: flex;
  gap: var(--ds-space-4);
  align-items: center;
  width: 100%;
  justify-content: space-between;
}
.dialpad__actions .call-btn--accept { width: 3rem; height: 3rem; }

/* ── Шторка дзвінка ────────────────────────────────────────
   Один шар на два стани замість двох повноекранних оверлеїв. Раніше при
   «прийняти» перший їхав униз, другий угору, і між ними було видно
   застосунок. Тепер шторка виїжджає рівно один раз, а стан міняється
   всередині неї. Жест — спільний з фотошторкою (`static/sheet.js`). */
.call-sheet {
  position: fixed;
  inset: 0;
  z-index: 10000;
  pointer-events: none;
}
.call-sheet[data-open] { pointer-events: auto; }

.call-sheet__panel {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  /* «До верху»: шторка на весь екран, але лишається шторкою — приходить
     знизу, тягнеться вниз, має ручку. */
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--ds-surface-sunken);
  border-radius: 1rem 1rem 0 0;
  overflow: hidden;
  transform: translateY(100%);
  transition: transform 0.3s var(--ease-out);
  will-change: transform;
  padding-bottom: env(safe-area-inset-bottom);
}
.call-sheet[data-open] .call-sheet__panel { transform: translateY(0); }
/* Поки палець веде панель, перехід вимкнено: інакше кожен кадр жесту
   ставав би анімацією і панель відставала б від пальця. */
.call-sheet__panel[data-drag] { transition: none; }

.call-sheet__grip {
  flex-shrink: 0;
  height: 1.5rem;
  padding-top: env(safe-area-inset-top);
  display: grid;
  place-items: center;
  cursor: grab;
  touch-action: none;
}
.call-sheet__grip::before {
  content: "";
  width: 2.25rem;
  height: 0.25rem;
  border-radius: var(--ds-radius-full);
  background: var(--ds-border-strong);
}
.call-sheet__grip:active { cursor: grabbing; }
/* Поки дзвонять, тягнути нема куди: вибір — відповісти або відхилити.
   Ручка лишається на місці (щоб панель не стрибала), але не ловить
   палець. */
.call-sheet[data-state="ring"] .call-sheet__grip {
  opacity: 0;
  pointer-events: none;
}

/* Два стани однієї шторки. Показаний рівно один. */
.call-sheet__ring,
.call-sheet__live { display: none; flex: 1 1 auto; min-height: 0; position: relative; }
.call-sheet[data-state="ring"] .call-sheet__ring { display: flex; align-items: center; justify-content: center; }
.call-sheet[data-state="live"] .call-sheet__live { display: flex; }

/* Згорнутий дзвінок: смужка на всю ширину зверху, розмова триває. Не
   плашка по центру: та накривала б поле пошуку, а смужка ОПУСКАЄ
   застосунок на свою висоту (`.layout[data-call-min]` нижче) — як
   робить телефон, коли дзвінок згорнутий. */
.call-island {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10001;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-3);
  height: calc(2.25rem + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
  background: var(--ds-surface-raised);
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  color: var(--ds-text-primary);
  font-size: var(--ds-label-size);
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.call-island:active { background: var(--ds-surface-hover); }
/* `hidden` проти класу з display: атрибут програє класу, тому ховаємо явно.
   Без цього згорнута смужка світилась би завжди. */
.call-island[hidden] { display: none; }

/* Застосунок під смужкою, а не за нею.
   Відступом, а не «висота мінус смужка плюс поле зверху»: два вирази з
   `100dvh` і `env(safe-area-inset-top)` браузер перераховує щоразу, коли
   на телефоні їде адресний рядок, і застосунок під смужкою сіпався.
   `box-sizing: border-box` стоїть глобально, тож padding з'їдає висоту
   сам, без другої арифметики (10.09.2026). */
.layout[data-call-min] {
  padding-top: calc(2.25rem + env(safe-area-inset-top));
}

.call-island__timer {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  letter-spacing: 0.1em;
}

/* Стан «дзвонять» усередині шторки. Клас лишався від часів окремого
   повноекранного оверлея; тепер це просто один із двох станів. */
.call-sheet__ring {
  padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.call-sheet__ring .call-ui {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-5);
  padding: var(--ds-space-6);
  text-align: center;
  width: 100%;
  max-width: 22.5rem;
}

.call-avatar {
  width: 7.5rem;
  height: 7.5rem;
  border-radius: 50%;
  background: color-mix(in srgb, white 8%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  color: color-mix(in srgb, white 70%, transparent);
  font-size: 2.5rem;
  font-weight: 300;
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.125rem;
  animation: avatar-pulse 2s ease-in-out infinite;
}

@keyframes avatar-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.03); }
}

.call-info {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

/* Підпис стану виклику. Блимання (`status-blink`, opacity 0.4↔1 кожні
   півтори секунди) прибрано: воно нічого не повідомляло, зате читалось як
   несправність екрана — а сам виклик і так рухається пульсом аватара. */
.call-status {
  font-size: 0.75rem;
  color: color-mix(in srgb, white 40%, transparent);
  text-transform: uppercase;
  letter-spacing: 0.1875rem;
  font-family: var(--font-mono, monospace);
}

.call-name {
  font-size: 2rem;
  font-weight: 300;
  color: white;
  letter-spacing: -0.0.3125rem;
}

.call-buttons {
  display: flex;
  gap: 5rem;
  margin-top: var(--ds-space-6);
  align-items: center;
}

.call-btn-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-2);
}

.call-btn__label {
  font-size: 0.75rem;
  color: color-mix(in srgb, white 50%, transparent);
  letter-spacing: 0.03125rem;
}

.call-btn {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s ease;
  position: relative;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.call-btn svg {
  width: 1.75rem;
  height: 1.75rem;
}

/* Call-control colours are safety-critical — iOS/Apple HIG keeps accept
   solid green and reject solid red even in otherwise monochrome UIs so
   users never misidentify them under pressure. Ф2b: rebased onto the
   brand success/danger tokens (was iOS system #34c759/#ff3b30 literals)
   so the rule still holds while satisfying color-literal 0 — same green/
   red family, see Ф2b report for the exact hex delta. */
.call-btn--accept {
  background: var(--ds-feedback-success);
  color: white;
}

.call-btn--accept::before {
  content: "";
  position: absolute;
  inset: -0.5rem;
  border-radius: 50%;
  border: 0.125rem solid var(--ds-feedback-success);
  animation: ring-expand 1.5s ease-out infinite;
}

.call-btn--accept::after {
  content: "";
  position: absolute;
  inset: -1rem;
  border-radius: 50%;
  border: 0.0625rem solid var(--ds-feedback-success);
  animation: ring-expand 1.5s ease-out infinite 0.3s;
  opacity: 0.5;
}

@keyframes ring-expand {
  0% { transform: scale(0.8); opacity: 1; }
  100% { transform: scale(1.4); opacity: 0; }
}

.call-btn--reject {
  background: var(--ds-feedback-danger);
  color: white;
}

.call-btn:active {
  transform: scale(0.9);
}

/* Legacy support */
.incoming-call { display: none; }
.call-actions { display: none; }

/* Call button in header */
.btn--call {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  border: none;
  border-radius: var(--ds-radius-sm);
  width: 2.25rem;
  height: 2.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out);
  margin-inline-start: auto;
  -webkit-tap-highlight-color: transparent;
}

.btn--call:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-feedback-success);
}

.btn--call:active {
  transform: scale(0.95);
}

.btn--call svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: currentColor;
}

/* ═══════════════════════════════════════════
   CALL OVERLAY (video + audio) — PWA safe
   ═══════════════════════════════════════════ */

.call-overlay {
  background: black;
  align-items: stretch;
  justify-content: stretch;
  padding: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
}

/* Call controls muted state */
.call-ctrl--off {
  background: color-mix(in srgb, var(--ds-feedback-danger) 30%, transparent);
  border-color: var(--ds-feedback-danger);
}
.call-ctrl--off svg { fill: var(--ds-feedback-danger); }

/* Audio-only call: HIDE video visually but keep it in the layout.
   `display: none` stops HTMLMediaElement playback on Chrome/Safari —
   audio tracks inside a hidden <video> go silent even though the
   underlying MediaStream still has them. Collapse to a zero-sized
   invisible element instead so the track keeps playing.
   Своє відео сюди НЕ входить: показуючи екран співрозмовнику без камери,
   треба бачити, що саме ти показуєш. Ним керує `data-local`. */
.call-overlay--audio #remote-video {
  width: 0;
  height: 0;
  opacity: 0;
  position: absolute;
  pointer-events: none;
}
.call-overlay[data-local="off"] #local-video { display: none; }
.call-overlay--audio {
  align-items: center;
  justify-content: center;
  flex-direction: column;
  background: black;
}

.call-overlay--audio .call-overlay__bar {
  position: static;
  margin-top: var(--ds-space-6);
  background: none;
}
.call-overlay--audio .call-overlay__peer { display: none; }

/* Созвон — теж «без картинки», але панель мусить лишитись унизу: правило
   вище розпускає її в потік (для розмови вдвох, де центр тримає висоту), і
   в созвоні вона в'їжджала просто під плитки. Іде ПІСЛЯ нього — при рівній
   вазі перемагає останнє. */
.call-overlay--group .call-overlay__bar {
  position: absolute;
  bottom: 0;
  margin-top: 0;
  background: color-mix(in srgb, black 70%, transparent);
}
.call-overlay--group .call-overlay__peer { display: block; }

/* Remote video: fill entire screen */
#remote-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: color-mix(in srgb, white 7%, black);
}

/* Local video: PiP in corner */
#local-video {
  position: absolute;
  bottom: 6.25rem;
  right: var(--ds-space-4);
  width: 6.25rem;
  aspect-ratio: 3/4;
  object-fit: cover;
  background: black;
  border: none;
  z-index: 2;
  border-radius: var(--ds-radius-xs);
}

/* Камера того, хто показує екран: екран займає сцену, обличчя стоїть
   поряд зі своїм віконцем. Поки показу немає, елемент порожній і його
   ховає `data-peer-camera` (rtc.js кладе туди камеру лише разом з
   екраном). */
#remote-camera {
  position: absolute;
  bottom: 6.25rem;
  left: var(--ds-space-4);
  width: 6.25rem;
  aspect-ratio: 3/4;
  object-fit: cover;
  background: black;
  z-index: 2;
  border-radius: var(--ds-radius-xs);
}
.call-overlay[data-peer-camera="off"] #remote-camera { display: none; }

/* Чужий екран не ріжемо: обличчя можна обрізати під сцену, а таблицю чи
   код — ні. На вертикальному телефоні від 16:9 інакше лишалась середня
   смуга (22.09.2026). */
#remote-video[data-kind="screen"] { object-fit: contain; }
/* У созвоні сцена закінчується над смугою плиток: інакше нижні рядки
   показаного документа ховались під плитками й кнопками (знімок
   23.09.2026). Смуга 6.25rem + панель 9rem. */
.call-overlay--group.call-overlay--stage #remote-video {
  /* Згори місце під підпис «хто показує», знизу смуга 6.25rem + панель
     9rem. Висоту треба назвати: у <video> з height:auto нижній відступ
     не діє, і картинка прилипала до верху. */
  inset: 3.25rem 0 15.25rem 0;
  height: calc(100% - 18.5rem);
  background: transparent;
}

/* Свій показ: маленьке вікно над своєю камерою, щоб бачити, що саме
   зараз дивляться інші. Пропорція екрана, а не обличчя. */
#local-screen {
  position: absolute;
  bottom: 11.5rem;
  right: var(--ds-space-4);
  width: 8rem;
  aspect-ratio: 16/10;
  object-fit: contain;
  background: black;
  z-index: 2;
  border-radius: var(--ds-radius-xs);
}
.call-overlay[data-local-screen="off"] #local-screen,
.call-overlay:not([data-local-screen]) #local-screen { display: none; }
/* Камери немає — своє віконце вільне, показ стає на його місце. */
.call-overlay[data-local="off"][data-local-screen="on"] #local-screen { bottom: 6.25rem; }

/* Дотик по відео перемикає дзеркало (rtc.js, data-mirror). */
#remote-video, #remote-camera, #local-video { cursor: pointer; }
#remote-video[data-mirror], #remote-camera[data-mirror],
#local-video[data-mirror] { transform: scaleX(-1); }

/* Созвон: плитка на кожного учасника. Сітка сама вирішує, скільки в
   рядку — на двох це два великих прямокутники, на шести шість менших, без
   окремих правил під кожну кількість. */
.call-tiles {
  position: absolute;
  inset: 0;
  /* Місце під нижню панель: інакше плитки лізуть під кнопки. */
  bottom: 9rem;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  align-content: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-3);
  overflow: auto;
}
.call-tiles[hidden] { display: none; }

/* У созвоні без показу відео-елемент не потрібен зовсім: порожній
   <video> на iOS малює системну кнопку «play» поверх плиток. */
.call-overlay--group:not(.call-overlay--stage) #remote-video { display: none; }


/* Браузер відмовився запускати відео сам (режим енергозбереження на
   iOS) — кажемо про це словами замість системної кнопки «play»
   посеред екрана; перший дотик по шторці запускає картинку. */
.call-sheet[data-needs-tap] .call-overlay::after {
  content: attr(data-tap-hint);
  position: absolute;
  left: 50%;
  bottom: 11rem;
  transform: translateX(-50%);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: color-mix(in srgb, black 70%, transparent);
  color: var(--ds-text-primary);
  font-size: var(--ds-meta-size);
  border-radius: var(--ds-radius-sm, 0.5rem);
  pointer-events: none;
  z-index: 3;
}


/* Сцена показу в созвоні: картинка одного учасника на весь екран, решта
   плиток — смугою знизу над панеллю. Без цієї смуги плитки (inset: 0,
   z-index: 1) накривали `#remote-video` цілком, і показаний екран бачив
   лише той, хто показує. */
.call-overlay--group.call-overlay--stage #call-tiles {
  inset: auto 0 9rem 0;
  height: 6.25rem;
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: 7rem;
  align-content: end;
  /* Коли плитки влазять — по центру під сценою; ні — гортаються від
     лівого краю (safe не дає обрізати першу). */
  justify-content: safe center;
  overflow-x: auto;
  overflow-y: hidden;
}
.call-overlay--group.call-overlay--stage .call-tile {
  min-height: 0;
  /* Смуга 5.5rem: аватар, імʼя і підпис «екран/камера» мусять влізти
     без стискання імені в нуль (знімок 23.09.2026). */
  gap: 0.5rem;
  padding: var(--ds-space-2) var(--ds-space-2) 0.375rem;
  background: color-mix(in srgb, black 55%, transparent);
}
.call-overlay--group.call-overlay--stage .call-tile__avatar {
  width: 2.25rem;
  height: 2.25rem;
  font-size: 0.75rem;
}


.call-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  min-height: 8rem;
  padding: var(--ds-space-3);
  border-radius: var(--ds-radius-md, 0.75rem);
  background: color-mix(in srgb, white 5%, transparent);
}

.call-tile__avatar {
  position: relative;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, white 8%, transparent);
  color: color-mix(in srgb, white 65%, transparent);
  font-family: var(--font-mono, monospace);
  font-size: 1.125rem;
  letter-spacing: 0.0625rem;
}
/* Те саме кільце голосу, що й у розмові вдвох: видно, хто зараз говорить. */
.call-tile__avatar::after {
  content: "";
  position: absolute;
  inset: -0.375rem;
  border-radius: 50%;
  border: 0.0625rem solid color-mix(in srgb, white 22%, transparent);
  transform: scale(calc(1 + var(--call-level, 0) * 0.16));
  transition: transform 90ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .call-tile__avatar::after { transform: none; transition: none; }
}

.call-tile__name {
  font-size: var(--ds-meta-size);
  color: color-mix(in srgb, white 70%, transparent);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* У кого є картинка, того плитку можна натиснути: він стане на сцену.
   Чия картинка на сцені, та плитка обведена; під імʼям — що саме він
   показує. */
.call-tile {
  transition: box-shadow var(--ds-motion) var(--ds-ease-out), background-color var(--ds-motion) var(--ds-ease-out), transform var(--ds-motion-fast) var(--ds-ease-out);
}
.call-tile[data-video] { cursor: pointer; }
.call-tile[data-video]:hover { background: color-mix(in srgb, white 9%, transparent); }
.call-tile[data-video]:active { transform: scale(0.97); }
.call-tile[data-video]:focus-visible {
  outline: 0.125rem solid color-mix(in srgb, white 70%, transparent);
  outline-offset: 0.125rem;
}
.call-tile[data-video]::after {
  content: attr(data-video-label);
  margin-top: -0.375rem;
  font-size: var(--ds-eyebrow-size);
  color: color-mix(in srgb, white 45%, transparent);
  line-height: 1;
}
.call-tile--stage {
  box-shadow: inset 0 0 0 0.125rem var(--ds-accent);
}
.call-tile--stage::after { color: var(--ds-accent); }
/* Новий учасник проявляється на своєму місці. */
.call-tile--enter { animation: call-tile-in 280ms cubic-bezier(0.2, 0.8, 0.2, 1) both; }
@keyframes call-tile-in {
  from { opacity: 0; transform: translateY(0.5rem) scale(0.96); }
}
@media (prefers-reduced-motion: reduce) {
  .call-tile { transition: none; }
  .call-tile--enter { animation: none; }
  .call-tile[data-video]:active { transform: none; }
}

/* Чия картинка на сцені: підпис угорі зліва, над відео. */
.call-stage-label {
  position: absolute;
  top: calc(var(--ds-space-3) + env(safe-area-inset-top));
  left: var(--ds-space-3);
  z-index: 3;
  max-width: calc(100% - 2 * var(--ds-space-3));
  padding: 0.375rem 0.75rem;
  border-radius: var(--ds-radius-full);
  background: color-mix(in srgb, black 60%, transparent);
  backdrop-filter: blur(0.5rem);
  -webkit-backdrop-filter: blur(0.5rem);
  color: color-mix(in srgb, white 88%, transparent);
  font-size: var(--ds-label-size);
  font-weight: var(--ds-label-weight);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}
.call-stage-label[hidden],
.call-overlay:not(.call-overlay--stage) .call-stage-label { display: none; }
.call-stage-label[data-pinned="1"]::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 0.375rem; height: 0.375rem; margin-right: 0.5rem; border-radius: 50%;
  background: var(--ds-accent);
}

.call-tile__state {
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: color-mix(in srgb, white 35%, transparent);
  font-family: var(--font-mono, monospace);
}

/* Лічильник учасників на кнопці созвону в шапці кімнати. */
.room-call__count {
  margin-inline-start: 0.25rem;
  font-size: 0.6875rem;
  font-family: var(--font-mono, monospace);
  color: var(--ds-text-secondary);
}
.room-call__count[hidden] { display: none; }

/* Center content (avatar + name + timer) for audio calls */
.call-overlay__center {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-4);
  z-index: 1;
  flex: 1;
}

.call-overlay__avatar {
  position: relative;
  width: 6.25rem;
  height: 6.25rem;
  border-radius: 50%;
  background: color-mix(in srgb, white 6%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  color: color-mix(in srgb, white 60%, transparent);
  font-size: 2.25rem;
  font-weight: 300;
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.125rem;
}

/* Кільце навколо аватара ходить за ГОЛОСОМ співрозмовника (`--call-level`
   ставить CallLevel у rtc.js). Потрібне саме тоді, коли камери немає: без
   нього екран весь дзвінок стоїть нерухомо і не видно, чи щось іде. Не
   світіння і не пульсація «для краси» — товщина стала, рухається тільки
   розмір, і рівно на стільки, скільки прийшло звуку. */
.call-overlay__avatar::after {
  content: "";
  position: absolute;
  inset: -0.625rem;
  border-radius: 50%;
  border: 0.0625rem solid color-mix(in srgb, white 22%, transparent);
  transform: scale(calc(1 + var(--call-level, 0) * 0.13));
  transition: transform 90ms linear;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .call-overlay__avatar::after { transform: none; transition: none; }
}

.call-overlay__name {
  font-size: 1.5rem;
  font-weight: 300;
  color: white;
  letter-spacing: -0.04em;
}

.call-overlay__timer {
  font-size: 0.875rem;
  color: color-mix(in srgb, white 40%, transparent);
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.15em;
}

/* Live subtitles */
.call-subtitles {
  position: absolute;
  bottom: 6.25rem;
  left: var(--ds-space-4);
  right: var(--ds-space-4);
  z-index: 4;
  text-align: center;
  pointer-events: none;
}
.call-subtitles__line {
  display: inline-block;
  background: color-mix(in srgb, black 70%, transparent);
  color: white;
  font-size: var(--ds-body-size);
  letter-spacing: -0.02em;
  padding: var(--ds-space-1) var(--ds-space-3);
  border-radius: var(--ds-radius-xs);
  max-width: 100%;
  animation: fadeInUp 0.2s var(--ease-out);
}

/* Bottom controls bar — safe area for home indicator */
.call-overlay__bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-3);
  padding: var(--ds-space-5) var(--ds-space-5) calc(var(--ds-space-5) + env(safe-area-inset-bottom));
  background: color-mix(in srgb, black 70%, transparent);
  z-index: 3;
}

.call-overlay__peer {
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: color-mix(in srgb, white 40%, transparent);
  font-family: var(--font-mono, monospace);
}

.call-overlay__controls {
  display: flex;
  gap: var(--ds-space-4);
  align-items: center;
}

.call-ctrl {
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  border: 0.0625rem solid color-mix(in srgb, white 15%, transparent);
  background: color-mix(in srgb, white 8%, transparent);
  color: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, transform 0.1s;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.call-ctrl:active {
  transform: scale(0.9);
}

/* Увімкнений стан (показ екрана): та сама кнопка, лише щільніша заливка.
   Без кольору-сигналу — червоне тут означає «вимкнено/відбій». */
.call-ctrl--active {
  background: color-mix(in srgb, white 24%, transparent);
  border-color: color-mix(in srgb, white 35%, transparent);
}

.call-ctrl.muted {
  background: var(--ds-feedback-danger);
  border-color: var(--ds-feedback-danger);
}

.call-ctrl--end {
  background: var(--ds-feedback-danger);
  border-color: var(--ds-feedback-danger);
  width: 4rem;
  height: 3.25rem;
  border-radius: 1.625rem;
}

.call-ctrl--end:active {
  background: color-mix(in srgb, var(--ds-feedback-danger) 80%, black);
}
}

@layer components {
/* Панель кімнати переїхала в 72-rooms.css (19.09.2026). Тут лишилась
   лише справа: під палець керма по 48 px (Frain, «Responsive Web
   Design»: рахувати, що вводять пальцем). */
@media (pointer: coarse) {
  .case-view .btn,
  .case-view .case-todo__input {
    min-height: 3rem;
  }
}
}

/* ── Запис зустрічі (23.09.2026) ─────────────────────────────
   Кнопка: коло з крапкою; під час запису крапка червона й повільно
   дихає. Позначка «Запис» — у всіх учасників угорі по центру: писати
   людей потай не можна. */
.call-ctrl--rec .call-ctrl__rec-dot { fill: color-mix(in srgb, white 55%, transparent); }
.call-ctrl--rec.call-ctrl--active .call-ctrl__rec-dot {
  fill: var(--ds-feedback-danger);
  animation: call-rec-pulse 1.6s ease-in-out infinite;
}
@keyframes call-rec-pulse { 50% { opacity: 0.35; } }
.call-ctrl--rec[hidden] { display: none; }

.call-rec-badge {
  position: absolute;
  top: calc(var(--ds-space-3) + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  max-width: calc(100% - 12rem);
  padding: 0.375rem 0.75rem;
  border-radius: var(--ds-radius-full);
  background: color-mix(in srgb, var(--ds-feedback-danger) 22%, black);
  color: color-mix(in srgb, white 92%, transparent);
  font-size: var(--ds-label-size);
  font-weight: var(--ds-label-weight);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}
.call-rec-badge::before {
  content: ""; flex-shrink: 0;
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--ds-feedback-danger);
  animation: call-rec-pulse 1.6s ease-in-out infinite;
}
.call-rec-badge[hidden] { display: none; }
/* Підпис «хто на сцені» стоїть зліва вгорі; на вузькому екрані позначка
   запису йде під нього, щоб не налазили. */
@media (max-width: 30em) {
  .call-rec-badge { top: calc(var(--ds-space-3) + 2.5rem + env(safe-area-inset-top)); max-width: calc(100% - 2rem); }
}
@media (prefers-reduced-motion: reduce) {
  .call-ctrl--rec.call-ctrl--active .call-ctrl__rec-dot, .call-rec-badge::before { animation: none; }
}

/* ── 55-login-overlays.css ── */
/* ОВЕРЛЕЇ, МОДАЛКА, ВХІД
 *
 * Форму входу малює Rust (`handlers::components::login`); тут лишився
 * лише повноекранний бекдроп і модальне вікно.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   OVERLAY / MODAL
   ═══════════════════════════════════════════ */

.overlay {
  position: fixed;
  inset: 0;
  background: var(--ds-surface-base);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

/* ═══════════════════════════════════════════
   LOGIN
   ═══════════════════════════════════════════ */

/* Login is rendered by ui-stack now (see beseda::handlers::components::login).
   The `.login` overlay class still drives the full-viewport backdrop
   inside index.html; everything else (.login__box / __input / __btn /
   __error / __hint / __logo / __passkey / __grid / __scan) was retired
   when the form moved off Tera. */
.login {
  flex-direction: column;
  overflow: hidden;
}

.overlay--settings {
  background: color-mix(in srgb, var(--p-neutral-1000) 60%, transparent);
}

.modal {
  width: 90%;
  max-width: 22.5rem;
  /* легасі --space-8 = 2rem literal, точно == --ds-space-6 (p-space-800),
     хоч індекси і не збігаються — значення тотожне. */
  padding: var(--ds-space-6);
  border: 0.0625rem solid var(--ds-text-primary);
  background: var(--ds-surface-base);
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
}

.modal--settings {
  max-width: 25rem;
  width: 100%;
  background: var(--ds-surface-raised);
  border: none;
  border-radius: var(--ds-radius-md);
  /* легасі --space-6 = 1.5rem literal, точно == --ds-space-5 (p-space-600),
     індекси не збігаються, значення тотожне. */
  padding: var(--ds-space-5);
}

/* ── Apple-стиль вхід: PIN-пад + Face ID ── Ф2a, з @layer overrides
   (CSS для форми входу цілком жив там від самого початку — тут раніше
   був лише беkдроп/модалка, як пояснює коментар на початку файлу). */
#login-pad {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
  padding-top: 0.5rem;
}
#login-pad[hidden] { display: none; }
.login-pad__who {
  font-size: var(--ds-label-size);
  font-weight: 600;
  color: var(--ds-text-primary);
  letter-spacing: 0.01em;
}
.login-pad__face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  padding: 0.75rem 1.25rem;
  background: transparent;
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-secondary);
  font-size: var(--ds-eyebrow-size);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.login-pad__face:hover { border-color: var(--ds-accent); color: var(--ds-accent); }
.login-pad__hint {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  min-height: 1.2em;
}
.login-pad__dots {
  display: flex;
  gap: 0.875rem;
  min-height: 0.8rem;
}
.pad-dot {
  width: 0.8rem;
  height: 0.8rem;
  border-radius: 50%;
  border: 0.09375rem solid var(--ds-text-muted);
  transition: background 0.12s, border-color 0.12s;
}
.pad-dot--on { background: var(--ds-text-primary); border-color: var(--ds-text-primary); }
.pad-dots--shake { animation: pad-shake 0.4s; }
@keyframes pad-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-0.5rem); }
  40% { transform: translateX(0.5rem); }
  60% { transform: translateX(-0.3125rem); }
  80% { transform: translateX(0.3125rem); }
}
.login-pad__grid {
  display: grid;
  grid-template-columns: repeat(3, 4.4rem);
  gap: 0.8rem;
  margin-top: 0.25rem;
}
.pad-key {
  width: 4.4rem;
  height: 4.4rem;
  border-radius: 50%;
  border: none;
  background: color-mix(in srgb, var(--p-neutral-0) 7%, transparent);
  color: var(--ds-text-primary);
  font-size: 1.5rem;
  font-family: var(--ds-font);
  cursor: pointer;
  transition: background 0.1s, transform 0.06s;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
.pad-key:active { background: color-mix(in srgb, var(--p-neutral-0) 18%, transparent); transform: scale(0.96); }
.login-pad__other {
  margin-top: 0.25rem;
  background: none;
  border: none;
  color: var(--ds-text-subtle);
  font-size: var(--ds-eyebrow-size);
  cursor: pointer;
}
.login-pad__other:hover { color: var(--ds-text-primary); }
.passkey-offer {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 350;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.625rem 1rem;
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  box-shadow: var(--ds-shadow-3);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-primary);
  animation: dim-menu-in 160ms var(--ease-out) both;
}
.passkey-offer button {
  padding: 0.375rem 0.75rem;
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-xs);
  background: transparent;
  color: var(--ds-text-secondary);
  cursor: pointer;
  font-size: var(--ds-eyebrow-size);
}
.passkey-offer button[data-a="yes"] {
  background: var(--ds-accent);
  border-color: var(--ds-accent);
  /* Текст ПОВЕРХ акцентної заливки — точна роль --ds-on-accent (темна тема
     #06140d проти колишнього #10251a, обидва майже чорні; світла тема
     коректно стає білою на глибоко-зеленій кнопці). */
  color: var(--ds-on-accent);
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .pad-dots--shake { animation: none; }
}
}

/* ── 60-settings.css ── */
/* НАЛАШТУВАННЯ — шухляда, групи, рядки
 *
 * Розмітку тримає `handlers::components::settings_view`; правила тут
 * мусять іти за тими самими класами, інакше шухляда «пливе».
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   SETTINGS DRAWER (slide-over)
   ═══════════════════════════════════════════ */

.settings-backdrop {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: color-mix(in srgb, var(--p-neutral-1000) 40%, transparent);
}

/* Bottom-sheet drawer pattern — shared by settings, accounts, and any
   future side panel. Slides up from the bottom uniformly. */
.settings-drawer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 80vh;
  z-index: 1000;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-strong);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  animation: drawerSlideUp 0.18s var(--ease-out);
  box-shadow: var(--shadow-lg);
}

@keyframes drawerSlideUp {
  from { transform: translateY(100%); }
  to { transform: none; }
}

.settings-drawer__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--ds-space-4);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  flex-shrink: 0;
}
/* Деталь-заголовок (drill-in): space-between розкидав стрілку-назад
   далеко ліворуч і назву далеко праворуч — не пов'язані. Групуємо
   ліворуч. Ф2a, з @layer overrides. */
.settings-drawer__header:has(.settings-drawer__back) {
  justify-content: flex-start;
  gap: var(--ds-space-3);
}

.settings-drawer__back {
  width: 1.375rem;
  height: 1.375rem;
  margin-right: var(--ds-space-2);
  flex-shrink: 0;
  cursor: pointer;
  color: var(--ds-text-secondary);
  transition: color var(--ds-motion-fast) var(--ds-ease-out);
}
.settings-drawer__back:hover { color: var(--ds-accent); }

.settings-drawer__title {
  font-size: var(--t2);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ds-text-secondary);
  margin-inline-end: auto;
}
.settings-drawer__title--sub {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  color: var(--ds-text-primary);
}
.settings-drawer__title--sub:hover { color: var(--ds-accent); }

/* iOS push: діти #settings-body свіжовставлені на кожен htmx-swap, тож
   слайд програється на drill-in / назад. */
@keyframes ds-settings-slide {
  from { transform: translateX(0.875rem); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
#settings-body > * {
  animation: ds-settings-slide 0.22s var(--ease-out);
}

/* iOS-style category rows (top-level list) */
.settings-row--category {
  cursor: pointer;
  transition: background-color 0.15s;
}
.settings-row--category:hover,
.settings-row--category:active {
  background-color: var(--ds-surface-hover);
}
.settings-row--category:active {
  opacity: 0.7;
}

.settings-row__chevron {
  font-size: 1.5rem;
  font-weight: 300;
  color: var(--ds-text-muted);
  margin-left: auto;
  padding-left: var(--ds-space-2);
  align-self: center;
}

/* Ф2a: центрована колонка + ghost-скролбар — з @layer overrides.
   Було: full-bleed скрол на всю ширину, контроли впритул до правого
   краю — «гараж». */
.settings-drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--ds-space-4);
  max-width: 38.75rem;
  margin-inline: auto;
  width: 100%;
  scrollbar-width: none;
}
.settings-drawer__body::-webkit-scrollbar { display: none; }

/* Groups — playlist-bot `.playlist-card` :1169-1190 (Ф1b план §6): bg
   raised (не overlay), radius-md (12px, було lg=16), рамка strong на
   hover — рахуємо через :has(), бо клікабельний елемент лежить
   УСЕРЕДИНІ картки (`.settings-row--category`), не сама картка. */
.settings-group {
  margin-bottom: var(--space-5);
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-md);
  padding: 0 var(--ds-space-4);
  transition: border-color var(--duration-fast) var(--ease-out);
}
.settings-group:has(.settings-row--category:hover) {
  border-color: var(--ds-border-strong);
}

/* Заголовок секції без підкреслення (картка вже обрамляє) + моно-eyebrow,
   як у списку діалогів. */
.settings-group__title {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ds-meta-size);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ds-text-subtle);
  margin: 0;
  padding: var(--ds-space-3) 0 var(--ds-space-1);
}

/* Рядки всередині картки: комфортна висота + hairline-розділювач між
   ними, а не суцільний паддинг. */
.settings-group .settings-row {
  padding: var(--ds-space-3) 0;
  border-top: 0.0625rem solid var(--ds-border-subtle);
}
.settings-group .settings-row__label { font-size: var(--t3); }

/* Row: label left, control right */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--ds-space-2) 0;
  gap: var(--ds-space-3);
  font-size: var(--ds-body-size);
  color: var(--ds-text-primary);
}

.settings-row__control {
  display: flex;
  gap: var(--ds-space-1);
  align-items: center;
  flex-shrink: 0;
}

.settings-row__control .btn {
  width: auto;
}
/* Активний сегмент у settings-row__control (theme/model-toggle-подібні
   пари кнопок) несе акцентний зелений; решта лишається тихою. Ф2a,
   overrides — той самий принцип, узагальнений пізніше на .btn.active
   в 40-composer.css. */
.settings-row__control .btn.active,
.settings-row__control .btn[aria-pressed="true"] {
  background: var(--ds-accent-subtle);
  border-color: var(--ds-accent);
  color: var(--ds-accent);
}

.settings-row--danger span {
  color: var(--ds-text-muted);
}

/* Block: vertical stack for complex sections */
.settings-block {
  padding: var(--ds-space-3) 0;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.settings-block:last-child {
  border-bottom: none;
}

/* Ф2a, з @layer overrides — «Settings profile cleanup»: блоки (Telegram/
   Email) мали сирий svg+текст на одній базовій лінії, а форми/статус
   впритул зліва під плиткою іконки — рваний лівий край. Вирівнюємо
   текстову колонку кожного блоку ПІСЛЯ плитки іконки (2rem + space-1). */
.settings-block__label {
  font-size: var(--t3);
  font-weight: 500;
  color: var(--ds-text-primary);
  margin-bottom: var(--ds-space-1);
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
}

.settings-block__status,
.settings-block__error,
.settings-block__mono,
.settings-block form {
  margin-left: calc(2rem + var(--ds-space-1));
}
.settings-block form {
  display: flex;
  gap: var(--ds-space-2);
  align-items: center;
  margin-top: var(--ds-space-2);
}
.settings-block form .input { flex: 1; min-width: 0; }

.settings-block__status {
  font-size: var(--ds-label-size);
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-2);
}

.settings-block__error {
  font-size: var(--ds-label-size);
  color: var(--ds-feedback-danger);
  margin-top: var(--ds-space-1);
}

/* Успішний результат дії у рядку налаштувань (зміна PIN тощо) — той самий
   розмір і відступ, що в .settings-block__error, інша роль кольору. */
.settings-row__hint--ok {
  color: var(--ds-feedback-success);
}

.settings-block__mono {
  font-family: var(--font-mono);
  font-size: var(--ds-body-size);
  margin: var(--ds-space-2) 0;
}

/* .settings-block__code лишився без margin-left: жоден рендерер його не
   випускає (grep: templates/, src/, crates/ui-organisms — 0 хітів),
   тож своєї власної версії margin-left він у Ф2a не отримав. */
.settings-block__code {
  margin: var(--ds-space-2) 0;
}

.settings-block__code span {
  font-size: 1.5em;
  letter-spacing: 0.2em;
  font-family: var(--font-mono);
}

.settings-block__row {
  display: flex;
  gap: var(--ds-space-2);
  align-items: center;
  margin-top: var(--ds-space-2);
}

.settings-block__row .input {
  flex: 1;
  max-width: 8.75rem;
}

.settings-block__form {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

.settings-block__form .input {
  width: 100%;
}

.settings-block .btn {
  width: auto;
}

/* Signature: full-width editor, action under it — not an input squeezed
   against the right edge. Ф2a, з @layer overrides. */
.settings-block .settings-row {
  flex-direction: column;
  align-items: stretch;
  gap: var(--ds-space-2);
}
.settings-block .settings-row .settings-row__control { width: 100%; }
.settings-block .settings-row .settings-row__control .btn { align-self: flex-end; }

/* Профіль у стилі списків: стек-рядки на всю ширину, значення справа
   моно, підказки дрібним під рядком. */
.settings-row--stack {
  flex-direction: column;
  align-items: stretch;
  gap: var(--ds-space-2);
}
.settings-row--stack .input,
.settings-row--stack form { width: 100%; }
.settings-row--stack form { display: flex; gap: var(--ds-space-2); align-items: center; }
.settings-row--stack form .input { flex: 1; min-width: 0; }
.settings-row--stack .btn { width: auto; align-self: flex-end; }
.settings-group .settings-row--tight { border-top: none; padding-top: 0; }
.settings-group .settings-row--hint { border-top: none; padding: 0 0 var(--ds-space-3); }
.settings-row__value {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  text-align: right;
  word-break: break-all;
}
.settings-row__sub { display: inline-flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.settings-row__sub .settings-row__value { text-align: left; }
.settings-row__control select.input--small { max-width: 16rem; }

/* Leading icon → плитка, як у справжньому застосунку налаштувань.
   Ф2a: перенесено з 80-media-preview.css (де жила лише SVG-розмірна
   версія) і змерджено зі стилем-плиткою, що жив у @layer overrides —
   там завжди перемагав шаром, тож 1.125rem/margin-space-2/fg-muted
   ніколи фактично не рендерились. */
.settings-row__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  vertical-align: -0.15em;
  margin-inline-end: var(--ds-space-1);
  border-radius: var(--ds-radius-md);
  background: var(--ds-surface-hover);
  color: var(--ds-text-secondary);
  stroke-width: 2;
}
.settings-row__icon svg { width: 1.05rem; height: 1.05rem; }
/* Топ-рівень навігації (одна категорія = одна картка) отримує велику
   плитку — playlist-bot `.playlist-icon` :1191-1200 (48px, accent-subtle
   тінт). Рядки-тумблери всередині категорії лишають малу 2rem плитку
   вище — там на екрані їх багато поспіль, 48px був би завеликим. */
.settings-row--category .settings-row__icon {
  width: 3rem;
  height: 3rem;
  border-radius: var(--ds-radius-md);
  background: var(--ds-accent-subtle);
  color: var(--ds-accent);
}
.settings-row--category .settings-row__icon svg { width: 1.375rem; height: 1.375rem; }
.settings-row--category[hx-post] { cursor: pointer; }

/* Список TG-акаунтів у профілі. */
.tg-vis { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }

/* Foundation: перемикач on/off (заміна «2 кнопок»-сегментеда там, де
   налаштування бінарне). Розмітка: <label class="switch"><input
   type=checkbox><span class="switch__slider">. */
.switch {
  position: relative;
  display: inline-flex;
  width: 2.75rem;
  height: 1.625rem;
  flex-shrink: 0;
  cursor: pointer;
}
.switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.switch__slider {
  flex: 1;
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-default);
  transition: background-color var(--ds-motion) var(--ds-ease-out),
              border-color var(--ds-motion) var(--ds-ease-out);
}
.switch__slider::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0.1875rem;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--ds-radius-full);
  background: var(--p-neutral-0);
  transform: translateY(-50%);
  box-shadow: 0 0.0625rem 0.125rem color-mix(in srgb, var(--p-neutral-1000) 30%, transparent);
  transition: transform var(--ds-motion) var(--ds-ease-out);
}
.switch input:checked + .switch__slider {
  background: var(--ds-accent);
  border-color: transparent;
}
.switch input:checked + .switch__slider::before {
  transform: translate(1.125rem, -50%);
}

/* Drawer is already full-width bottom-sheet; mobile override no longer needed. */

.modal__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-6);
}

.modal__header h2 {
  font-size: var(--t2);
  font-weight: 600;
  margin: 0;
}

.modal__header .btn--icon {
  width: 2rem;
  height: 2rem;
  background: var(--ds-surface-overlay);
  border-radius: 50%;
  flex-shrink: 0;
}

.modal__header .btn--icon:hover {
  background: var(--ds-surface-hover);
}

.modal h2 {
  font-size: var(--ds-meta-size);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ds-text-secondary);
  margin-bottom: var(--ds-space-4);
}

.modal input {
  width: 100%;
  padding: var(--ds-space-3) var(--ds-space-4);
  font-size: 1rem;
  font-family: var(--ds-font);
  background: var(--ds-surface-overlay);
  border: none;
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  outline: none;
  margin-bottom: var(--ds-space-3);
}

.modal input:focus {
  background: var(--ds-surface-hover);
}

.modal input::placeholder {
  color: var(--ds-text-muted);
}

/* Only full-width for submit buttons, not icon buttons */
.modal > .btn,
.settings-section .btn {
  width: 100%;
}

/* Settings sections */
.settings-section {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-6);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.settings-section:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
}

.settings-section h3 {
  font-size: var(--ds-body-size);
  font-weight: 600;
  color: var(--ds-text-primary);
  margin-bottom: var(--ds-space-3);
}

.settings-hint {
  font-size: 0.6875rem;
  color: var(--ds-text-muted);
  margin-top: 0.375rem;
}

.settings-status {
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  margin-bottom: var(--ds-space-3);
  line-height: 1.5;
}

.settings-status.connected {
  color: var(--ds-feedback-success);
}

.settings-status.unlocked {
  color: var(--ds-text-primary);
}

.settings-status.waiting {
  color: var(--warning);
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

.tg-login {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

.tg-login .input--small {
  padding: var(--ds-space-3) var(--ds-space-4);
  font-size: 1rem;
  background: var(--ds-surface-overlay);
  border: none;
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  outline: none;
  transition: background 0.15s ease;
}

.tg-login .input--small::placeholder {
  color: var(--ds-text-muted);
}

.tg-login .input--small:focus {
  background: var(--ds-surface-hover);
}

.tg-login .btn {
  margin-top: var(--ds-space-2);
}

.tg-error {
  font-size: var(--ds-meta-size);
  color: var(--ds-feedback-danger);
  letter-spacing: 0.03em;
}

/* Telegram share/link */
.tg-share,
.tg-link {
  margin-top: var(--ds-space-4);
}

.tg-share-row,
.tg-link-row {
  display: flex;
  gap: var(--ds-space-3);
  align-items: center;
}

.tg-share-code {
  font-family: var(--font-mono, monospace);
  font-size: 1.125rem;
  letter-spacing: 0.1875rem;
  color: var(--ds-text-primary);
  font-weight: 600;
}

.tg-link-input {
  width: 6.875rem;
  flex-shrink: 0;
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.125rem;
  text-align: center;
}

.tg-link-status {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  margin-top: var(--ds-space-2);
}

/* Inline button — doesn't stretch to full width */
.btn--inline {
  width: auto;
  flex-shrink: 0;
}

/* Settings danger button */
.settings-section .btn--danger {
  background: color-mix(in srgb, var(--ds-feedback-danger) 10%, transparent);
  color: var(--ds-feedback-danger);
  border: none;
  border-radius: var(--ds-radius-sm);
}

.settings-section .btn--danger:hover {
  background: color-mix(in srgb, var(--ds-feedback-danger) 20%, transparent);
}
}

/* ── 65-feedback.css ── */
/* ЗВОРОТНИЙ ЗВ'ЯЗОК — тости, попап оновлення, смуга прокрутки
 *
 * Дрібні, але наскрізні поверхні: показуються поверх будь-якого екрана,
 * тому їхні правила не можна ховати всередину однієї поверхні.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   NOTIFICATION (legacy — hidden, replaced by toasts)
   ═══════════════════════════════════════════ */

.notification {
  display: none;
}

/* ═══════════════════════════════════════════
   TOASTS
   ═══════════════════════════════════════════ */

.toast-container {
  position: fixed;
  bottom: var(--ds-space-5);
  left: 50%;
  transform: translateX(-50%);
  z-index: 600;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--ds-space-2);
  pointer-events: none;
  max-width: 90vw;
  width: 22.5rem;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  font-size: var(--ds-body-size);
  font-family: var(--ds-font);
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 0.2s, transform 0.2s;
}

.toast--visible {
  opacity: 1;
  transform: translateY(0);
}

.toast--exit {
  animation: toastExit 0.25s ease-in forwards;
}

@keyframes toastExit {
  to { opacity: 0; transform: translateY(-0.5rem) scale(0.95); }
}

.toast__icon {
  flex-shrink: 0;
  display: flex;
  color: var(--ds-text-secondary);
}

.toast--success .toast__icon { color: var(--ds-feedback-success); }
.toast--error .toast__icon { color: var(--ds-feedback-danger); }

.toast__text {
  flex: 1;
  min-width: 0;
}

.toast__progress {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 0.125rem;
  background: var(--ds-accent);
  animation: toastProgress 3.5s linear forwards;
  border-radius: 0 0 var(--ds-radius-sm) var(--ds-radius-sm);
}

@keyframes toastProgress {
  from { width: 100%; }
  to { width: 0%; }
}

@keyframes notifyIn {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(1rem);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* ═══════════════════════════════════════════
   UPDATE POPUP
   ═══════════════════════════════════════════ */

.update-popup {
  position: fixed;
  top: var(--ds-space-6);
  right: var(--ds-space-6);
  z-index: 1000;
  animation: slideInRight 0.3s ease;
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.update-popup__content {
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-strong);
  padding: var(--ds-space-5);
  display: flex;
  gap: var(--ds-space-4);
  align-items: flex-start;
  max-width: 20rem;
  box-shadow: 0 0.5rem 2rem color-mix(in srgb, black 30%, transparent);
}

.update-popup__icon {
  font-size: var(--t2);
  line-height: 1;
}

.update-popup__text {
  flex: 1;
}

.update-popup__title {
  font-weight: 600;
  font-size: var(--ds-body-size);
  margin-bottom: var(--ds-space-1);
}

.update-popup__version {
  font-size: var(--ds-meta-size);
  color: var(--ds-accent);
  font-family: var(--font-mono);
}

.update-popup__message {
  font-size: var(--ds-label-size);
  color: var(--ds-text-subtle);
  margin-top: var(--ds-space-2);
}

.update-popup__actions {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

.update-popup__btn {
  white-space: nowrap;
}

.update-popup__dismiss {
  font-size: var(--ds-meta-size);
  padding: var(--ds-space-1) var(--ds-space-2);
}

/* ═══════════════════════════════════════════
   SCROLLBAR
   ═══════════════════════════════════════════ */

::-webkit-scrollbar {
  width: 0.5rem;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--ds-border-strong);
  border-radius: var(--ds-radius-full);
  border: 0.125rem solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--ds-text-muted);
  background-clip: content-box;
}

/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--ds-border-strong) transparent;
}
}

/* ── 70-widgets.css ── */
/* ВІДЖЕТИ — картки, що малює LLM (погода, новини, курси, замовлення)
 *
 * Малює JS; частина класів може бути мертвою. Аудит — у Ф2/Ф3, тут
 * лише перенесення блоком, щоб не змінити рендер.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   WIDGET SYSTEM - AI-Rendered Components
   ═══════════════════════════════════════════ */

.widget {
  margin: var(--ds-space-2) 0;
  padding: var(--ds-space-3);
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-meta-size);
}

.widget.w-error {
  background: color-mix(in srgb, var(--ds-feedback-danger) 8%, transparent);
  border-color: color-mix(in srgb, var(--ds-feedback-danger) 20%, transparent);
  color: var(--ds-feedback-danger);
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

/* Widget Layout */
.w-header {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding-bottom: 0.375rem;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  margin-bottom: 0.375rem;
}

.w-body {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
}

.w-footer {
  margin-top: 0.375rem;
  padding-top: var(--ds-space-1);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  font-size: var(--ds-eyebrow-size);
}

/* Widget Flex */
.w-row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  flex-wrap: wrap;
}

.w-col {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
}

/* Widget Typography */
.w-title {
  font-weight: 500;
  font-size: var(--ds-label-size);
}

.w-meta {
  margin-inline-start: auto;
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
}

.w-label {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
}

.w-value {
  font-family: var(--font-mono);
  color: var(--ds-text-primary);
}

.w-big {
  font-size: var(--ds-body-size);
  font-weight: 600;
  font-family: var(--font-mono);
  color: var(--ds-text-primary);
}

.w-highlight {
  color: var(--ds-accent);
}

.w-muted {
  color: var(--ds-text-subtle);
  font-size: var(--ds-eyebrow-size);
}

/* Widget List */
.w-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.w-list-item {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: var(--ds-space-1) 0;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  animation: w-anim-slide 0.3s ease-out backwards;
}

.w-list-item:last-child {
  border-bottom: none;
}

.w-list-item .w-muted {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
}

/* Widget Icons */
.w-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  color: currentColor;
}

.w-icon--sm {
  width: 0.875rem;
  height: 0.875rem;
}

.w-icon svg {
  width: 100%;
  height: 100%;
}

/* Widget Animations */
@keyframes w-anim-fade-kf {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes w-anim-slide-kf {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes w-anim-pulse-kf {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

@keyframes w-anim-bounce-kf {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-0.25rem);
  }
}

.w-anim-fade {
  animation: w-anim-fade-kf 0.3s ease-out;
}

.w-anim-slide {
  animation: w-anim-slide-kf 0.3s ease-out;
}

.w-anim-pulse {
  animation: w-anim-pulse-kf 2s ease-in-out infinite;
}

.w-anim-bounce {
  animation: w-anim-bounce-kf 1s ease-in-out infinite;
}

/* Staggered list animations */
.w-list-item:nth-child(1) {
  animation-delay: 0ms;
}
.w-list-item:nth-child(2) {
  animation-delay: 50ms;
}
.w-list-item:nth-child(3) {
  animation-delay: 100ms;
}
.w-list-item:nth-child(4) {
  animation-delay: 150ms;
}
.w-list-item:nth-child(5) {
  animation-delay: 200ms;
}
.w-list-item:nth-child(6) {
  animation-delay: 250ms;
}

/* File Transfer Styles */
.file-transfer-container {
  position: fixed;
  bottom: 4.375rem;
  right: 1.25rem;
  width: 18.75rem;
  max-height: 25rem;
  overflow-y: auto;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
  padding: 0.625rem;
  z-index: 1000;
  box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, black 30%, transparent);
}

.file-transfer-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.625rem;
  padding-bottom: 0.3125rem;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.file-transfer-header h4 {
  margin: 0;
  font-size: 0.875rem;
  font-weight: 600;
}

.file-transfer-item {
  margin-bottom: 0.5rem;
  padding: 0.5rem;
  /* --bg-primary не оголошений ні в src/css.rs, ні деінде: висяча легасі-
     назва, яка мовчки резолвиться у прозорий фон і в проді (static/css),
     і тут — переносимо доказово побитим, не вигадуючи значення. */
  background: var(--bg-primary);
  border-radius: var(--ds-radius-sm);
  border: 0.0625rem solid var(--ds-border-subtle);
  transition: all 0.2s ease;
}

.file-transfer-item:hover {
  background: var(--ds-surface-hover);
}

.file-transfer-item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--ds-space-1);
}

.file-transfer-item-name {
  font-weight: bold;
  font-size: 0.75rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 11.25rem;
}

.file-transfer-item-size {
  font-size: 0.6875rem;
  color: var(--ds-text-secondary);
}

.file-transfer-item-type {
  font-size: 0.6875rem;
  color: var(--ds-text-secondary);
  margin-inline-start: 0.3125rem;
}

.file-transfer-progress {
  height: var(--ds-space-1);
  background: var(--ds-surface-raised);
  border-radius: var(--ds-radius-xs);
  overflow: hidden;
  margin: var(--ds-space-1) 0;
}

.file-transfer-progress-bar {
  height: 100%;
  width: 0%;
  background: var(--ds-accent);
  transition: width 0.3s ease;
}

.file-transfer-item-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.625rem;
  color: var(--ds-text-secondary);
}

.file-transfer-status {
  transition: color 0.3s ease;
}

.file-transfer-status.error {
  color: var(--ds-feedback-danger);
}

.file-transfer-status.success {
  color: var(--ds-feedback-success);
}

.file-transfer-peer {
  font-size: 0.625rem;
  opacity: 0.8;
}

/* File message in chat */
.message.file-message {
  padding: 0.5rem 0.75rem;
  border-radius: 0.75rem;
  max-width: 70%;
  margin: var(--ds-space-1) 0;
  position: relative;
}

.message.file-message.outgoing {
  background: var(--ds-accent-subtle);
  border: 0.0625rem solid var(--ds-accent);
  align-self: flex-end;
  margin-inline-start: auto;
}

.message.file-message.incoming {
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  align-self: flex-start;
}

.file-message-content {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.file-message-icon {
  font-size: 1rem;
}

.file-message-info {
  display: flex;
  flex-direction: column;
}

.file-message-name {
  font-weight: 500;
  font-size: 0.875rem;
}

.file-message-size {
  font-size: 0.6875rem;
  color: var(--ds-text-secondary);
  margin-top: 0.125rem;
}

.file-message-download {
  margin-inline-start: auto;
  font-size: 0.75rem;
  color: var(--ds-accent);
  text-decoration: none;
}

.file-message-download:hover {
  text-decoration: underline;
}

/* ─── File Cards (Beautify) ─── */
.file-card {
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
  overflow: hidden;
  max-width: 20rem;
  min-width: 12.5rem;
}

.file-card__preview {
  display: block;
  max-height: 15rem;
  overflow: hidden;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  cursor: pointer;
}

.file-card__preview img {
  width: 100%;
  display: block;
  object-fit: cover;
  max-height: 15rem;
  transition: transform 0.2s ease;
}

.file-card__preview:hover img {
  transform: scale(1.03);
}

.file-card__info {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.625rem;
}

.file-card__icon-wrap {
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ds-surface-raised);
  border-radius: var(--ds-radius-md);
}

.file-card__icon {
  width: 1.125rem;
  height: 1.125rem;
  color: var(--ds-text-secondary);
}

/* Категорійні кольори типів файлів — не статус і не бренд, а власна
   палітра «іконка = тип», якої в DESIGN_SYSTEM.md немає (лише accent/
   success/warning/danger). Ф2b мапить їх на ці 4 ролі; лоссі — 5 типів
   стиснуто в 4 токени (audio і archive ділять warning, video йде в
   accent за браком фіолетового primitve). Було/стало — у звіті фази. */
.file-card--image .file-card__icon { color: var(--ds-feedback-success); }
.file-card--video .file-card__icon { color: var(--ds-accent); }
.file-card--audio .file-card__icon { color: var(--ds-feedback-warning); }
.file-card--pdf .file-card__icon { color: var(--ds-feedback-danger); }
.file-card--archive .file-card__icon { color: var(--ds-feedback-warning); }
.file-card--doc .file-card__icon { color: var(--ds-text-primary); }

.file-card__details {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
}

.file-card__name {
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-card__size {
  font-size: 0.6875rem;
  color: var(--ds-text-secondary);
}

.file-card__download {
  flex-shrink: 0;
  color: var(--ds-accent);
  opacity: 0.7;
  transition: opacity 0.15s;
  display: flex;
  align-items: center;
  padding: var(--ds-space-1);
  border-radius: var(--ds-radius-sm);
}

.file-card__download:hover {
  opacity: 1;
  background: var(--ds-surface-hover);
}

/* File button in input area */
.btn--file {
  background: transparent;
  border: none;
  color: var(--ds-text-secondary);
  padding: 0.5rem;
  cursor: pointer;
  border-radius: var(--ds-radius-sm);
  transition: all 0.2s ease;
}

.btn--file:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
}

.btn--file svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* ── Share Dialog ── */
.modal--share {
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: 0.75rem;
  padding: 0;
  max-width: 25rem;
  width: 90vw;
  /* --fg-primary: як і --bg-primary вище, ніде не оголошений (нуль
     збігів по css.rs і всіх *.css) — переносимо як є. */
  color: var(--fg-primary);
}
.modal--share::backdrop {
  background: color-mix(in srgb, black 85%, transparent);
}

.share-contacts {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
  max-height: 50vh;
  overflow-y: auto;
}

.share-contact-item {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2);
  border-radius: var(--ds-radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}

.share-contact-item:hover {
  background: var(--ds-surface-overlay);
}

.share-contact-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6875rem;
  font-weight: 700;
  flex-shrink: 0;
}

.share-contact-name {
  flex: 1;
  font-size: var(--ds-body-size);
  font-weight: 500;
}

.share-contact-btn {
  padding: var(--ds-space-1) 0.75rem;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  border: none;
  border-radius: var(--ds-radius-sm);
  cursor: pointer;
  transition: opacity var(--duration-fast);
}

.share-contact-btn:hover {
  opacity: 0.85;
}

.share-contact-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.share-contact-btn--done {
  background: var(--ds-feedback-success);
  color: white;
}

.share-empty,
.share-loading {
  text-align: center;
  padding: var(--ds-space-3);
  color: var(--ds-text-secondary);
  font-size: var(--ds-body-size);
}

.chat-header__from {
  display: block;
  font-size: 0.625rem;
  color: var(--ds-text-secondary);
  font-weight: 400;
  opacity: 0.7;
}

.chat-header__status--shared {
  color: var(--ds-text-secondary);
  font-size: 0.6875rem;
  font-weight: 400;
}

.dialog-source--shared {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
}

.dialog-avatar--shared {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
}

/* AI Email Assistant button */
#ai-email-btn {
  display: none;
  align-items: center;
  justify-content: center;
  background: var(--ds-accent);
  border: none;
  border-radius: var(--ds-radius-md);
  color: white;
  cursor: pointer;
  padding: var(--ds-space-1) 0.5rem;
  margin-inline-start: 0.375rem;
  transition: opacity 0.2s;
}
#ai-email-btn:hover { opacity: 0.85; }

/* ── AI Inline Draft Mode ── */
.ai-model-select {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-md);
  padding: 0.125rem 0.375rem;
  font-size: 0.6875rem;
  cursor: pointer;
  outline: none;
  margin-inline-start: var(--ds-space-1);
}
.ai-model-select:focus {
  border-color: var(--ds-accent);
}
.btn--send--ai {
  background: var(--ds-accent);
  color: white;
  border-radius: 50%;
  transition: all 0.2s var(--ease-out);
}

.btn--send--ai svg {
  stroke: white;
}
/* Було #a855f7 (фіолетовий) — окремий колір лише для цієї кнопки,
   фіолетового primitive в системі немає. Ф2b: accent (зелений бренду),
   документовано як задумана зміна — див. звіт фази. */
.btn--send--ai-dialog {
  background: var(--ds-accent);
  color: white;
  border-radius: 50%;
  transition: all 0.2s var(--ease-out);
}
.btn--send--ai-dialog svg {
  stroke: white;
}

.message--ai-draft .message__bubble {
  border: 0.0625rem solid var(--ds-border-strong);
  background: var(--ds-surface-raised);
}

.ai-draft-header {
  font-size: 0.625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03125rem;
  color: var(--ds-accent);
  margin-bottom: var(--ds-space-1);
}

.ai-draft-text {
  font-size: var(--ds-body-size);
  line-height: 1.5;
}
.ai-draft-text--editing {
  outline: 0.125rem solid var(--ds-accent);
  border-radius: var(--ds-radius-sm);
  padding: 0.5rem;
  background: var(--ds-surface-base);
  min-height: 3.75rem;
  cursor: text;
}

.ai-draft-error {
  color: var(--ds-feedback-danger);
  font-size: var(--ds-label-size);
}

.ai-draft-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.625rem;
  flex-wrap: wrap;
}

.btn--draft-send,
.btn--draft-edit,
.btn--draft-retry,
.btn--draft-continue {
  padding: 0.375rem 0.875rem;
  font-size: 0.75rem;
  font-weight: 600;
  border: none;
  border-radius: var(--ds-radius-sm);
  cursor: pointer;
  transition: opacity 0.15s;
}

.btn--draft-send {
  background: var(--ds-feedback-success);
  color: white;
}

.btn--draft-edit {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-primary);
}

.btn--draft-retry {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-secondary);
}

.btn--draft-continue {
  background: var(--ds-accent);
  color: white;
}

.btn--draft-send:hover,
.btn--draft-edit:hover,
.btn--draft-retry:hover,
.btn--draft-continue:hover {
  opacity: 0.85;
}

.message--ai-instruction .message__bubble {
  opacity: 0.7;
  font-style: italic;
}

/* ============ Cases ============ */
.case-view {
  padding: 1rem 1.25rem;
  overflow-y: auto;
  height: 100%;
}
.case-header {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.case-title {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0;
  color: var(--fg-primary);
}
.case-status {
  font-size: 0.6875rem;
  padding: 0.125rem 0.5rem;
  border-radius: 0.625rem;
  font-weight: 500;
}
.case-status--open {
  background: color-mix(in srgb, var(--ds-feedback-success) 15%, transparent);
  color: var(--ds-feedback-success);
}
.case-status--closed {
  background: color-mix(in srgb, var(--ds-text-muted) 15%, transparent);
  color: var(--ds-text-muted);
}
.case-header__actions {
  margin-inline-start: auto;
  display: flex;
  gap: 0.375rem;
}
/* Було rgba(33,150,243,…)/#64b5f6 (material blue) — синього primitive
   в системі теж немає (як і фіолетового вище). Ф2b: accent. */
.case-btn-generate {
  background: color-mix(in srgb, var(--ds-accent) 15%, transparent);
  color: var(--ds-accent);
}
.case-btn-generate:hover {
  background: color-mix(in srgb, var(--ds-accent) 25%, transparent);
}

/* Case Members */
.case-members {
  border-top: 0.0625rem solid var(--ds-border-subtle);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  padding: 0.75rem 0;
  margin-bottom: 1rem;
}
.case-members h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin: 0 0 0.5rem 0;
  letter-spacing: 0.03125rem;
}
.case-member {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--ds-space-1) 0;
  font-size: 0.8125rem;
}
.case-member--unlinked {
  opacity: 0.4;
}
.case-member__name {
  color: var(--fg-primary);
  font-weight: 500;
}
.case-member__role {
  color: var(--ds-text-muted);
  font-size: 0.6875rem;
}
.case-member__unlinked {
  color: var(--ds-feedback-danger);
  font-size: 0.625rem;
  font-style: italic;
}
.case-member__goto {
  margin-inline-start: auto;
  font-size: 1rem;
  opacity: 0.5;
  cursor: pointer;
  background: none;
  border: none;
  color: var(--ds-text-secondary);
  padding: 0.125rem 0.375rem;
}
.case-member__goto:hover {
  opacity: 1;
}
.case-btn-add-member {
  margin-top: 0.375rem;
  font-size: 0.6875rem;
  opacity: 0.6;
}

/* Channel Badge */
.channel-badge {
  font-size: 0.5625rem;
  padding: 0.0625rem 0.3125rem;
  border-radius: 0.1875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.01875rem;
}
/* Було rgba(33,150,243,.2)/#64b5f6 — той самий «синій без токена», що й
   .case-btn-generate вище; той самий компроміс на accent. */
.channel-badge--tg {
  background: color-mix(in srgb, var(--ds-accent) 20%, transparent);
  color: var(--ds-accent);
}
.channel-badge--email {
  background: color-mix(in srgb, var(--ds-feedback-warning) 20%, transparent);
  color: var(--ds-feedback-warning);
}
.channel-badge--p2p {
  background: color-mix(in srgb, var(--ds-feedback-success) 20%, transparent);
  color: var(--ds-feedback-success);
}

/* Case Body (rendered markdown) */
.case-body {
  line-height: 1.6;
  color: var(--ds-text-secondary);
  font-size: 0.8125rem;
}
.case-body h1 {
  font-size: 1.125rem;
  color: var(--fg-primary);
  margin: 1.25rem 0 0.5rem;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  padding-bottom: var(--ds-space-1);
}
.case-body h2 {
  font-size: var(--t2);
  color: var(--fg-primary);
  margin: 1.125rem 0 0.375rem;
}
.case-body h3 {
  font-size: 0.8125rem;
  color: var(--ds-text-secondary);
  margin: 0.875rem 0 var(--ds-space-1);
}
.case-body p {
  margin: var(--ds-space-1) 0;
}
.case-body ul {
  margin: var(--ds-space-1) 0;
  padding-inline-start: 1.25rem;
}
.case-body li {
  margin: 0.125rem 0;
}
.case-body blockquote {
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  margin: 0.375rem 0;
  padding: var(--ds-space-1) 0.75rem;
  color: var(--ds-text-muted);
  font-style: italic;
}
.case-body table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.5rem 0;
  font-size: 0.75rem;
}
.case-body th,
.case-body td {
  border: 0.0625rem solid var(--ds-border-subtle);
  padding: var(--ds-space-1) 0.5rem;
  text-align: left;
}
.case-body th {
  background: var(--ds-surface-active);
  font-weight: 600;
  color: var(--fg-primary);
}
.case-body code {
  background: var(--ds-surface-active);
  padding: 0.0625rem var(--ds-space-1);
  border-radius: 0.1875rem;
  font-size: 0.75rem;
}
.case-body pre {
  background: var(--ds-surface-active);
  padding: 0.625rem;
  border-radius: var(--ds-radius-md);
  overflow-x: auto;
  margin: 0.5rem 0;
}
.case-body pre code {
  background: none;
  padding: 0;
}
.case-body strong {
  color: var(--fg-primary);
}
.case-body__empty {
  color: var(--ds-text-muted);
  font-style: italic;
  text-align: center;
  padding: 2.5rem 0;
}
.case-body__generating {
  color: var(--ds-text-muted);
  font-style: italic;
  animation: pulse 1.5s ease-in-out infinite;
}
.case-body__error {
  color: var(--ds-feedback-danger);
}
.case-body__editor {
  width: 100%;
  min-height: 18.75rem;
  background: var(--ds-surface-active);
  color: var(--fg-primary);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-md);
  padding: 0.625rem;
  font-family: monospace;
  font-size: 0.75rem;
  resize: vertical;
}
.case-body__editor-actions {
  display: flex;
  gap: 0.375rem;
  margin-top: 0.5rem;
}

/* Case Checklist */
.case-checklist {
  list-style: none;
  padding-inline-start: 0;
}
.case-checklist li::before {
  content: "\2610 ";
}
.case-checklist li.case-todo-done::before {
  content: "\2611 ";
}
.case-checklist li.case-todo-done {
  text-decoration: line-through;
  opacity: 0.6;
}

/* Case Todos */
.case-todos {
  border-top: 0.0625rem solid var(--ds-border-subtle);
  padding: 0.75rem 0;
  margin-top: 1rem;
}
.case-todos h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin: 0 0 0.5rem 0;
  letter-spacing: 0.03125rem;
}
.case-todo {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.1875rem 0;
  font-size: 0.8125rem;
  color: var(--ds-text-secondary);
}
.case-todo input[type="checkbox"] {
  cursor: pointer;
  accent-color: var(--ds-accent);
}
.case-todo__done {
  text-decoration: line-through;
  opacity: 0.5;
}
.case-todo__input {
  background: transparent;
  border: none;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  color: var(--fg-primary);
  font-size: 0.8125rem;
  padding: var(--ds-space-1) 0;
  width: 100%;
  outline: none;
}
.case-todo__input:focus {
  border-color: var(--ds-accent);
}

/* Chat header status for cases */
.chat-header__status--case {
  color: var(--ds-text-muted);
  font-size: 0.6875rem;
  margin-inline-start: 0.5rem;
}

/* Case context menu */
.case-ctx-menu {
  position: fixed;
  z-index: 999;
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-1) 0;
  min-width: 11.25rem;
  box-shadow: 0 0.25rem 1rem color-mix(in srgb, black 40%, transparent);
}
.case-ctx-item {
  padding: 0.5rem 1rem;
  cursor: pointer;
  font-size: 0.8125rem;
  color: var(--ds-text-secondary);
}
.case-ctx-item:hover {
  background: var(--ds-surface-hover);
}
.case-ctx-item--danger {
  color: var(--ds-feedback-danger);
}
}

/* ── 72-rooms.css ── */
/* ═══════════════════════════════════════════════════════════════════
   ROOMS — forum pass (beseda 05.09.2026: "Кімната й теми як форум
   Telegram" — density, forum-style topics, colored author names,
   quick actions). Room detail panel, topic rows, tag chips, chat-header
   crumb/meta, room-post author styling, composer draft-mode segment.
   Tokens only — see DESIGN_SYSTEM.md §9. Owner: R (handlers/components/
   rooms.rs, dialogs.rs) + ui_organisms (dialog_list.rs, message_list.rs,
   chat.rs — outside this repo's lint root, styled here by class).
   ═══════════════════════════════════════════════════════════════════ */
@layer components {

.room-view {
  /* 16 px = 2 модулі. Було 1.25rem збоку — некратне, і від нього
     розділи всередині рахували свої краї по-різному. */
  padding: 1rem;
  overflow-y: auto;
  height: 100%;
}

/* ── Room header: title + count, "+ Тема" toggle, participants ── */

/* Заголовок панелі кімнати прибрано: назву й лічильник тем уже показує
   шапка розмови над панеллю, а склад учасників — власний розділ
   «Учасники». Панель повторювала і те, і те (10.09.2026). Розділи
   малює `.room-section__head` у 50-call.css. */

/* ── Tag filter chips above the topic list ── */

.room-tagbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-bottom: 0.75rem;
}

.tag-chip--filter {
  cursor: pointer;
  border: 0.0625rem solid transparent;
  background: var(--ds-surface-hover);
  color: var(--ds-text-secondary);
}

.tag-chip--filter:hover,
.tag-chip--filter:focus-visible {
  border-color: var(--ds-border-default);
}

.tag-chip--filter.tag-chip--active {
  background: color-mix(in srgb, var(--ds-accent) 15%, transparent);
  color: var(--ds-accent);
}

/* ── Topic rows — forum thread list ── */

.room-topics {
  list-style: none;
  margin: 0 0 1rem 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
}

/* Дефект №9 (план §2): теми були картками з рамкою на кожен рядок —
   Swiss/Minimalism хоче рядки, розділені hairline, а не коробки. Один
   `border-bottom` на кожному рядку читається як розділювач списку
   (та сама мова, що в `.dialog-item`), не як окрема картка. */
.room-topic {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  min-height: 3.5rem;
  padding: 0.625rem 0.75rem;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  cursor: pointer;
}

.room-topic:last-child {
  border-bottom: none;
}

.room-topic:hover,
.room-topic:focus-visible {
  background: var(--ds-surface-hover);
}

/* Hit-area on touch: the row can stay visually tight, but a thumb needs
   the full 44px — the DS floor for anything tappable (§8, phone shots). */
@media (max-width: 30em) {
  .room-topic { min-height: 2.75rem; }
}

.room-topic__avatar {
  width: 2.25rem;
  height: 2.25rem;
  min-width: 2.25rem;
  border-radius: var(--ds-radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ds-meta-size);
  font-weight: 600;
  text-transform: uppercase;
  background: hsl(var(--avatar-hue, 220), 25%, 25%);
  color: hsl(var(--avatar-hue, 220), 30%, 72%);
  flex-shrink: 0;
}

[data-theme="light"] .room-topic__avatar {
  background: hsl(var(--avatar-hue, 220), 30%, 92%);
  color: hsl(var(--avatar-hue, 220), 40%, 40%);
}

.room-topic__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.room-topic__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.room-topic__title {
  font-weight: 600;
  color: var(--ds-text-primary);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.room-topic__project {
  font-size: 0.6875rem;
  color: var(--ds-text-muted);
  padding: 0.125rem 0.5rem;
  border-radius: var(--ds-radius-xs);
  background: var(--ds-surface-hover);
  flex-shrink: 0;
}

.room-topic__preview {
  font-size: 0.8125rem;
  color: var(--ds-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.room-topic__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.room-topic__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.25rem;
  flex-shrink: 0;
}

.room-topic__time {
  /* Дефект №11: Inter tabular-nums, не mono. */
  font-family: var(--ds-font);
  font-size: 0.6875rem;
  color: var(--ds-text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.room-topic__badge {
  font-size: 0.6875rem;
  font-weight: 600;
  min-width: 1.125rem;
  text-align: center;
  padding: 0.0625rem 0.375rem;
  border-radius: var(--ds-radius-xs);
  background: var(--ds-accent);
  color: var(--ds-on-accent);
}

/* `:not([hidden])` matters: a bare `.room-topic-add { display: flex }`
   outranks the `hidden` attribute's UA-stylesheet `display: none` (a
   class beats an attribute-default on specificity), so the "+ Тема" form
   rendered expanded by default despite the `hidden` attribute being on
   the element the whole time (beseda 05.09.2026 — confirmed in the live
   HTML, so this was a pure CSS bug, not a missing attribute). */
.room-topic-add:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  padding: 0.75rem 0;
  margin-bottom: 0.75rem;
  border-top: 0.0625rem solid var(--ds-border-subtle);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.room-topic-add .input--small {
  flex: 1;
  min-width: 8rem;
}

/* Tag chip — shared by topics, posts, and the filter bar. */
.tag-chip {
  display: inline-block;
  font-size: 0.625rem;
  font-weight: 500;
  line-height: 1;
  padding: 0.1875rem 0.4375rem;
  border-radius: var(--ds-radius-xs);
  background: color-mix(in srgb, var(--ds-accent) 15%, transparent);
  color: var(--ds-accent);
}

/* Tag row under a post's bubble text (RoomSource::post_html). */
.room-post__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.375rem;
}

/* Marker appended to a topic post after room_reply_client — "draft→tg:…" */
.room-post__meta {
  display: block;
  font-size: 0.6875rem;
  color: var(--ds-text-muted);
  margin-top: 0.25rem;
}

/* ── Chat header additions: room→topic breadcrumb + tags/participants
   meta line (ui_organisms::ChatHeader — outside this repo's lint root,
   inline-styled for base name/dot already; these two pieces get real
   classes since they're new surface, not a patch on existing markup). ── */

.chat-header__title-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

.chat-header__crumb {
  align-self: flex-start;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.chat-header__crumb::before {
  content: "‹ ";
}

.chat-header__crumb:hover,
.chat-header__crumb:focus-visible {
  color: var(--ds-text-secondary);
}

.chat-header__meta {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Multi-party topic posts (message_list.rs) ──
   A 1:1 dialog never sets `sender_kind`, so `.message--roompost` only
   ever appears on room/topic bubbles — safe to unconditionally show the
   header there without touching the (currently unused) `.messages--group`
   convention other dialog kinds still nominally rely on. */

/* Only INCOMING roomposts get a header — Telegram never labels your own
   messages either, and `message_view_to_bubble` sets `sender_kind` on
   BOTH sides (needed so several agents on the outgoing side could still
   be told apart), so the side has to be excluded here, in CSS, not by
   withholding the data. `:not(.message--grouped)`: a grouped bubble
   (same sender as the one above) must still hide its header — otherwise
   "name only on the first bubble of a run" (§8) breaks, since this file
   loads after `30-chat-messages.css` and would win an equal-specificity
   tie against `.message--grouped .message__header{display:none}`. */
.message--roompost.message--incoming:not(.message--grouped) .message__header {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 0.375rem;
  /* Base `.message__header` opens at 0 opacity/height via the
     `.messages--group` gate (dead — no container ever gets that class,
     05.09.2026 finding); it also comes with `margin-bottom: 0.125rem`
     from the base rule, kept. */
}

/* Base `.message__time` is built for the OLD "reveal on hover, next to
   the text" placement: `max-width: 0; overflow: hidden` collapses it to
   nothing until `:hover`. Inside a header it must stay a normal, always-
   visible flex item — the collapse rule left the header 90+px tall (a
   0-width box still lays out "17:01" as one letter per wrapped line,
   inflating height while width stayed ~0), which is the "величезні
   порожні поля" bug: the header's real content sat in a sliver at the
   top of a bubble stretched to fit that invisible column. Scoped to
   roomposts so the normal 1:1 hover-time behaviour is untouched. */
.message--roompost .message__header .message__time {
  opacity: 1;
  max-width: none;
  overflow: visible;
  flex-shrink: 0;
  white-space: nowrap;
  margin-left: auto;
}

.message__sender--tinted {
  color: hsl(var(--sender-hue, 220), 55%, 62%);
  font-weight: var(--ds-weight-strong);
  min-width: 0;
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

[data-theme="light"] .message__sender--tinted {
  color: hsl(var(--sender-hue, 220), 55%, 38%);
}

.message__sender-badge {
  font-size: 0.625rem;
  font-weight: 500;
  padding: 0.0625rem 0.375rem;
  border-radius: var(--ds-radius-xs);
  background: var(--ds-surface-hover);
  color: var(--ds-text-muted);
  text-transform: lowercase;
  flex-shrink: 0;
  white-space: nowrap;
}

/* Avatar column: deliberately omitted for room/topic posts (the "or
   without" half of the brief) — the name already carries identity as the
   bubble's first line, and a real circular avatar column reopened the
   exact flex-sizing trap this file just fixed once (see `.message__time`
   above); not worth resurrecting for a 24px circle. Base `.message__avatar`
   is already `display: none` by default (same dead `.messages--group`
   gate), this just makes the choice explicit instead of accidental. */
.message--roompost .message__avatar {
  display: none;
}

/* ── Composer: topic mode switch ("У тему" / "Чернетка клієнту") ──
   Same shape as `.email-meta` in body_shell.html — a row above the
   input, shown only for `topic-*` dialogs (transport.js). */

#topic-meta:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-subtle);
}

.topic-mode {
  display: inline-flex;
  gap: 0.125rem;
  padding: 0.125rem;
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-hover);
  align-self: flex-start;
}

.topic-mode__item {
  padding: 0.25rem 0.625rem;
  border-radius: var(--ds-radius-xs);
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  background: none;
  border: none;
  cursor: pointer;
}

.topic-mode__item[aria-pressed="true"] {
  background: var(--ds-surface-raised);
  color: var(--ds-text-primary);
  font-weight: 500;
}

.topic-mode__routes:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  align-items: center;
}

.topic-mode__routes-label {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
}

.topic-mode__route {
  padding: 0.1875rem 0.5rem;
  border-radius: var(--ds-radius-xs);
  border: 0.0625rem solid var(--ds-border-default);
  background: none;
  color: var(--ds-text-secondary);
  font-size: 0.75rem;
  cursor: pointer;
}

.topic-mode__route[aria-pressed="true"] {
  border-color: var(--ds-accent-border);
  background: var(--ds-accent-subtle);
  color: var(--ds-accent);
}

/* ── Панель кімнати, 23.09.2026 ──────────────────────────────────
   Було (знімок 23.09): кожен розділ у своїй коробці з заголовком
   усередині, кнопки чотирьох різних розмірів («Копіювати» більша за
   поле, «Прибрати» квадратом на кожному рядку, «Додати» в іншому
   шрифті). Тепер згрупований список, як у налаштуваннях телефона:
   тиха мітка розділу над карткою, у картці рядки однієї висоти, дії
   однієї ваги. Руйнівна дія («Прибрати») тиха і червоніє лише під
   курсором. */
.room-view { display: flex; flex-direction: column; gap: 1.5rem; padding: 1rem 1rem 2rem; }
.room-view > * { width: 100%; max-width: 38rem; }
@media (width > 48em) {
  .room-view { align-items: center; padding: 1.5rem 2rem 3rem; }
}

/* Головна дія кімнати. */
.room-call { display: flex; flex-direction: column; gap: 0.5rem; }
.room-call__start {
  width: 100%; min-height: 3rem;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  border-radius: var(--ds-radius-lg);
  font-size: var(--ds-heading-size); font-weight: var(--ds-weight-strong);
  transition: transform var(--ds-motion-fast) var(--ds-ease-out), background-color var(--ds-motion) var(--ds-ease-out);
}
.room-call__start:active { transform: scale(0.98); }
.room-call__start svg { width: 1.125rem; height: 1.125rem; }
.room-call__hint { margin: 0; font-size: var(--ds-meta-size); color: var(--ds-text-muted); text-align: center; }

/* Розділ: мітка над карткою, сама коробка розділу прозора. */
.room-block { padding: 0; background: none; border: 0; border-radius: 0; }
.room-block__head {
  display: flex; align-items: center; gap: 0.5rem;
  min-height: 2rem; margin: 0 0 0.375rem; padding: 0 0.25rem;
}
.room-block__title {
  margin: 0;
  font-size: var(--ds-label-size); font-weight: var(--ds-weight-strong);
  color: var(--ds-text-muted);
}
.room-block__count {
  font-size: var(--ds-meta-size); color: var(--ds-text-subtle);
  font-variant-numeric: tabular-nums;
}
.room-block__action { margin-left: auto; }
.room-block__note {
  margin: 0.5rem 0 0; padding: 0 0.25rem;
  font-size: var(--ds-meta-size); color: var(--ds-text-muted); line-height: 1.5;
}

/* Спільна картка для вмісту розділу. */
.room-invite, .room-people, .room-block .room-topics, .room-empty, .room-invite__make {
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
}

/* Запрошення: посилання й «Копіювати» — одне поле, кнопка всередині. */
.room-invite {
  display: flex; align-items: center; gap: 0.5rem;
  min-height: 3rem; padding: 0.375rem 0.375rem 0.375rem 0.875rem;
}
.room-invite__url {
  flex: 1; min-width: 0;
  padding: 0; min-height: 0; background: none; border: 0;
  font-family: var(--ds-font); font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.room-invite__copy {
  flex-shrink: 0; min-height: 2.25rem; padding: 0 0.875rem;
  border: 0; border-radius: var(--ds-radius-md);
  background: var(--ds-surface-hover); color: var(--ds-text-primary);
  font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
  transition: background-color var(--ds-motion) var(--ds-ease-out);
}
.room-invite__copy:hover { background: var(--ds-surface-active); }
.room-invite__make {
  width: 100%; min-height: 3rem; color: var(--ds-text-primary);
  font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
}
.room-invite__make:hover { background: var(--ds-surface-hover); }
.room-textbtn {
  padding: 0; border: 0; background: none; font: inherit; cursor: pointer;
  color: var(--ds-text-secondary); text-decoration: underline; text-underline-offset: 0.2em;
  transition: color var(--ds-motion) var(--ds-ease-out);
}
.room-textbtn:hover { color: var(--ds-feedback-danger); }

/* Учасники: рядки однієї висоти, круглі аватари, як у плитках созвону. */
.room-people { list-style: none; margin: 0; padding: 0 0.875rem; }
.room-person { display: flex; align-items: center; gap: 0.75rem; min-height: 3.25rem; }
.room-person + .room-person { border-top: 0.0625rem solid var(--ds-border-subtle); }
.room-person .room-topic__avatar {
  width: 2.25rem; height: 2.25rem; min-width: 2.25rem;
  border-radius: 50%; font-size: var(--ds-eyebrow-size); letter-spacing: 0.02em;
}
.room-person__name {
  flex: 1; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--ds-body-size); color: var(--ds-text-primary);
}
.room-person__role {
  padding: 0.125rem 0.5rem; border-radius: var(--ds-radius-full);
  background: var(--ds-surface-hover);
  font-size: var(--ds-eyebrow-size); color: var(--ds-text-muted);
}
/* Позначка «у созвоні» живе в розмітці, видно її лише з data-live —
   transport.js перемикає атрибут, а не пише текст. */
.room-person__live { display: none; align-items: center; gap: 0.375rem; font-size: var(--ds-meta-size); color: var(--ds-accent); }
.room-person__live::before {
  content: ""; width: 0.375rem; height: 0.375rem; border-radius: 50%; background: currentColor;
}
.room-person[data-live] .room-person__live { display: inline-flex; }
.room-person__remove.btn {
  min-height: 2rem; padding: 0 0.625rem;
  border: 0; border-radius: var(--ds-radius-sm); background: none;
  font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
  color: var(--ds-text-muted);
  transition: color var(--ds-motion) var(--ds-ease-out), background-color var(--ds-motion) var(--ds-ease-out), opacity var(--ds-motion) var(--ds-ease-out);
}
.room-person__remove.btn:hover, .room-person__remove.btn:focus-visible {
  color: var(--ds-feedback-danger);
  background: color-mix(in srgb, var(--ds-feedback-danger) 12%, transparent);
}
/* Мишкою «Прибрати» з'являється на рядку під курсором: три червоні
   кнопки поспіль читались як головна дія списку. */
@media (hover: hover) and (pointer: fine) {
  .room-person__remove.btn { opacity: 0; }
  .room-person:hover .room-person__remove.btn,
  .room-person:focus-within .room-person__remove.btn { opacity: 1; }
}

/* Додати з контактів: поле й кнопка однієї висоти й одного шрифту. */
.room-add { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.room-add__select {
  flex: 1; min-width: 0; min-height: 2.75rem;
  padding: 0 2.25rem 0 0.875rem;
  appearance: none; -webkit-appearance: none;
  background-color: var(--ds-surface-raised);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ds-text-muted) 50%),
    linear-gradient(135deg, var(--ds-text-muted) 50%, transparent 50%);
  background-position: calc(100% - 1.125rem) 50%, calc(100% - 0.8125rem) 50%;
  background-size: 0.3125rem 0.3125rem;
  background-repeat: no-repeat;
  color: var(--ds-text-secondary);
  border: 0.0625rem solid var(--ds-border-subtle); border-radius: var(--ds-radius-lg);
  font: inherit; font-size: var(--ds-label-size);
  cursor: pointer;
}
.room-add__select:focus-visible { outline: 0.125rem solid var(--ds-accent-border); outline-offset: 0.0625rem; }
.room-add .btn {
  min-height: 2.75rem; padding: 0 1rem;
  border: 0; border-radius: var(--ds-radius-lg);
  background: var(--ds-surface-hover); color: var(--ds-text-primary);
  font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
}
.room-add .btn:hover { background: var(--ds-surface-active); }

/* Теми. */
.room-block .room-topics { margin: 0; padding: 0 0.875rem; }
.room-block .room-topic { padding-left: 0; padding-right: 0; border-radius: 0; }
.room-block .room-topic-add { margin-bottom: 0.5rem; }
.room-block .room-tagbar { margin-bottom: 0.5rem; }
.room-block__action.btn {
  min-height: 2rem; padding: 0 0.625rem;
  border: 0; border-radius: var(--ds-radius-sm); background: none;
  font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
  color: var(--ds-accent);
}
.room-block__action.btn:hover { background: var(--ds-accent-subtle); }
.room-empty {
  margin: 0; padding: 1rem 0.875rem;
  background: none; border-style: dashed; border-color: var(--ds-border-default);
  font-size: var(--ds-label-size); color: var(--ds-text-muted); line-height: 1.5;
}

@media (pointer: coarse) {
  .room-block__action.btn, .room-person__remove.btn { min-height: 2.75rem; }
}
@media (prefers-reduced-motion: reduce) {
  .room-call__start, .room-invite__copy, .room-person__remove.btn, .room-textbtn { transition: none; }
  .room-call__start:active { transform: none; }
}


/* Записи зустрічей. Рядок: зліва час, під ним «тривалість · розмір»;
   справа одна дія (завантажити або зупинити). Під рядком плеєр на всю
   ширину картки. Інформація стискається, кнопка — ні: на вузькому
   телефоні рядок не розлазиться за картку (знімок 24.09.2026). */
.room-recs__list {
  list-style: none; margin: 0; padding: 0 0.875rem;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
}
.room-rec { padding: 0.75rem 0; }
.room-rec + .room-rec { border-top: 0.0625rem solid var(--ds-border-subtle); }
.room-rec__row { display: flex; align-items: center; gap: 0.75rem; }
.room-rec__row + .room-rec__audio { margin-top: 0.625rem; }
.room-rec__info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.125rem; }
.room-rec__when {
  font-size: var(--ds-body-size); font-weight: 500; color: var(--ds-text-primary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.room-rec__meta {
  font-size: var(--ds-meta-size); color: var(--ds-text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.room-rec__live { color: var(--ds-feedback-danger); }
.room-rec__live::before {
  content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; margin-right: 0.375rem;
  border-radius: 50%; background: currentColor; vertical-align: 0.05em;
  animation: room-rec-pulse 1.4s ease-in-out infinite;
}
@keyframes room-rec-pulse { 50% { opacity: 0.35; } }
.room-rec__btn {
  flex: none; display: inline-flex; align-items: center; gap: 0.375rem;
  min-height: 2.25rem; padding: 0 0.75rem;
  font: inherit; font-size: var(--ds-label-size); font-weight: var(--ds-label-weight);
  color: var(--ds-text-primary); text-decoration: none; white-space: nowrap;
  background: transparent; cursor: pointer;
  border: 0.0625rem solid var(--ds-border-subtle); border-radius: 999px;
  transition: background-color 150ms ease, border-color 150ms ease;
}
.room-rec__btn svg { width: 1rem; height: 1rem; }
.room-rec__btn:hover { background: var(--ds-surface-hover, rgba(127,127,127,.12)); }
.room-rec__btn:focus-visible { outline: 0.125rem solid var(--ds-accent); outline-offset: 0.125rem; }
.room-rec__btn--stop { color: var(--ds-feedback-danger); border-color: currentColor; }
.room-rec__btn--del { color: var(--ds-text-muted); }
.room-rec__btn--del:hover { color: var(--ds-feedback-danger); }
.room-rec__btn[disabled] { opacity: 0.5; cursor: default; }
/* На вузькому екрані лишається тільки значок: слово не влазить поруч
   із часом. Підпис для екранного читача — в aria-label. */
@media (max-width: 26rem) {
  .room-rec__btn:not(.room-rec__btn--stop) { width: 2.5rem; padding: 0; justify-content: center; }
  .room-rec__btn:not(.room-rec__btn--stop) span { display: none; }
}
@media (pointer: coarse) { .room-rec__btn { min-height: 2.75rem; } .room-rec__btn:not(.room-rec__btn--stop) { min-width: 2.75rem; } }
@media (prefers-reduced-motion: reduce) { .room-rec__live::before { animation: none; } }
/* Два класи: загальне `.messages audio { height: auto }` (панель живе
   всередині #messages) інакше перемагає і плеєр схлопується в нуль. */
.room-rec .room-rec__audio { display: block; width: 100%; height: 2.5rem; min-height: 2.5rem; }

}

/* На панелі кімнати созвон уже веде велика кнопка «Почати созвон» —
   трубка в шапці була другою кнопкою тієї самої дії (знімок 23.09.2026).
   У темах кімнати великої кнопки немає, там трубка лишається. */
body:has(#messages .room-call) #room-call-btn { display: none; }

/* ── 74-invites.css ── */
/* ЗАПРОШЕННЯ — картка з посиланням у налаштуваннях + окрема сторінка
   /i/<token>, яку бачить запрошений одразу після переходу.
   Розмітку тримають handlers::components::settings_view::render_invites
   і handlers::components::invites::{render_success_page,render_used_page}.
   Токени лише з DESIGN_SYSTEM.md — див. 60-settings.css для сусідніх правил. */
@layer components {

.invite-link-card {
  align-items: stretch;
}

.invite-link-card__url {
  display: block;
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-sunken);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-body-size);
  color: var(--ds-text-primary);
  word-break: break-all;
}

/* /i/<token> — самостійна сторінка (не шухляда налаштувань), тому власний
   центрований контейнер замість .settings-row/.settings-group. */
.invite-accept {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--ds-space-6) var(--ds-space-4);
  background: var(--ds-surface-base);
}

.invite-accept__card {
  width: 100%;
  max-width: 25rem;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-3);
  padding: var(--ds-space-6);
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-md);
}

.invite-accept__title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: var(--ds-weight-medium);
  color: var(--ds-text-primary);
}

.invite-accept__lead {
  margin: 0;
  color: var(--ds-text-secondary);
  font-size: var(--ds-body-size);
}

.invite-accept__row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-sunken);
  border-radius: var(--ds-radius-sm);
}

.invite-accept__label {
  flex-shrink: 0;
  color: var(--ds-text-muted);
  font-size: var(--ds-label-size);
}

.invite-accept__value {
  flex: 1;
  min-width: 0;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-body-size);
  color: var(--ds-text-primary);
  word-break: break-all;
}

.invite-accept__hint {
  margin: 0;
  color: var(--ds-text-muted);
  font-size: var(--ds-label-size);
}

.invite-accept__open {
  margin-top: var(--ds-space-2);
  text-align: center;
}

}

/* ── 76-apps.css ── */
/* APPS — in-chat mini-apps (Ф4, 06.09.2026: «плейлист інтегрувати як
   застосунок усередині бесіди» — Артем). `app-<slug>` dialogs open a
   persistent `<iframe class="app-frame">` (templates/body_shell.html) over
   `#messages` instead of a message thread — `.layout.app-open` (set by
   `updateAppFrame()`, static/transport.js) hides the composer and the
   message list underneath while the frame is shown, without ever unmounting
   it (music keeps playing while another dialog is open).
   Tokens only — no hex/px magic. Owner: handlers/components/apps.rs,
   dialogs.rs::build_dialogs, templates/body_shell.html. */

.app-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--ds-surface-base);
}

.layout.app-open .input-area,
.layout.app-open #message-form {
  display: none;
}

.layout.app-open #messages {
  visibility: hidden;
}

/* ── 80-media-preview.css ── */
/* МЕДІА І ПЕРЕГЛЯД — роздільники дат, кнопка «вниз», пошук у чаті, панель перегляду, вивантаження
 *
 * Усе, що показує вміст повідомлення поза баблом: прев'ю файлу,
 * прогрес вивантаження, вбудовані картинки.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer components {
/* ═══════════════════════════════════════════
   DATE SEPARATORS
   ═══════════════════════════════════════════ */

.date-separator {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: var(--ds-space-3) 0 var(--ds-space-1);
  align-self: center;
}

.date-separator::before,
.date-separator::after {
  display: none;
}

.date-separator__text {
  /* Маленька пілюля по центру (Telegram-метрика, Артем 05.09.2026) —
     раніше голий текст без фону, губився в стрічці баблів. */
  font-family: var(--font-mono);
  font-size: var(--font-size-eyebrow);
  color: var(--ds-text-muted);
  background: var(--ds-surface-overlay);
  padding: 0.1875rem 0.625rem;
  border-radius: var(--ds-radius-full);
  white-space: nowrap;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  font-weight: 500;
  opacity: 0.9;
}

/* ── "You stopped here" marker ──────────────
   Shown when a thread reopens at the message the reader left off on, so the
   jump reads as deliberate instead of as a random scroll position. Same
   eyebrow type as the date separator; a rule on both sides is what tells the
   two apart at a glance. */
.resume-divider {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  margin: var(--ds-space-3) 0 var(--ds-space-1);
  font-family: var(--font-mono);
  font-size: var(--font-size-eyebrow);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--ds-text-muted);
  white-space: nowrap;
  opacity: 0.75;
}

.resume-divider::before,
.resume-divider::after {
  content: "";
  flex: 1;
  height: 0.0625rem;
  background: currentColor;
  opacity: 0.35;
}

/* ═══════════════════════════════════════════
   SCROLL-TO-BOTTOM FAB
   ═══════════════════════════════════════════ */

.scroll-bottom-btn {
  position: absolute;
  bottom: var(--ds-space-4);
  right: var(--ds-space-4);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  background: var(--ds-surface-active);
  border: 0.0625rem solid var(--ds-border-strong);
  color: var(--ds-text-secondary);
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 10;
  box-shadow: 0 0.125rem 0.5rem color-mix(in srgb, var(--p-neutral-1000) 25%, transparent);
  transition: opacity var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out),
              background var(--duration-fast);
}

.scroll-bottom-btn:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
  transform: scale(1.08);
}

.scroll-bottom-btn.visible {
  display: flex;
}

.scroll-bottom-btn__badge {
  position: absolute;
  top: -0.25rem;
  right: -0.25rem;
  min-width: 1.125rem;
  height: 1.125rem;
  border-radius: var(--radius-full);
  background: var(--ds-accent);
  color: var(--ds-on-accent);
  font-size: 0.6875rem;
  font-weight: 600;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0 0.25rem;
  line-height: 1;
}

.scroll-bottom-btn__badge.has-count {
  display: flex;
}

/* Link preview card */
.message__link-preview {
  display: flex;
  gap: var(--ds-space-2);
  background: var(--ds-surface-hover);
  margin-top: var(--ds-space-1);
  overflow: hidden;
  max-width: 20rem;
}
.message__link-preview img {
  width: 4rem;
  height: 4rem;
  object-fit: cover;
  flex-shrink: 0;
}
.link-preview__text {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: var(--ds-space-1) var(--ds-space-2);
  font-size: var(--ds-meta-size);
  min-width: 0;
}
.link-preview__text strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ds-text-primary);
}
.link-preview__text span {
  color: var(--ds-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.link-preview__domain {
  color: var(--ds-text-muted);
  font-size: var(--ds-eyebrow-size);
}

/* Voice input button recording state */
#voice-input-btn.recording {
  color: var(--ds-accent);
  animation: pulse 1s ease-in-out infinite;
}

/* Reactions */
.message__reactions {
  display: flex;
  gap: 0.25rem;
  margin-top: var(--ds-space-1);
  flex-wrap: wrap;
}
.reaction {
  font-size: var(--ds-label-size);
  padding: 0.125rem 0.25rem;
  background: var(--ds-surface-hover);
  cursor: pointer;
}
.react-picker {
  display: flex;
  gap: 0.125rem;
  padding: 0.25rem;
  background: var(--ds-surface-active);
  box-shadow: var(--shadow-md);
  position: absolute;
  bottom: 100%;
  z-index: 10;
}
.react-picker button {
  background: none;
  border: none;
  font-size: 1.25rem;
  cursor: pointer;
  padding: 0.25rem;
  transition: transform var(--duration-fast) var(--ease-spring);
}
.react-picker button:hover {
  transform: scale(1.3);
}

/* Reply quote — 2-line clamped snippet of the message being answered.
   Was single-line nowrap which truncated aggressively; the 2-line clamp
   shows enough context without ballooning the bubble. Amber-tint +
   accent border read as "this is a quote", matching Telegram/Signal. */
.message__quote {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;

  padding: 0.25rem 0.5rem;
  margin-bottom: var(--ds-space-1);
  /* ШІ-патерн №1 (кольорова кромка) прибрано 06.09.2026: цитата = тональний крок, без рейла */
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-overlay);
  font-size: var(--ds-meta-size);
  line-height: 1.35;
  color: var(--ds-text-primary);
  max-width: 100%;

  cursor: pointer;
  transition: background 0.15s ease;
}
.message__quote:hover {
  background: var(--ds-accent-border);
}
/* Outgoing bubble already tinted accent-light; contrast the quote a bit
   more so it doesn't blend with its own background. */
.message--outgoing .message__quote {
  background: color-mix(in srgb, var(--p-neutral-0) 6%, transparent);
  border-left-color: var(--ds-accent);
}
.message--outgoing .message__quote:hover {
  background: color-mix(in srgb, var(--p-neutral-0) 10%, transparent);
}

/* ═══════════════════════════════════════════
   MESSAGE SEARCH
   ═══════════════════════════════════════════ */

.msg-search-bar {
  display: none;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-3);
  background: var(--ds-surface-raised);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.msg-search-bar.active {
  display: flex;
}

.msg-search-bar input {
  flex: 1;
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  color: var(--ds-text-primary);
  padding: var(--ds-space-1) var(--ds-space-2);
  font-size: var(--ds-body-size);
  font-family: var(--ds-font);
  outline: none;
}

.msg-search-bar input:focus {
  border-color: var(--ds-accent);
}

.msg-search-bar__count {
  font-size: var(--ds-label-size);
  color: var(--ds-text-muted);
  white-space: nowrap;
  min-width: 3.125rem;
  text-align: center;
}

.msg-search-bar button {
  background: transparent;
  border: none;
  color: var(--ds-text-secondary);
  cursor: pointer;
  padding: var(--ds-space-1);
  border-radius: var(--ds-radius-xs);
  display: flex;
  align-items: center;
}

.msg-search-bar button:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
}

.message--search-highlight .message__bubble {
  outline: 0.125rem solid var(--ds-accent);
  outline-offset: 0.0625rem;
}

.message__text mark {
  background: color-mix(in srgb, var(--ds-feedback-warning) 30%, transparent);
  color: inherit;
  border-radius: 0.125rem;
  padding: 0 0.0625rem;
}

/* ── File Preview Panel ── */
/* ═══════════════════════════════════════════
   PREVIEW PANEL — mobile-first
   ═══════════════════════════════════════════ */
#preview-panel {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  background: var(--ds-surface-base);
  border-top: 0.0625rem solid var(--ds-surface-overlay);
  z-index: 100;
  transition: height 0.3s var(--ease-out);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* Mobile: full height */
#preview-panel.open {
  height: 100vh;
  height: 100dvh;
}
/* Desktop: half height */
@media (min-width: 48.0625em) {
  #preview-panel.open {
    height: 50vh;
  }
}

.preview-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--ds-space-2) var(--ds-space-4);
  border-bottom: 0.0625rem solid var(--ds-surface-overlay);
  flex-shrink: 0;
}
.preview-title {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-label-size);
  color: var(--ds-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}
.preview-actions {
  display: flex;
  gap: var(--ds-space-3);
  align-items: center;
  flex-shrink: 0;
}
.preview-download {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  text-decoration: none;
}
.preview-download:hover { color: var(--ds-text-primary); }
.preview-close {
  background: none;
  border: none;
  color: var(--ds-text-secondary);
  cursor: pointer;
  font-size: var(--t3);
  padding: var(--ds-space-1);
}
.preview-close:hover { color: var(--ds-text-primary); }
.preview-body {
  flex: 1;
  overflow: auto;
  padding: var(--ds-space-3);
  -webkit-overflow-scrolling: touch;
}
@media (min-width: 48.0625em) {
  .preview-body { padding: var(--ds-space-4); }
}
.preview-body table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-meta-size);
}
@media (min-width: 48.0625em) {
  .preview-body table { font-size: var(--ds-label-size); }
}
.preview-body table td {
  padding: var(--ds-space-1) var(--ds-space-2);
  border: 0.0625rem solid var(--ds-surface-overlay);
  white-space: nowrap;
}
.preview-body pre {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-label-size);
  white-space: pre-wrap;
  word-break: break-word;
}
.preview-body img {
  max-width: 100%;
  max-height: 80vh;
}
@media (min-width: 48.0625em) {
  .preview-body img { max-height: 40vh; }
}
/* Шапка: хрестик ліворуч (великий палець), назва тягнеться, дії праворуч.
   Порядок задано flex-order, щоб розмітка лишалась читабельною. */
/* Назад — перша й найбільша ціль у шапці: екран файлу відкривається на
   весь дисплей, і вихід з нього має знаходитись без прицілювання. */
.preview-back {
  order: -1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  font-size: var(--t3);
  background: none;
  border: none;
  color: var(--ds-text-primary);
  cursor: pointer;
}
.preview-back:active { color: var(--ds-accent); }
.preview-size {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
  flex-shrink: 0;
}
/* Дії — самі іконки (підпис лишається в aria-label/title): три слова в
   рядок на телефоні не влазять і однаково не читаються. */
.preview-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  font-size: var(--t3);
  background: none;
  border: none;
  color: var(--ds-text-secondary);
  text-decoration: none;
  cursor: pointer;
}
.preview-action:hover, .preview-action:active { color: var(--ds-accent); }

/* Тіло: одна картинка / плеєр / кадр по центру, з полями під шапку. */
.preview-media {
  display: block;
  max-width: 100%;
  max-height: 100%;
  margin: 0 auto;
}
.preview-audio { width: 100%; }
.preview-frame {
  width: 100%;
  height: 100%;
  min-height: 60vh;
  border: 0;
  background: var(--ds-surface-raised);
}
.preview-text {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-meta-size);
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--ds-text-secondary);
}
/* Розбір документа з common::doc_preview: таблиця xlsx, текст docx.
   Широку таблицю прокручуємо всередині, а не ламаємо сторінку
   (Frain: контейнер зі своїм overflow-x). */
.preview-doc { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.preview-doc table { width: max-content; min-width: 100%; }

.preview-empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--ds-space-2);
  min-height: 40vh;
  color: var(--ds-text-muted);
  font-size: var(--ds-label-size);
  text-align: center;
}
.preview-empty__icon { font-size: var(--t1); color: var(--ds-text-subtle); }
.preview-empty__name {
  font-family: var(--font-mono, monospace);
  color: var(--ds-text-secondary);
  word-break: break-all;
  max-width: 20rem;
}
.preview-empty__note { font-size: var(--ds-meta-size); }

/* Книга контактів: рядок веде в поштовий діалог, тож підпис дії видно. */
.contact-write {
  font-size: var(--ds-eyebrow-size);
  font-family: var(--font-mono, monospace);
  color: var(--ds-accent);
  text-transform: lowercase;
}
.settings-row--source[hx-get] { cursor: pointer; }

/* Джерела в налаштуваннях: назва, стан крапкою, дрібний підпис знизу. */
.settings-row--source { flex-wrap: wrap; }
.source-name { font-weight: 500; }
.source-status {
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
}
/* Крапка-індикатор прибрана: стан пишеться словом поруч, і кольоровий
   кружечок біля нього — заборонений ШІ-патерн. Різницю тримає сам
   напис: увімкнене — звичайним кольором, вимкнене — тьмяним. */
.source-status--on { color: var(--ds-text-secondary); }
.settings-row__hint {
  flex-basis: 100%;
  margin-top: var(--ds-space-1);
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  word-break: break-all;
}

/* Чіп-документ у баблі — кнопка, тож гасимо типові стилі кнопки. */
button.message__file {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  padding: var(--ds-space-1) 0;
  text-align: left;
}

.preview-loading, .preview-error, .preview-unsupported {
  color: var(--ds-text-muted);
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-label-size);
  padding: var(--ds-space-4);
}

/* ═══════════════════════════════════════════
   UPLOAD PROGRESS BAR
   ═══════════════════════════════════════════ */
.upload-bar {
  position: relative;
  height: 1.5rem;
  background: var(--ds-surface-overlay);
  overflow: hidden;
  flex-shrink: 0;
}
.upload-bar__fill {
  position: absolute;
  inset: 0;
  width: 0;
  background: var(--ds-accent);
  opacity: 0.15;
  transition: width 0.15s linear;
}
.upload-bar__text {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-family: var(--font-mono, monospace);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  letter-spacing: 0.05em;
}
.upload-bar--done .upload-bar__fill {
  width: 100%;
  background: var(--ds-feedback-success);
  opacity: 0.2;
}
.upload-bar--done .upload-bar__text { color: var(--ds-feedback-success); }
.upload-bar--done {
  animation: uploadDone 0.4s var(--ease-out) forwards;
}
@keyframes uploadDone {
  to { opacity: 0; height: 0; }
}
.upload-bar--error .upload-bar__fill {
  width: 100%;
  background: var(--ds-feedback-danger);
  opacity: 0.15;
}
.upload-bar--error .upload-bar__text { color: var(--ds-feedback-danger); }

/* Settings row + button icons — Feather SVGs inline with stroke=currentColor.
   .settings-row__icon переїхав у 60-settings.css Ф2a (там і решта
   .settings-row__* — той самий компонент, змерджений з тайл-версією,
   що жила в @layer overrides). */
.settings-row__label { display: inline-flex; align-items: center; gap: var(--ds-space-2); }

.btn__icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  stroke-width: 2;
}
.btn--icon .btn__icon { width: 1.125rem; height: 1.125rem; }

/* ═══════════════════════════════════════════
   INLINE MEDIA BUBBLES (image/file attachments)
   Telegram/WhatsApp pattern — blurred thumb now,
   full bitmap once chunks arrive.
   ═══════════════════════════════════════════ */
.message--media .message__bubble--media {
  padding: 0;
  overflow: hidden;
  background: transparent;
}
.message__image {
  display: block;
  max-width: min(18rem, 70vw);
  max-height: 24rem;
  width: auto;
  height: auto;
  border-radius: var(--ds-radius-md);
  object-fit: cover;
  transition: filter 0.3s ease, transform 0.3s ease;
  cursor: zoom-in;
}
.message__image:hover { filter: brightness(1.03); }

.message--file .message__bubble--file {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-width: 10rem;
}
.message__file-icon {
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  color: var(--ds-text-subtle);
}
.message__file-info { flex: 1; min-width: 0; }
.message__file-name {
  font-size: var(--ds-label-size);
  font-weight: 500;
  color: var(--ds-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.message__file-size {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
  font-family: var(--font-mono, monospace);
}
.message__file-link {
  width: 1.75rem;
  height: 1.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ds-accent);
  font-weight: 700;
  text-decoration: none;
  border-radius: 50%;
  border: 0.0625rem solid var(--ds-border-subtle);
  transition: background 0.15s ease;
}
.message__file-link:hover { background: var(--ds-surface-hover); }

/* ── Лайтбокс і відео в бабблі ── Ф2a, з @layer overrides. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--p-neutral-1000) 88%, transparent);
  cursor: zoom-out;
  animation: dim-menu-in 120ms var(--ease-out) both;
}
.lightbox img {
  max-width: 96vw;
  max-height: 94vh;
  border-radius: var(--ds-radius-sm);
}
.message video {
  max-width: min(20rem, 70vw);
  border-radius: var(--ds-radius-sm);
  display: block;
}
.message img { cursor: zoom-in; }
}

/* ── Шторка перегляду фото ───────────────────────────────────────
   Замінює `.lightbox` (порожній оверлей без підпису й дій). Геометрія й
   жест — з формана (`foreman/static/css/layout.css` .fm-sheet-*), токени
   наші. На телефоні закривають ручка і свайп, на десктопі — Esc і клік
   по тлу; окрема кнопка-хрестик не потрібна, бо тягнути мишею нічого. */
.photo-sheet {
  position: fixed;
  inset: 0;
  z-index: 400;
  pointer-events: none;
}
.photo-sheet[data-open] { pointer-events: auto; }

.photo-sheet__back {
  position: absolute;
  inset: 0;
  /* Прозорий старт того ж кольору, у який затемнюємось — інакше перехід
     ішов би від «нічого» і в перший кадр блимав би. */
  background: color-mix(in srgb, var(--p-neutral-1000) 0%, transparent);
  transition: background 0.25s var(--ease-out);
}
.photo-sheet[data-open] .photo-sheet__back {
  background: color-mix(in srgb, var(--p-neutral-1000) 78%, transparent);
}

.photo-sheet__panel {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  max-height: 92dvh;
  display: flex;
  flex-direction: column;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-subtle);
  border-radius: 1rem 1rem 0 0;
  transform: translateY(100%);
  transition: transform 0.3s var(--ease-out);
  padding-bottom: env(safe-area-inset-bottom);
}
.photo-sheet[data-open] .photo-sheet__panel { transform: translateY(0); }
/* Поки палець веде панель, перехід вимкнено: інакше кожен кадр жесту
   ставав би анімацією і панель відставала б від пальця. */
.photo-sheet__panel[data-drag] { transition: none; }

.photo-sheet__grip {
  flex-shrink: 0;
  height: 1.5rem;
  display: grid;
  place-items: center;
  cursor: grab;
  touch-action: none;
}
.photo-sheet__grip::before {
  content: "";
  width: 2.25rem;
  height: 0.25rem;
  border-radius: var(--ds-radius-full);
  background: var(--ds-border-strong);
}
.photo-sheet__grip:active { cursor: grabbing; }

.photo-sheet__stage {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: 0 var(--ds-space-3);
  overflow: hidden;
}
.photo-sheet__img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--ds-radius-sm);
}
/* Висота на час завантаження: інакше панель схлопується в смужку і
   відкриття читається як поломка. Перше відкриття тягне фото з Telegram —
   це секунди. */
.photo-sheet__stage[data-loading] { min-height: 40dvh; }
.photo-sheet__stage[data-loading] .photo-sheet__img { display: none; }
.photo-sheet__error {
  color: var(--ds-text-subtle);
  font-size: var(--ds-meta-size);
  padding: var(--ds-space-6);
}

/* Нижній рядок: ліворуч хто й коли (плюс підпис), праворуч дії. Hairline
   зверху — той самий роздільник, що між рядками списку. */
.photo-sheet__bar {
  flex-shrink: 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ds-space-3);
  padding: var(--ds-space-3);
  border-top: 0.0625rem solid var(--ds-border-subtle);
}
.photo-sheet__meta { min-width: 0; }
.photo-sheet__who {
  font-size: var(--ds-meta-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ds-text-subtle);
  font-variant-numeric: tabular-nums;
}
.photo-sheet__caption {
  margin-top: var(--ds-space-1);
  color: var(--ds-text-primary);
  /* Довгий підпис не має виштовхувати картинку: три рядки, далі багатокрапка. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.photo-sheet__actions {
  display: flex;
  gap: var(--ds-space-2);
  flex-shrink: 0;
}
.photo-sheet__btn {
  /* Apple HIG: 44pt. Дії тут головні, на них ціляться пальцем. */
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border: 0.0625rem solid var(--ds-border-default);
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-base);
  color: var(--ds-text-muted);
  cursor: pointer;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.photo-sheet__btn:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-hover);
  border-color: var(--ds-border-strong);
}
.photo-sheet__btn svg { width: 1.25rem; height: 1.25rem; }

/* На широкому екрані шторка стає діалогом по центру: тягнути мишею
   нічого, а панель на всю ширину монітора виглядала б як помилка. */
@media (width > 48em) {
  .photo-sheet { display: grid; place-items: center; }
  .photo-sheet__panel {
    position: relative;
    inset: auto;
    width: min(56rem, 92vw);
    max-height: 88dvh;
    border: 0.0625rem solid var(--ds-border-subtle);
    border-radius: var(--ds-radius-md);
    transform: translateY(1.5rem);
    opacity: 0;
    transition: transform 0.25s var(--ease-out), opacity 0.25s var(--ease-out);
  }
  .photo-sheet[data-open] .photo-sheet__panel { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .photo-sheet__panel { transition: none; }
}

/* ── 85-empty-loading.css ── */
/* ПОРОЖНЄ І ЗАВАНТАЖЕННЯ — переклад, empty states, скелетони, десктопна сітка
 *
 * Стани «ще нічого немає» і «ще вантажиться». Живуть у шарі utilities
 * історично: вони перекривають компоненти, а не описують їх.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer utilities {
/* ═══════════════════════════════════════════
   TRANSLATION
   ═══════════════════════════════════════════ */

.translate-settings {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
}

.translate-toggle {
  display: flex;
  align-items: center;
  cursor: pointer;
}

.translate-toggle input {
  display: none;
}

.translate-toggle__label {
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ds-meta-size);
  font-weight: 600;
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-secondary);
  transition: all 0.15s ease;
}

.translate-toggle__label:hover {
  color: var(--ds-text-primary);
}

.translate-toggle input:checked + .translate-toggle__label {
  background: var(--ds-accent);
  /* Роль «текст на плашці акценту» = --ds-on-accent. Було фіксовано
     #fff в обох темах; у light це той самий #ffffff, у dark
     --ds-on-accent = #06140d (майже чорний, підібраний для контрасту
     саме на --ds-accent), а не білий. Задумана зміна для dark теми. */
  color: var(--ds-on-accent);
}

.translate-langs {
  display: flex;
  align-items: center;
}

.lang-select {
  appearance: none;
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-primary);
  font-family: var(--ds-font);
  font-size: var(--ds-meta-size);
  font-weight: 500;
  padding: var(--ds-space-2);
  cursor: pointer;
  outline: none;
}

.lang-select:hover {
  background: var(--ds-surface-hover);
}

.peer-lang {
  font-size: var(--ds-meta-size);
  font-weight: 500;
  color: var(--ds-text-secondary);
  padding: var(--ds-space-2);
}

/* ═══════════════════════════════════════════
   EMPTY STATES
   ═══════════════════════════════════════════ */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-4);
  height: 100%;
  color: var(--ds-text-muted);
  animation: fadeIn 0.4s ease-out;
  user-select: none;
}

/* Порожній стан заповнено (ref: aino.agency ASCII + вагова вісь): голе
   «Виберіть чат» у чорній порожнечі читалось дешево. Маленький
   box-drawing чат-бабл (моно, наш термінальний натяк) + легкий текст.
   Ф2a, з @layer overrides. */
.empty-state::before {
  content: "\256D\2500\2500\2500\2500\2500\256E\A\2502  \00B7\00B7\00B7  \2502\A\2570\2500\2500\2500\252C\2500\2500\2500\256F\A        \2570";
  white-space: pre;
  font-family: var(--ds-font-mono);
  font-size: 1rem;
  line-height: 1.15;
  letter-spacing: 0.05em;
  color: var(--ds-text-disabled);
}
.empty-state__icon {
  opacity: 0.3;
  display: none;   /* ASCII-мітка вище замінює svg */
}

.empty-state__text {
  font-size: 0.9375rem;
  font-weight: var(--ds-weight-light);
  letter-spacing: 0.02em;
  color: var(--ds-text-muted);
}

/* ═══════════════════════════════════════════
   LOADING STATES + Skeleton screens (2026)
   ═══════════════════════════════════════════ */

.loading-state {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: var(--ds-space-6);
}

.loading-state--inline {
  height: auto;
  padding: var(--ds-space-3);
}

.loading-spinner {
  width: 1.5rem;
  height: 1.5rem;
  border: 0.125rem solid var(--ds-border-strong);
  border-top-color: var(--ds-accent);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

.loading-spinner--small {
  width: 1rem;
  height: 1rem;

/* ── Skeleton screens ── */
@keyframes skeleton-pulse {
  0% { opacity: 0.4; }
  50% { opacity: 0.8; }
  100% { opacity: 0.4; }
}
.skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-3);
  padding: var(--ds-space-4);
}
.skeleton__line {
  height: 0.875rem;
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-overlay);
  animation: skeleton-pulse 1.5s ease-in-out infinite;
}
.skeleton__line--narrow { width: 30%; }
.skeleton__line--medium { width: 60%; }
.skeleton__line--wide   { width: 85%; }
}

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

/* ═══════════════════════════════════════════
   DESKTOP — grid layout is now the base (see mobile-first refactor
   at top of file). This block only handles the remaining desktop-only
   specifics: back-button hidden, header title layout, etc.
   ═══════════════════════════════════════════ */

/* The old `.sidebar.hidden { transform: none !important }` override
   is unnecessary: on desktop `.sidebar.hidden` never has `transform`
   defined in the base, so there's nothing to override. */

/* `.btn--back` is "home" navigation only meaningful on mobile where
   the sidebar overlays the chat. On desktop both panes are always
   visible; the button is redundant. */
.btn--back { display: none; }

@media (max-width: 48em) {
  .btn--back { display: flex; }
}

/* Breakpoint unified to 48em (was the 48.0625em trick which caused a
   1px specificity overlap). Mobile MQs use `max-width: 48em`; desktop
   uses `min-width: 48em`; they meet at exactly 48em where the later
   rule in source order wins — acceptable since 48em boundary is
   rarely hit exactly. */
@media (width > 48em) {
  .header   { padding: 0 var(--ds-space-3); }
  .title    { font-size: var(--ds-body-size); }
  /* Вертикаль лишається ds-space-3 (роомніший ритм списку, §8): раніше
     override перебивав цей рядок, тепер він сам тримає значення. */
  .dialog-item  { padding: var(--ds-space-3) var(--ds-space-3); }
  .chat-header  { padding: var(--ds-space-2) var(--ds-space-3); }
  .messages { padding: var(--ds-space-3) var(--ds-space-3) var(--ds-space-6); }
  .input    { padding: var(--ds-space-1) var(--ds-space-2); }

  /* Bubble clamped to ~70-character reading measure on wide displays.
     Previously `max-width: 65%` meant on a 1920px viewport bubbles grew
     to ~830px — past the optimum line length. Ch.4 + Ch.11 both argue
     for ~66ch / 42rem ceiling. */
  .message { max-width: min(65%, 42rem); }

  /* #remote-video тут мав 40rem × 30rem: сцена созвону на столі займала
     лівий верхній кут, показаний екран виглядав малим і зсунутим
     (23.09.2026). Сцена бере весь шар, розмір задає 50-call.css. */
  #local-video  { width: 13rem; height: 10rem; }
}
}

/* Кнопка «Запросити за посиланням» на порожньому екрані (06.09.2026):
   один вторинний CTA під підказкою, відкриває розділ запрошень. */
.empty-state__action {
  margin-top: var(--ds-space-4);
}

/* ── 90-panels-pipeline.css ── */
/* ПАНЕЛІ АГЕНТА — agent panel, банер оновлення, AI-пайплайн, smart UI, beautify
 *
 * Експериментальні поверхні агента. Найбільший кандидат на прибирання
 * у Ф3: частина класів не згадується ні в шаблонах, ні в JS.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer utilities {
/* ═══════════════════════════════════════════
   AGENT PANEL — Brockmann Style
   ═══════════════════════════════════════════ */

.agent-toggle {
  position: fixed;
  bottom: var(--ds-space-5);
  right: var(--ds-space-5);
  width: 3rem;
  height: 3rem;
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  border: none;
  border-radius: 0;
  cursor: pointer;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.1s;
}

.agent-toggle:hover {
  transform: scale(1.05);
}

.agent-toggle:active {
  transform: scale(0.95);
}

.agent-panel {
  position: fixed;
  bottom: var(--ds-space-5);
  right: var(--ds-space-5);
  width: 25rem;
  max-width: calc(100vw - var(--ds-space-2) * 6);
  height: 30rem;
  max-height: calc(100vh - var(--ds-space-2) * 6);
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-text-primary);
  display: flex;
  flex-direction: column;
  z-index: 1001;
}

.agent-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--ds-space-4) var(--ds-space-5);
  border-bottom: 0.0625rem solid var(--ds-text-primary);
}

.agent-panel__header h2 {
  font-size: var(--ds-label-size);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 0;
}

.agent-panel__output {
  flex: 1;
  overflow-y: auto;
  padding: var(--ds-space-4);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-eyebrow-size);
  line-height: 1.5;
}

.agent-panel__output .cmd {
  color: var(--ds-text-subtle);
  margin-bottom: var(--ds-space-2);
}

.agent-panel__output .cmd::before {
  content: "";
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  margin-inline-end: 0.25rem;
  background: var(--ds-text-primary);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  vertical-align: middle;
}

.agent-panel__output .result {
  color: var(--ds-text-primary);
  margin-bottom: var(--ds-space-4);
  white-space: pre-wrap;
  word-break: break-word;
}

.agent-panel__output .tool {
  color: var(--ds-text-subtle);
  font-style: italic;
  margin-bottom: var(--ds-space-2);
}

.agent-panel__output .tool::before {
  content: "";
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  margin-inline-end: 0.25rem;
  background: var(--ds-text-subtle);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z'/%3E%3C/svg%3E")
    center/contain no-repeat;
  vertical-align: middle;
}

.agent-panel__output .error {
  /* Найближча роль — danger (#f0606e); було #ff4444, точного токена
     нема. Задумана зміна, обидва значення тут для звіту. */
  color: var(--ds-feedback-danger);
}

.agent-panel__input {
  display: flex;
  border-top: 0.0625rem solid var(--ds-text-primary);
}

.agent-panel__input .input {
  flex: 1;
  border: none;
  background: transparent;
  padding: var(--ds-space-4) var(--ds-space-5);
}

.agent-panel__input .btn--send {
  width: 3rem;
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  border: none;
  font-size: var(--ds-label-size);
}

/* Agent auth in settings */
.agent-auth {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

.agent-error {
  /* Найближча роль — danger (#f0606e); було #ff4444. */
  color: var(--ds-feedback-danger);
  font-size: var(--ds-eyebrow-size);
  min-height: var(--ds-space-5);
}

/* Mobile adjustments */
@media (max-width: 30em) {
  .agent-panel {
    width: calc(100vw - var(--ds-space-2) * 4);
    height: calc(100vh - var(--ds-space-2) * 20);
    bottom: var(--ds-space-4);
    right: var(--ds-space-4);
  }

  .agent-toggle {
    bottom: var(--ds-space-4);
    right: var(--ds-space-4);
  }
}

/* ═══════════════════════════════════════════
   UPDATE BANNER
   ═══════════════════════════════════════════ */

.update-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  padding: var(--ds-space-4);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-4);
  z-index: 9999;
  font-size: var(--ds-meta-size);
}

.update-banner button {
  background: var(--ds-surface-base);
  color: var(--ds-text-primary);
  border: none;
  padding: var(--ds-space-2) var(--ds-space-4);
  cursor: pointer;
  font-size: var(--ds-eyebrow-size);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* ═══════════════════════════════════════════
   AI PIPELINE (Brockmann style)
   ═══════════════════════════════════════════ */

.ai-pipeline {
  font-family: var(--ds-font);
  font-size: var(--ds-meta-size);
  line-height: 1.6;
}

.pipe-step {
  padding: var(--ds-space-2) 0;
  border-bottom: 0.0625rem solid color-mix(in srgb, var(--ds-text-primary) 5%, transparent);
  cursor: pointer;
  transition: background 0.1s;
}

.pipe-step:hover {
  background: color-mix(in srgb, var(--ds-text-primary) 2%, transparent);
}

.pipe-step:last-of-type {
  border-bottom: none;
}

.pipe-step__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.step-status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--ds-space-4) + var(--ds-space-1));
  height: calc(var(--ds-space-4) + var(--ds-space-1));
  font-size: var(--ds-eyebrow-size);
  font-family: var(--ds-font-mono);
  flex-shrink: 0;
}

.step--done .step-status {
  color: var(--ds-text-primary);
}

.step--pending {
  opacity: 0.4;
}

.step--pending .step-status {
  color: var(--ds-text-subtle);
}

.step--error .step-status {
  color: var(--ds-feedback-danger);
}

.step-name {
  flex: 1;
  font-weight: 500;
}

.step-toggle {
  font-size: 0.625rem;
  color: var(--ds-text-subtle);
  transition: transform 0.2s;
}

.pipe-step.expanded .step-toggle {
  transform: rotate(90deg);
}

.step-result {
  display: none;
  margin-top: var(--ds-space-2);
  margin-inline-start: var(--ds-space-5);
  padding: var(--ds-space-2);
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  max-height: 12.5rem;
  overflow-y: auto;
}

.pipe-step.expanded .step-result {
  display: block;
}

.step-result code {
  display: block;
  white-space: pre-wrap;
  word-break: break-all;
  line-height: 1.4;
}

/* Syntax highlighting for code results — поза каталогом ролей DESIGN_SYSTEM
   (там нема "keyword/string/number/comment"). Найближчі за роллю:
   keyword→danger (#f0606e, було #ff6b6b), string→accent-hover (#37c081
   dark/#268a5c light, було пастельне #a5d6a7 — зміна помітна),
   comment→text-subtle (#80808a dark ≈ було #888, дуже близько).
   number: синього токена в системі нема взагалі — тимчасово --tg
   (телеграм-бренд, єдиний синій, який десь визначений); Ф3 має завести
   справжній --ds-feedback-info. Усі 4 — задокументовані зміни, не hex. */
.step-result .hl-keyword {
  color: var(--ds-feedback-danger);
}
.step-result .hl-string {
  color: var(--ds-accent-hover);
}
.step-result .hl-number {
  color: var(--tg);
}
.step-result .hl-comment {
  color: var(--ds-text-subtle);
  font-style: italic;
}

.result-files {
  color: var(--ds-text-primary);
  margin-bottom: var(--ds-space-2);
}

.result-content {
  white-space: pre-wrap;
  word-break: break-all;
}

.result-edit {
  color: var(--ds-feedback-danger);
}

/* AI Thinking animation */
.ai-thinking {
  display: flex;
  align-items: center;
  padding: var(--ds-space-2) 0;
}

.thinking-dots {
  display: flex;
  gap: 0.25rem;
}

.thinking-dots span {
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--ds-text-subtle);
  animation: thinkPulse 1.4s ease-in-out infinite;
  will-change: transform, opacity;
}

.thinking-dots span:nth-child(2) {
  animation-delay: 0.2s;
}

.thinking-dots span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes thinkPulse {
  0%,
  80%,
  100% {
    transform: scale(0.6);
    opacity: 0.3;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}

/* Phase indicator for pipeline */
.ai-phase {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: var(--ds-space-3) var(--ds-space-4);
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  margin-bottom: var(--ds-space-2);
}

.phase-spinner {
  width: 1rem;
  height: 1rem;
  border: 0.125rem solid var(--ds-text-subtle);
  border-top-color: var(--ds-accent);
  border-radius: 50%;
  animation: phaseSpin 0.8s linear infinite;
}

.phase-text {
  color: var(--ds-text-secondary);
  font-size: var(--ds-label-size);
  font-weight: 500;
}

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

/* Collapsible steps */
.ai-steps-details {
  margin-top: var(--ds-space-2);
}

.ai-steps-summary {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  cursor: pointer;
  user-select: none;
  list-style: none;
}

.ai-steps-summary::-webkit-details-marker {
  display: none;
}

.ai-steps-summary::before {
  content: "▸ ";
}

.ai-steps-details[open] .ai-steps-summary::before {
  content: "▾ ";
}

.ai-steps-container {
  margin-top: var(--ds-space-2);
  padding-inline-start: var(--ds-space-2);
  border-left: 0.0625rem solid var(--ds-border-subtle);
}

.step--run .step-status {
  color: var(--ds-text-subtle);
  animation: blink 1s infinite;
}

@keyframes blink {
  50% {
    opacity: 0.3;
  }
}

.step-name {
  color: var(--ds-text-primary);
}

.step-params {
  margin-inline-start: 2rem;
  color: var(--ds-text-subtle);
  font-family: monospace;
  font-size: var(--ds-eyebrow-size);
  word-break: break-all;
}

.step-result {
  margin-inline-start: 2rem;
  color: var(--ds-text-subtle);
  font-family: monospace;
  font-size: var(--ds-eyebrow-size);
  opacity: 0.7;
  word-break: break-all;
}

.pipe-answer {
  margin-top: var(--ds-space-3);
  padding-top: var(--ds-space-3);
  border-top: 0.0625rem solid color-mix(in srgb, var(--ds-text-primary) 10%, transparent);
  color: var(--ds-text-primary);
  font-size: var(--ds-label-size);
  line-height: 1.5;
}

/* AI Controls */
.ai-controls {
  display: flex;
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
}

.ai-mode-btn {
  width: 2rem;
  height: 2rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--ds-radius-xs);
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: all 0.15s ease;
}

.ai-mode-btn:hover {
  color: var(--ds-text-primary);
}

.ai-mode-btn.active {
  background: var(--ds-accent);
  /* on-accent: dark #06140d (було #fff — задумана зміна), light #ffffff (те саме). */
  color: var(--ds-on-accent);
}

/* Pipeline mode wrapper for hints dropdown */
.ai-mode-wrapper {
  position: relative;
}

/* `--border-color` (4 місця нижче) не визначено ніде — ні в src/css.rs,
   ні в жодному :root. Це не легасі-міст, а битий (мертвий) custom
   property: var() без fallback і без визначення = guaranteed-invalid,
   тому весь border/border-bottom, що на ньому стояв, computed-style
   давно вже "none" (перевірено computed-style пробою до правки — рамка
   не рендериться взагалі, стрілка ::after теж невидима). Замінено на
   явний `transparent`, щоб зберегти той самий (невидимий) вигляд і
   прибрати битий легасі-хіт з лінтера. */
.pipeline-hints {
  display: none;
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 0.5rem;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid transparent;
  border-radius: var(--ds-radius-sm);
  padding: 0.5rem 0;
  min-width: 12.5rem;
  box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, black 30%, transparent);
  z-index: 1000;
}

.pipeline-hints::before {
  content: "";
  position: absolute;
  top: -0.375rem;
  left: 50%;
  transform: translateX(-50%);
  border-left: 0.375rem solid transparent;
  border-right: 0.375rem solid transparent;
  border-bottom: 0.375rem solid transparent;
}

.pipeline-hints::after {
  content: "";
  position: absolute;
  top: -0.3125rem;
  left: 50%;
  transform: translateX(-50%);
  border-left: 0.3125rem solid transparent;
  border-right: 0.3125rem solid transparent;
  border-bottom: 0.3125rem solid var(--ds-surface-raised);
}

.ai-mode-wrapper:hover .pipeline-hints,
.pipeline-hints:hover {
  display: block;
}

.pipeline-hints__header {
  padding: 0.25rem 0.75rem 0.5rem;
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  border-bottom: 0.0625rem solid transparent;
  margin-bottom: 0.25rem;
}

.pipeline-hints__item {
  padding: 0.5rem 0.75rem;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-primary);
  cursor: pointer;
  transition: background 0.15s ease;
}

.pipeline-hints__item:hover {
  background: var(--ds-surface-overlay);
}

/* Input autocomplete dropdown */
.input-autocomplete {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.25rem;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid transparent;
  border-radius: var(--ds-radius-sm);
  box-shadow: 0 0.25rem 0.75rem color-mix(in srgb, black 30%, transparent);
  z-index: 1000;
  max-height: 12.5rem;
  overflow-y: auto;
}

.input-autocomplete.visible {
  display: block;
}

.autocomplete-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.625rem 0.75rem;
  cursor: pointer;
  transition: background 0.15s ease;
}

.autocomplete-item:hover,
.autocomplete-item.active {
  background: var(--ds-surface-overlay);
}

.ac-cmd {
  font-family: monospace;
  color: var(--ds-accent);
  font-weight: 600;
}

.ac-label {
  flex: 1;
  color: var(--ds-text-primary);
  font-size: var(--ds-meta-size);
}

.ac-mode {
  font-size: var(--ds-eyebrow-size);
  padding: 0.125rem 0.375rem;
  border-radius: var(--ds-radius-xs);
  text-transform: uppercase;
}

.ac-mode--pipeline {
  background: var(--ds-accent);
  /* on-accent: dark #06140d (було #fff — задумана зміна), light #ffffff (те саме). */
  color: var(--ds-on-accent);
}

.ac-mode--chat {
  background: var(--ds-surface-overlay);
  color: var(--ds-text-subtle);
}

/* AI avatar color */
.dialog-avatar--ai {
  /* Найближчі ролі: text-disabled (#56565e, було #666) → surface-active
     (#2e2e35, було #333). Задумана зміна, точних сірих токенів нема. */
  background: var(--ds-surface-active);
  color: var(--ds-text-primary);
}

/* ═══════════════════════════════════════════
   PIPELINE
   ═══════════════════════════════════════════ */

.overlay--pipeline {
  align-items: flex-start;
  padding: var(--ds-space-4);
}

.pipeline-container {
  width: 100%;
  max-width: 37.5rem;
  max-height: 90vh;
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-text-primary);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.pipeline-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--ds-space-4);
  border-bottom: 0.0625rem solid var(--ds-text-primary);
}

.pipeline-header h2 {
  font-size: var(--ds-body-size);
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.pipeline-input {
  display: flex;
  gap: var(--ds-space-2);
  padding: var(--ds-space-4);
  border-bottom: 0.0625rem solid color-mix(in srgb, var(--ds-text-primary) 20%, transparent);
}

.pipeline-input .input {
  flex: 1;
  margin: 0;
  border: 0.0625rem solid var(--ds-text-subtle);
  padding: var(--ds-space-3);
}

.pipeline-status {
  padding: var(--ds-space-4);
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  display: none;
}

.pipeline-status.active {
  display: block;
  color: var(--ds-feedback-danger);
  animation: pulse 1s infinite;
}

/* pulse defined above */

.pipeline-plan {
  padding: 0 var(--ds-space-4);
  max-height: 9.5rem;
  overflow-y: auto;
}

.pipeline-plan-step {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) 0;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  border-bottom: 0.0625rem solid color-mix(in srgb, var(--ds-text-primary) 5%, transparent);
}

.pipeline-plan-step.done {
  color: var(--ds-text-primary);
}

.pipeline-plan-step.running {
  color: var(--ds-feedback-danger);
}

.pipeline-plan-step__num {
  width: calc(var(--ds-space-4) + var(--ds-space-1));
  height: calc(var(--ds-space-4) + var(--ds-space-1));
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0.0625rem solid currentColor;
  font-weight: 600;
}

.pipeline-plan-step.done .pipeline-plan-step__num {
  background: var(--ds-text-primary);
  color: var(--ds-surface-base);
  border-color: var(--ds-text-primary);
}

.pipeline-plan-step.running .pipeline-plan-step__num {
  border-color: var(--ds-feedback-danger);
  animation: pulse 1s infinite;
}

.pipeline-steps {
  flex: 1;
  overflow-y: auto;
  padding: var(--ds-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}

.pipeline-step {
  padding: var(--ds-space-3);
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-meta-size);
}

.pipeline-step.success {
  border-left-color: var(--ds-text-primary);
}

.pipeline-step.error {
  border-left-color: var(--ds-feedback-danger);
}

.pipeline-step__action {
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin-bottom: var(--ds-space-2);
}

.pipeline-step__data {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
  max-height: 6rem;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
}

.pipeline-result {
  padding: var(--ds-space-4);
  border-top: 0.0625rem solid var(--ds-text-primary);
  display: none;
}

.pipeline-result.visible {
  display: block;
}

.pipeline-result__title {
  font-size: var(--ds-meta-size);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ds-text-primary);
  margin-bottom: var(--ds-space-2);
}

.pipeline-result__summary {
  font-size: var(--ds-label-size);
  color: var(--ds-text-primary);
}

.pipeline-result__stats {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
  margin-top: var(--ds-space-2);
}

/* ═══════════════════════════════════════════
   SMART UI — Predictive Interface
   Desktop only: auto-hide panels when reading history
   ═══════════════════════════════════════════ */

@media (width > 48em) {
  /* Transition on the layout's grid tracks, not on the children's
     `width`. Grid track sizes animate cleanly in modern browsers when
     both states use the same unit. `width` transitions on grid-area
     children were no-ops (the grid is what controls their size), so
     the old setup animated `transform`+`opacity` only — the sidebar
     snapping to 0 column width was the jank. Now: 20rem ↔ 0rem on the
     grid itself, children just fade/slide into their own space. */
  .layout {
    transition: grid-template-columns var(--duration-normal) var(--ease-out),
                grid-template-rows var(--duration-normal) var(--ease-out);
  }
  .layout .sidebar,
  .layout .header {
    transition:
      transform var(--duration-normal) var(--ease-out),
      opacity var(--duration-normal) var(--ease-out);
  }

  /* Collapsed state — sidebar slides + fades (width is driven by the
     grid track collapse below). */
  .layout.smart-ui--collapsed .sidebar {
    opacity: 0;
    transform: translateX(-100%);
    pointer-events: none;
  }

  /* Collapsed state — header slides up */
  .layout.smart-ui--collapsed .header {
    transform: translateY(-100%);
    opacity: 0;
    pointer-events: none;
  }

  /* Collapse grid tracks to 0. Matching unit type (rem ↔ rem) is
     required for smooth interpolation of `grid-template-columns`. */
  .layout.smart-ui--collapsed {
    grid-template-columns: 0rem 1fr;
    grid-template-rows: 0rem 1fr;
  }

  /* Main area takes full space */
  .layout.smart-ui--collapsed .main {
    border-radius: 0;
  }

  /* Chat header stays visible but minimal */
  .layout.smart-ui--collapsed .chat-header {
    background: transparent;
    border-bottom-color: transparent;
  }

  /* Subtle indicator that panels are hidden */
  .layout.smart-ui--collapsed::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    width: 0.1875rem;
    height: 100%;
    background: var(--ds-accent);
    opacity: 0.3;
    z-index: 100;
    pointer-events: none;
  }
}

/* Settings Tabs */
.settings-tabs {
  display: flex;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  margin-bottom: var(--ds-space-5);
}

.tab-btn {
  flex: 1;
  background: transparent;
  border: none;
  border-bottom: 0.125rem solid transparent;
  padding: var(--ds-space-3);
  font-size: var(--ds-label-size);
  font-weight: 500;
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: all 0.2s ease;
}

.tab-btn:hover {
  color: var(--ds-text-primary);
}

.tab-btn.active {
  color: var(--ds-text-primary);
  border-bottom-color: var(--ds-accent);
}

.tab-content {
  display: none;
  animation: fadeIn 0.2s ease-out;
}

.tab-content.active {
  display: block;
}

/* ═══════════════════════════════════════════
   BEAUTIFY CARDS - Minimal SVG Style
   ═══════════════════════════════════════════ */

.beautify-card {
  margin: var(--ds-space-2) 0;
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
}

/* ─── Weather Card ─── */
.weather-card {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

.weather-card__icon {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--ds-text-primary);
  flex-shrink: 0;
}

.weather-card__icon svg {
  width: 100%;
  height: 100%;
}

/* Icon Animations */
.weather--sunny .weather-card__icon svg {
  animation: iconSpin 20s linear infinite;
}

.weather--partly .weather-card__icon svg,
.weather--cloudy .weather-card__icon svg {
  animation: iconDrift 4s ease-in-out infinite;
}

.weather--rainy .weather-card__icon svg {
  animation: iconBounce 1.5s ease-in-out infinite;
}

.weather--snowy .weather-card__icon svg {
  animation: iconFloat 3s ease-in-out infinite;
}

.weather--storm .weather-card__icon svg {
  animation: iconFlash 2s ease-in-out infinite;
}

.weather--foggy .weather-card__icon svg {
  animation: iconFade 3s ease-in-out infinite;
}

@keyframes iconSpin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes iconDrift {
  0%,
  100% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(0.25rem);
  }
}

@keyframes iconBounce {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(0.1875rem);
  }
}

@keyframes iconFloat {
  0%,
  100% {
    transform: translateY(0) scale(1);
  }
  50% {
    transform: translateY(-0.125rem) scale(1.02);
  }
}

@keyframes iconFlash {
  0%,
  85%,
  100% {
    opacity: 1;
  }
  90%,
  95% {
    opacity: 0.4;
  }
}

@keyframes iconFade {
  0%,
  100% {
    opacity: 0.7;
  }
  50% {
    opacity: 1;
  }
}

.weather-card__content {
  display: flex;
  align-items: baseline;
  gap: var(--ds-space-2);
  flex: 1;
  min-width: 0;
}

.weather-card__location {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
}

.weather-card__temp {
  display: flex;
  align-items: baseline;
}

.temp-value {
  font-size: var(--ds-label-size);
  font-weight: 600;
  font-family: var(--ds-font-mono);
  color: var(--ds-text-primary);
}

.temp-unit {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-subtle);
}

.weather-card__condition {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-secondary);
}

.weather-card__wind {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--ds-text-subtle);
  font-size: var(--ds-eyebrow-size);
  margin-inline-start: auto;
}

.wind-icon {
  width: 0.875rem;
  height: 0.875rem;
  animation: windBlow 2s ease-in-out infinite;
}

.wind-icon svg {
  width: 100%;
  height: 100%;
}

@keyframes windBlow {
  0%,
  100% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(0.125rem);
  }
}

.wind-value {
  font-family: var(--ds-font-mono);
}

/* ─── Exchange Card ─── */
.exchange-card {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

.exchange-card__icon {
  width: 1rem;
  height: 1rem;
  color: var(--ds-text-subtle);
  flex-shrink: 0;
  animation: exchangePulse 3s ease-in-out infinite;
}

.exchange-card__icon svg {
  width: 100%;
  height: 100%;
}

@keyframes exchangePulse {
  0%,
  100% {
    transform: scaleX(1);
  }
  50% {
    transform: scaleX(1.1);
  }
}

.exchange-card__rate {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

.exchange-card__from,
.exchange-card__to {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
}

.currency-code {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-subtle);
}

.currency-amount {
  font-size: var(--ds-label-size);
  font-weight: 600;
  font-family: var(--ds-font-mono);
  color: var(--ds-text-primary);
}

.exchange-card__to .currency-amount {
  color: var(--ds-accent);
}

.exchange-card__arrow {
  width: 0.875rem;
  height: 0.875rem;
  color: var(--ds-text-subtle);
  animation: arrowSlide 2s ease-in-out infinite;
}

.exchange-card__arrow svg {
  width: 100%;
  height: 100%;
}

@keyframes arrowSlide {
  0%,
  100% {
    transform: translateX(0);
    opacity: 0.5;
  }
  50% {
    transform: translateX(0.125rem);
    opacity: 1;
  }
}

.exchange-card__date {
  font-size: 0.7rem;
  color: var(--ds-text-subtle);
  margin-inline-start: auto;
}

/* ─── Error Card ─── */
.error-card {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  color: var(--ds-feedback-danger);
  background: color-mix(in srgb, var(--ds-feedback-danger) 8%, transparent);
  border-color: color-mix(in srgb, var(--ds-feedback-danger) 20%, transparent);
}

.error-card__icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.error-card__icon svg {
  width: 100%;
  height: 100%;
}

.error-card__text {
  font-size: var(--ds-meta-size);
}

/* ─── News Card ─── */
.news-card {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-3);
  max-width: 100%;
}

.news-card--empty {
  flex-direction: row;
  align-items: center;
  gap: var(--ds-space-2);
}

.news-card__header {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding-bottom: var(--ds-space-1);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}

.news-card__icon {
  width: 1.125rem;
  height: 1.125rem;
  flex-shrink: 0;
  color: var(--ds-accent);
}

.news-card__icon svg {
  width: 100%;
  height: 100%;
}

.news-card__title {
  font-weight: 500;
  font-size: var(--ds-label-size);
  text-transform: capitalize;
}

.news-card__count {
  margin-inline-start: auto;
  font-size: var(--ds-eyebrow-size);
  color: var(--muted);
}

.news-card__empty {
  color: var(--muted);
  font-size: var(--ds-meta-size);
}

.news-card__list {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-3);
}

.news-item {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
  animation: newsSlideIn 0.3s ease-out backwards;
}

@keyframes newsSlideIn {
  from {
    opacity: 0;
    transform: translateY(0.25rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.news-item__title {
  font-size: var(--ds-meta-size);
  line-height: 1.4;
  color: var(--ds-text-primary);
}

.news-item__meta {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  font-size: var(--ds-eyebrow-size);
  color: var(--muted);
}

.news-item__source,
.news-item__time {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
}

.news-item__source svg,
.news-item__time svg {
  width: 0.75rem;
  height: 0.75rem;
  opacity: 0.6;
}

/* ─── JSON Cards ─── */
.json-card {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-eyebrow-size);
}

.json-card--array {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--ds-space-2);
}

.json-card--list {
  gap: 0.625rem;
}

.json-row {
  display: flex;
  gap: var(--ds-space-2);
  align-items: baseline;
}

.json-key {
  color: var(--ds-accent);
  font-weight: 500;
  flex-shrink: 0;
}

.json-key::after {
  content: ":";
  color: var(--muted);
}

.json-value {
  color: var(--ds-text-primary);
  word-break: break-word;
}

.json-array-item {
  padding: var(--ds-space-1) 0.375rem;
  background: var(--surface);
  border-radius: calc(var(--ds-radius-md) * 0.5);
  color: var(--ds-text-primary);
}

.json-list-item {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.json-list-title {
  color: var(--ds-text-primary);
  font-family: var(--ds-font);
  font-size: var(--ds-meta-size);
}

.json-list-sub {
  color: var(--muted);
  font-size: var(--ds-eyebrow-size);
}

.json-more {
  color: var(--muted);
  font-size: var(--ds-eyebrow-size);
  font-style: italic;
}

/* ─── LLM Generated Widgets ─── */
.beautify-llm {
  font-size: var(--ds-meta-size);
  line-height: 1.5;
}

.beautify-llm ul,
.beautify-llm ol {
  margin: 0;
  padding-inline-start: var(--ds-space-4);
}

.beautify-llm li {
  margin-bottom: var(--ds-space-1);
}

.beautify-llm table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ds-eyebrow-size);
}

.beautify-llm td,
.beautify-llm th {
  padding: var(--ds-space-1);
  text-align: left;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}
}

/* ── 95-utilities.css ── */
/* УТИЛІТИ — дії на наведення, анімації, посилання, markdown, view transitions
 *
 * Наскрізні дрібниці поверх компонентів: мікроанімації, розмітка тексту
 * повідомлення, переходи між екранами.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

@layer utilities {
/* ═══════════════════════════════════════════
   MESSAGE HOVER ACTIONS
   ═══════════════════════════════════════════ */

.message__attachments {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
  margin-top: var(--ds-space-2);
  padding-top: var(--ds-space-2);
  border-top: 0.0625rem solid var(--ds-border-subtle);
}
.message__attachment {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  /* Палець, не курсор: чіп відкриває перегляд, тож тримаємо 2.75rem. */
  min-height: 2.75rem;
  padding: var(--ds-space-1) var(--ds-space-2);
  background: var(--ds-surface-hover);
  color: var(--ds-text-secondary);
  text-decoration: none;
  font-size: var(--ds-label-size);
  font-family: var(--ds-font-mono);
  overflow: hidden;
  cursor: pointer;
  transition: background var(--ds-motion-fast) var(--ds-ease-out);
}
.message__attachment:hover {
  color: var(--ds-text-primary);
  background: var(--ds-surface-active);
}
.message__attachment svg {
  flex-shrink: 0;
  opacity: 0.6;
}
/* Прев'ю першої сторінки просто в баблі: чіп із самою назвою не давав
   упізнати документ, доки його не відкриєш. Пропорції не тягнемо —
   обрізаємо по квадрату, щоб рядки чіпів лишались однакової висоти. */
.attachment__thumb {
  width: 2.5rem;
  height: 2.5rem;
  flex-shrink: 0;
  object-fit: cover;
  object-position: top center;
  background: var(--ds-surface-raised);
  border: 0.0625rem solid var(--ds-border-subtle);
}
.attachment__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attachment__size {
  opacity: 0.5;
  flex-shrink: 0;
}

.message__actions {
  position: absolute;
  bottom: 100%;
  right: 0;
  margin-bottom: 0.25rem;
  display: none;
  gap: 0.125rem;
  background: var(--ds-surface-active);
  padding: 0.125rem;
  box-shadow: 0 0.125rem 0.5rem color-mix(in srgb, black 30%, transparent);
  white-space: nowrap;
  z-index: 5;
}

.message--incoming .message__actions {
  right: auto;
  left: 0;
}
.message--outgoing .message__actions {
  right: 0;
  left: auto;
}

.message:hover .message__actions {
  display: flex;
}

.message__actions button {
  width: 1.75rem;
  height: 1.75rem;
  border: none;
  background: transparent;
  color: var(--ds-text-secondary);
  cursor: pointer;
  border-radius: var(--ds-radius-xs);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background var(--ds-motion-fast), color var(--ds-motion-fast);
}

.message__actions button:hover {
  background: var(--ds-surface-hover);
  color: var(--ds-text-primary);
}

/* Icon atom (ui-atoms::Icon → inline <svg class="icon">). Inherits color
   via currentColor; sized in em so it tracks the surrounding font. One
   baseline rule so every icon across the app aligns and scales the same. */
.icon {
  display: inline-block;
  vertical-align: -0.125em;
  flex: 0 0 auto;
  width: 1em;
  height: 1em;
}
.message__actions .icon {
  width: 1rem;
  height: 1rem;
}

/* Photo in messages */
.message__photo {
  display: block;
  max-width: 100%;
  max-height: 18.75rem;
  object-fit: contain;
  margin: 0.25rem 0;
  cursor: pointer;
}
.message__photo:hover { opacity: 0.9; }

/* Video in messages */
.message__video {
  display: block;
  max-width: 100%;
  max-height: 18.75rem;
  margin: 0.25rem 0;
}

/* Voice message */
.message__voice {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: 0.25rem 0;
}
.message__voice audio {
  width: 12.5rem;
  height: 2rem;
  filter: grayscale(1) brightness(1.5) contrast(0.8);
  opacity: 0.8;
}
.message__voice audio::-webkit-media-controls-panel {
  background: var(--ds-surface-overlay);
}
.message__voice audio::-webkit-media-controls-current-time-display,
.message__voice audio::-webkit-media-controls-time-remaining-display {
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-mono);
  font-size: 0.625rem;
}

/* Sticker */
.message__sticker {
  display: block;
  max-width: 10rem;
  max-height: 10rem;
  margin: 0.25rem 0;
}

/* File link */
.message__file {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.375rem 0.625rem;
  background: color-mix(in srgb, var(--ds-text-primary) 4%, transparent);
  font-size: var(--ds-meta-size);
  font-family: var(--ds-font-mono);
  color: var(--ds-text-secondary);
  text-decoration: none;
  margin: 0.25rem 0;
}
.message__file:hover { color: var(--ds-accent); }
.message__file svg { width: 0.875rem; height: 0.875rem; }

/* Inline audio (from safe_html voice upload) */
.message__text audio {
  display: block;
  max-width: 15rem;
  height: 2rem;
  margin: 0.25rem 0;
  filter: grayscale(1) brightness(1.5) contrast(0.8);
  opacity: 0.8;
}

/* Inline file link (from safe_html) */
.message__text a[href*="/api/files/"] {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.5rem;
  background: color-mix(in srgb, var(--ds-text-primary) 4%, transparent);
  font-size: var(--ds-meta-size);
  font-family: var(--ds-font-mono);
}

/* Media badge fallback */
.message__media-badge {
  display: inline-block;
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.125rem 0;
}

/* Transcription (whisper) */
.message__transcription {
  font-size: var(--ds-label-size);
  color: var(--ds-text-secondary);
  font-style: italic;
  padding: var(--ds-space-1) var(--ds-space-2);
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  margin-bottom: 0.125rem;
}

/* Voice waveform player — base .message__voice flex row is defined once
   above (native-audio block); children differ per player generation. */
.voice-play {
  /* HIG 44pt — outer hit area transparent, inner circle 2rem
     accent-tinted with outline instead of solid fill. Less shouty,
     reads as "button" without screaming. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0.375rem;
  border-radius: 50%;
  background: var(--ds-accent-subtle);
  color: var(--ds-accent);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background 0.15s ease, transform 0.1s ease;
}
.voice-play:hover { background: color-mix(in srgb, var(--ds-accent) 22%, transparent); }
.voice-play:active { transform: scale(0.94); }
.voice-play svg { width: 1rem; height: 1rem; }
.voice-play.playing svg { display: none; }
.voice-play.playing::after { content: "||"; font-size: 0.625rem; font-weight: 700; letter-spacing: 0.0625rem; }
.voice-waveform {
  height: 1.75rem;
  min-width: 7.5rem;
  flex: 1;
  display: block;
  cursor: pointer;
  touch-action: none;
  transition: filter 0.2s ease;
}
.voice-waveform:hover { filter: brightness(1.15); }
.voice-waveform:active { filter: brightness(0.9); }
/* Subtle "breathing" when audio is playing — mirrors Telegram's waveform
   pulse. Applied to the parent wrapper via .playing class set by toggleVoice. */
.message__voice .voice-play.playing ~ .voice-waveform {
  animation: voicePulse 1.6s ease-in-out infinite;
}
@keyframes voicePulse {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(1.12); }
}
.voice-dur {
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
  font-family: var(--ds-font-mono);
  min-width: 2rem;
  text-align: right;
}
.message__voice audio { display: none; }

/* Live voice waveform in call messages */
.message--voice-live .message__bubble {
  min-width: 8.75rem;
}
.message--voice-live .message__transcription {
  min-height: 1em;
}

/* Code blocks in messages */
.message__text pre {
  background: var(--ds-surface-base);
  padding: var(--ds-space-2);
  margin: var(--ds-space-1) 0;
  overflow-x: auto;
  font-size: var(--ds-meta-size);
}
.message__text code {
  font-family: var(--ds-font-mono);
  font-size: 0.9em;
}
.message__text pre code {
  font-size: var(--ds-meta-size);
}
.message__text code:not(pre code) {
  background: color-mix(in srgb, var(--ds-text-primary) 6%, transparent);
  padding: 0.0625rem 0.25rem;
}

/* AI email draft — bottom sheet (mini-app style) */
#ai-reply-box {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 200;
  transform: translateY(100%);
  transition: transform 0.3s var(--ds-ease-in-out);
  max-height: 70vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-strong);
}
#ai-reply-box:not(:empty) {
  transform: translateY(0);
}

.ai-draft {
  padding: var(--ds-space-3);
}
.ai-draft__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--ds-eyebrow-size);
  color: var(--ds-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: var(--ds-space-2);
  padding-bottom: var(--ds-space-2);
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}
.ai-draft__handle {
  width: 2rem;
  height: 0.25rem;
  background: var(--ds-text-muted);
  border-radius: 0.125rem;
  margin: 0 auto var(--ds-space-2);
}
.ai-draft__subject {
  font-size: var(--ds-label-size);
  font-family: var(--ds-font-mono);
  color: var(--ds-text-secondary);
  margin-bottom: var(--ds-space-2);
}
.ai-draft__text {
  /* Легасі t3 — плинний clamp(0.875rem…1rem, +0.3vw); точної ds-ролі
     нема (у ds-тірі шкала фіксована, без fluid). Найближче — body-size
     (0.875rem, нижня межа t3): на вузьких екранах ідентично, на
     широких трохи менше за попередній максимум (1rem). Задумана зміна. */
  font-size: var(--ds-body-size);
  line-height: 1.6;
  padding: var(--ds-space-3);
  background: var(--ds-surface-base);
  min-height: 5rem;
  max-height: 40vh;
  overflow-y: auto;
  outline: none;
}
.ai-draft__text:focus {
  box-shadow: inset 0.125rem 0 0 var(--ds-accent);
}
.ai-draft__actions {
  display: flex;
  gap: var(--ds-space-2);
  margin-top: var(--ds-space-3);
  padding-top: var(--ds-space-2);
  border-top: 0.0625rem solid var(--ds-border-subtle);
}
.ai-draft__actions .btn--small {
  flex: 1;
  text-align: center;
  font-family: var(--ds-font-mono);
  text-transform: lowercase;
  letter-spacing: 0.05em;
}

/* Reply bar */
.reply-bar {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-3);
  background: var(--ds-surface-overlay);
  border-left: none;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  animation: msgAppear 0.15s var(--ds-ease-out);
}
.reply-bar__text {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.reply-bar__text strong {
  color: var(--ds-text-primary);
  margin-inline-end: 0.25rem;
}
.reply-bar__close {
  font-size: 0.875rem;
  color: var(--ds-text-muted);
  padding: 0.25rem;
}

/* Message swipe hint */
.message {
  touch-action: pan-y;
  overflow: visible;
}

/* AI loading indicator */
.ai-loading {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-3);
  color: var(--ds-text-muted);
  font-size: var(--ds-meta-size);
  font-family: var(--ds-font-mono);
  animation: aiFade 1.5s ease-in-out infinite;
}
@keyframes aiFade {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

/* Accounts bottom sheet — і шторка «це та сама людина» на тих самих рейках */
#accounts-box,
#link-box {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 200;
  transform: translateY(100%);
  transition: transform 0.3s var(--ds-ease-in-out);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--ds-surface-raised);
  border-top: 0.0625rem solid var(--ds-border-strong);
}
#accounts-box:not(:empty),
#link-box:not(:empty) { transform: translateY(0); }

/* Рядок пікера — кнопка, але виглядає як рядок списку. */
.link-picker__row {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
  text-align: left;
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.link-picker__row:hover { background: var(--ds-surface-hover); }
.link-picker input[type="search"] { width: 100%; margin-bottom: var(--ds-space-2); }
.accounts-panel { padding: var(--ds-space-3); }
.accounts-panel__header {
  display: flex; justify-content: space-between; align-items: center;
  font-size: var(--ds-eyebrow-size); color: var(--ds-text-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
  padding-bottom: var(--ds-space-2); border-bottom: 0.0625rem solid var(--ds-border-subtle);
  margin-bottom: var(--ds-space-3);
}
.accounts-panel__item {
  display: flex; align-items: center; gap: var(--ds-space-3);
  padding: var(--ds-space-2) 0; border-bottom: 0.0625rem solid var(--ds-border-subtle);
}
.accounts-panel__type {
  font-family: var(--ds-font-mono); font-size: var(--ds-meta-size);
  text-transform: uppercase; color: var(--ds-text-muted); width: 3.75rem;
}
.accounts-panel__key { flex: 1; font-size: var(--ds-body-size); font-family: var(--ds-font-mono); }
.accounts-panel__status { font-size: var(--ds-eyebrow-size); }
.accounts-panel__status--authorized { color: var(--ds-feedback-success); }
/* Легасі --warning тут НЕ роль feedback-warning (амбер) — у мосту
   src/css.rs це фіксований #d4d4d7 (притлумлений світлий текст), інакше
   змінився б колір. Найближча роль за тим самим hex — text-secondary
   (#b4b4ba dark, дуже близько; у light theme раніше світло-сірий на
   кремовому фоні майже не читався — тепер коректний темний текст). */
.accounts-panel__status--connected { color: var(--ds-text-secondary); }
.accounts-panel__status--disconnected { color: var(--ds-text-muted); }
.accounts-panel__status--active { color: var(--ds-feedback-success); }
.accounts-panel__empty { color: var(--ds-text-muted); font-size: var(--ds-label-size); padding: var(--ds-space-4) 0; text-align: center; }
.accounts-panel__add { margin-top: var(--ds-space-3); padding-top: var(--ds-space-2); }
.accounts-panel__add-title { font-size: var(--ds-meta-size); color: var(--ds-text-secondary); margin-bottom: var(--ds-space-2); }
.accounts-panel__add form { display: flex; gap: var(--ds-space-2); }
.accounts-panel__section {
  font-size: var(--ds-eyebrow-size); color: var(--ds-text-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
  margin-top: var(--ds-space-3); margin-bottom: var(--ds-space-1);
}
.accounts-panel__hint {
  font-size: var(--ds-meta-size); color: var(--ds-text-muted);
  font-family: var(--ds-font-mono);
  padding: var(--ds-space-2) 0;
}

/* Toast notifications */
.toast-container {
  position: fixed;
  top: var(--ds-space-3);
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
  pointer-events: none;
}
.toast {
  background: var(--ds-surface-active);
  color: var(--ds-text-primary);
  padding: var(--ds-space-2) var(--ds-space-4);
  font-size: var(--ds-label-size);
  font-family: var(--ds-font-mono);
  border: 0.0625rem solid var(--ds-border-default);
  transition: opacity 0.3s;
}

/* Перекладу не вийшло. Раніше на місці провалу вставлявся вихідний текст,
   і відрізнити зламане від «і так цією мовою» було нічим. */
.message__translation--failed {
  color: var(--ds-feedback-warning);
  font-style: normal;
}

/* Що НАСПРАВДІ прочитає співрозмовник, коли моє повідомлення пішло його
   мовою. Головним рядком лишається те, що я набрав, — інакше автор не
   впізнає власне повідомлення у своїй же стрічці. Згорнуто за
   замовчуванням: розгорнутий переклад читався як друге повідомлення. */
.message__sent-as {
  margin-top: 0.25rem;
}

.message__sent-as-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
  cursor: pointer;
  list-style: none;
  opacity: 0.75;
}

.message__sent-as-toggle::-webkit-details-marker { display: none; }

.message__sent-as-toggle:hover { opacity: 1; }

.message__sent-as-toggle svg {
  width: 0.75rem;
  height: 0.75rem;
}

.message__sent-as-code {
  letter-spacing: 0.04em;
}

.message__sent-as-text {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-muted);
  margin-top: 0.25rem;
  padding-top: 0.25rem;
  border-top: 0.0625rem dashed var(--ds-border-subtle);
}

/* Translation under message */
.message__translation {
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  margin-top: 0.25rem;
  padding-top: 0.25rem;
  border-top: 0.0625rem solid var(--ds-border-subtle);
  font-style: italic;
}

/* Long press: show actions on mobile */
.message.show-actions .message__actions {
  display: flex;
}

@media (hover: none) {
  .message:hover .message__actions {
    display: none;
  }
  .message.show-actions .message__actions {
    display: flex;
  }
}

/* ═══════════════════════════════════════════
   MICRO-ANIMATIONS (Dribbble style)
   ═══════════════════════════════════════════ */

/* View Transitions — scoped to key containers */
#dialog-list { view-transition-name: dialog-list; }
#messages { view-transition-name: messages; }

/* Message appear — spring slide up + fade in */
/* Skip animation during bulk load — only animate new messages */
.messages--no-anim .msg-appear,
.messages--no-anim .message--outgoing.msg-appear {
  animation: none;
}
/* Message appear — opacity + subtle Y-slide. Transform on :not(msgAppear)
   element itself doesn't shift siblings (it's just visual), so no scroll
   jitter; the spring/scale version that did caused layout reflow and was
   removed. 0.375rem of travel is enough to feel "pop in" without drawing the
   eye too much. */
.msg-appear {
  animation: msgAppear 0.28s var(--ds-ease-out);
}
@keyframes msgAppear {
  from { opacity: 0; transform: translateY(0.375rem); }
  to   { opacity: 1; transform: translateY(0); }
}
.message--outgoing.msg-appear { animation-name: msgAppearOut; }
@keyframes msgAppearOut {
  from { opacity: 0; transform: translateY(0.375rem) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
/* Ехо (outbox.js) стало справжнім баблом на тому ж місці (transport.js
   appendOnlyNew): сам бабл удруге не зʼявляється, проявляються лише час
   і статус. */
.message--confirmed .message__time,
.message--confirmed .message__status {
  animation: msgConfirm var(--ds-motion) var(--ds-ease-out);
}
@keyframes msgConfirm {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* Повідомлення зникло з відповіді сервера (видалено): згасає на місці,
   вузол знімає transport.js mergeThread по animationend. */
.message--gone {
  animation: msgGone var(--ds-motion) var(--ds-ease-out) forwards;
  pointer-events: none;
}
@keyframes msgGone {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(0.375rem); }
}
@media (prefers-reduced-motion: reduce) {
  .msg-appear { animation: none; }
  .message--confirmed .message__time,
  .message--confirmed .message__status { animation: none; }
  .message--gone { animation: none; opacity: 0; }
}

/* Typing indicator (P2P) */
.typing-indicator {
  padding: var(--ds-space-2) var(--ds-space-4);
  color: var(--ds-text-secondary);
  font-size: var(--ds-meta-size);
}
.typing-dots { display: inline-flex; gap: 0.1875rem; }
.typing-dots span {
  width: 0.3125rem; height: 0.3125rem;
  background: var(--ds-text-secondary);
  border-radius: 50%;
  animation: typingBounce 1.2s ease-in-out infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.3; }
  30% { transform: translateY(-0.25rem); opacity: 1; }
}

/* AI typing indicator */
.ai-typing::after {
  content: "";
  display: inline-block;
  width: 0.375rem;
  height: 0.375rem;
  background: var(--ds-text-secondary);
  border-radius: 50%;
  animation: aiDots 1s ease-in-out infinite;
  margin-inline-start: 0.125rem;
}
@keyframes aiDots {
  0%, 100% { opacity: 0.2; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1.2); }
}

/* Icon tap — scale + stroke draw on click. cubic-bezier тут і нижче —
   легасі --ease-spring; ds-тір має лише ease-out/ease-in-out, spring-кривої
   нема, тому значення інлайновано дослівно з мосту src/css.rs. */
.icon-tap { transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); }
.icon-tap:active { transform: scale(0.85); }
.icon-tap:hover { transform: scale(1.1); }
.icon-tap svg {
  transition: stroke-dashoffset 0.3s ease;
}
.icon-tap:active svg {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: iconDraw 0.4s ease forwards;
}
@keyframes iconDraw {
  to { stroke-dashoffset: 0; }
}

/* Icon pulse — breathing glow for online status */
.icon-pulse { animation: iconPulse 2s ease-in-out infinite; }
@keyframes iconPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* Icon bounce — pop for unread badge */
.icon-bounce { animation: badgePop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes badgePop {
  0% { transform: scale(0); }
  60% { transform: scale(1.2); }
  100% { transform: scale(1); }
}

/* Icon float — gentle hover for empty states */
.icon-float { animation: iconFloat 3s ease-in-out infinite; }
/* iconFloat defined in icon-animations section */

/* Status dot */
.status-dot {
  display: inline-block;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--ds-feedback-success);
  margin-inline-end: 0.25rem;
  vertical-align: middle;
}

/* Icon in tabs — align with text */
.dialog-tab svg {
  vertical-align: -0.1875rem;
  margin-inline-end: 0.125rem;
}

/* Empty state icon */
.empty-state__icon {
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-3);
}
.empty-state__icon svg {
  width: 3rem;
  height: 3rem;
}

/* Message action buttons — smooth reveal */
.message__actions {
  transition: opacity 0.15s var(--ds-ease-out), transform 0.15s var(--ds-ease-out);
  transform: translateY(0.125rem);
}
.message:hover .message__actions {
  transform: translateY(0);
}

/* Badge in chat header — subtle entrance */
.chat-header__badge svg {
  width: 0.75rem;
  height: 0.75rem;
  vertical-align: -0.0625rem;
  margin-inline-end: 0.125rem;
}

/* ═══════════════════════════════════════════
   LINKIFIED TEXT
   ═══════════════════════════════════════════ */

.message__text a {
  color: var(--ds-accent);
  text-decoration: none;
  border-bottom: 0.0625rem solid var(--ds-accent-border);
  transition: border-color var(--ds-motion-fast);
}

.message__text a:hover {
  border-bottom-color: var(--ds-accent);
}

.message--incoming .message__text a {
  color: var(--tg);
  border-bottom-color: color-mix(in srgb, var(--tg) 20%, transparent);
}

.message--incoming .message__text a:hover {
  border-bottom-color: var(--tg);
}

/* ═══════════════════════════════════════════
   MARKDOWN IN MESSAGES
   ═══════════════════════════════════════════ */

.msg-code-block {
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: var(--ds-radius-sm);
  padding: var(--ds-space-2) var(--ds-space-3);
  margin: var(--ds-space-2) 0;
  overflow-x: auto;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-label-size);
  line-height: 1.6;
  white-space: pre;
}

.msg-code-block code {
  background: none;
  padding: 0;
  border: none;
}

.msg-code-inline {
  background: var(--ds-surface-base);
  border: 0.0625rem solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-xs);
  padding: 0.0625rem 0.3125rem;
  font-family: var(--ds-font-mono);
  font-size: 0.9em;
}

.msg-quote {
  background: var(--ds-surface-overlay);
  border-radius: var(--ds-radius-sm);
  padding: var(--ds-space-1) var(--ds-space-3);
  margin: var(--ds-space-1) 0;
  color: var(--ds-text-secondary);
  font-style: italic;
}

.message__text strong {
  font-weight: 600;
}

.message__text del {
  opacity: 0.6;
}

/* ═══════════════════════════════════════════
   IMAGE PREVIEW IN MESSAGES
   ═══════════════════════════════════════════ */

.msg-img-preview {
  display: block;
  max-width: 100%;
  max-height: 18.75rem;
  border-radius: var(--ds-radius-sm);
  margin-top: var(--ds-space-2);
  cursor: pointer;
  object-fit: contain;
  background: var(--ds-surface-base);
}

.msg-img-preview:hover {
  opacity: 0.9;
}

/* ═══════════════════════════════════════════
   VIEW TRANSITIONS
   ═══════════════════════════════════════════ */

@keyframes fade-in {
  from { opacity: 0; transform: translateY(0.25rem); }
}
@keyframes fade-out {
  to { opacity: 0; transform: translateY(-0.25rem); }
}

::view-transition-old(root) {
  animation: 150ms cubic-bezier(0.4, 0, 0.2, 1) both fade-out;
}
::view-transition-new(root) {
  animation: 200ms cubic-bezier(0.4, 0, 0.2, 1) both fade-in;
}

/* ═══════════════════════════════════════════
   MICROINTERACTIONS — spring physics, entry animations
   ═══════════════════════════════════════════ */

@keyframes spring-in {
  0%   { opacity: 0; transform: scale(0.92) translateY(0.5rem); }
  60%  { transform: scale(1.01) translateY(-0.0625rem); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

@keyframes spring-pop {
  0%   { transform: scale(0.8); }
  60%  { transform: scale(1.05); }
  100% { transform: scale(1); }
}

@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

/* Skeleton screens */
.skeleton {
  background: linear-gradient(90deg, var(--ds-surface-overlay) 25%, var(--ds-surface-hover) 50%, var(--ds-surface-overlay) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
  border-radius: var(--ds-radius-sm);
}

.skeleton--text { height: 0.75rem; width: 60%; margin-bottom: var(--ds-space-2); }
.skeleton--text-short { height: 0.75rem; width: 40%; margin-bottom: var(--ds-space-2); }
.skeleton--avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; flex-shrink: 0; }
.skeleton--row { display: flex; gap: var(--ds-space-3); align-items: center; padding: var(--ds-space-3); }
.skeleton--row-content { flex: 1; display: flex; flex-direction: column; gap: var(--ds-space-1); }

/* Dialog list items — entry.
   Was: `spring-in 350ms` on a bouncy curve with a per-row delay ramp up to
   300ms, layered on top of the earlier `dialogRevealIn` (which also animated
   `filter: blur()` — off the compositor). The list re-renders on every SSE
   nudge, so that decoration replayed on every incoming message: rows bounced
   and greyed while the operator was trying to read them, and the last rows
   arrived a third of a second late.
   Now: one 120ms fade, no stagger, no bounce, opacity only. The list is data
   to be read, not an entrance. */
.dialog-item {
  animation: dialogFadeIn 120ms var(--ds-ease-out) both;
}

@keyframes dialogFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Press feedback on the most-tapped target in the app. Fires on pointer-down
   (that is what :active is), so the row answers before the request does.
   Ф2a: І фон, І transform в одному transition — версія лише з transform
   (як тут стояло) глушила плавність :active { transform: scale(.99) }
   РАЗОМ із переходом фону рядка; версія з @layer overrides мала обидва
   і, як найвищий шар, завжди перемагала. Utilities > components, тож
   мердж лишається саме тут, інакше рядок втратить background-transition. */
.dialog-item {
  transition:
    background var(--ds-motion-fast) var(--ds-ease-out),
    transform var(--ds-motion-fast) var(--ds-ease-out);
}

.dialog-item:active {
  transform: scale(0.99);
}

/* Messages — slide in */
/* No blanket animation on `.message`. A bouncy `spring-in 250ms` used to sit
   here and applied to EVERY bubble on EVERY render — it overrode the careful
   `.msg-appear` system above (which distinguishes a newly arrived message from
   a thread being re-rendered) and made a live-updating thread wobble. New
   messages animate through `.msg-appear`; the rest just appear. */

/* Buttons — spring tap */
.btn, .btn--small, .btn--glyph {
  transition: transform 120ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.btn--small:active, .btn--glyph:active {
  transform: scale(0.94);
}
/* Плоский .btn: Ф2a — з @layer overrides `.btn:active { transform:
   translateY(0.0312rem) }` перебивав pružну scale(.94) вище незалежно від
   шару. .btn--small/.btn--glyph лишають пружину; лише «голий» .btn —
   тихіший 0.0312rem натиск (Linear-стриманість). */
.btn:active { transform: translateY(0.0312rem); }

/* Avatars — subtle hover lift */
.avatar, [class*="avatar"] {
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 200ms ease;
}
.avatar:hover, [class*="avatar"]:hover {
  transform: scale(1.05);
}

/* Interactive icons — rotate on hover */
.icon-hover-rotate {
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.icon-hover-rotate:hover {
  transform: rotate(15deg);
}

/* FAB — spring pop-in */
.fab, .scroll-bottom-fab {
  animation: spring-pop 350ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* Loading spinners — smooth */
@keyframes pulse-dot {
  0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
  40% { transform: scale(1); opacity: 1; }
}

.typing-indicator {
  display: inline-flex;
  gap: 0.25rem;
  align-items: center;
}
.typing-indicator span {
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--ds-text-muted);
  animation: pulse-dot 1.4s ease-in-out infinite both;
}
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
.typing-indicator span:nth-child(2) { animation-delay: 0.16s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.32s; }

/* ═══════════════════════════════════════════
   GLOBAL SPEED SWEEP — Ф2a, з @layer overrides
   backdrop-filter blur — найгірший ворог швидкості: кожна матова
   поверхня перемальовує blur на КОЖНОМУ кадрі скролу/кейстроку. Наші
   поверхні мають справжній суцільний колір (токени), тож зняття
   фросту нічого не втрачає, крім гімміку. Це утиліти-шар — вищий за
   всі @layer components, де ще лишились 17 `backdrop-filter: blur()`
   (10-layout, 30-chat-messages, 40-composer, 55-login-overlays,
   65-feedback, 70-widgets) — тому `!important`, потрібний у
   @layer overrides, тут ЗАЙВИЙ: шар сам по собі виграє незалежно
   від специфічності. Без !important лічильник лінтера падає. */
*, *::before, *::after {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* Матові поверхні фейково давали глибину через `…92%, transparent` +
   blur. Без фросту робимо ключовий хром повністю суцільним, щоб нічого
   не просвічувало. */
/* .top-bar і .popover випали зі списку — мертві класи: 0 хітів у
   templates/, src/, static/*.js, crates/ui-*. .input-area отримав своє
   рішення прямо в 40-composer.css (вже без color-mix/blur). */
.dialog-search, .chat-header, .settings-drawer, .settings-drawer__header,
.preview-header, .modal, .menu, .context-menu {
  background-color: var(--ds-surface-raised);
}

/* Курація `transition: all` (80 вживань) → лише компоситор-дешеві
   властивості. `all` перезапускає transition для КОЖНОЇ зміненої
   властивості, включно з layout (width/height) = reflow-джанк.
   Слайд-дровери/панелі йдуть через @keyframes, не transition — не
   зачеплені. */
*, *::before, *::after {
  transition-property: transform, opacity, background-color, border-color, color, fill, stroke, box-shadow, outline-color;
}
} /* закриває @layer utilities */

/* ═══════════════════════════════════════════
   REDUCED MOTION — Stepanov: "Don't pay for what you don't use."
   Було ДВА окремих блоки (тут і в MESSAGE HOVER ACTIONS вище), кожен
   з !important — інакше *,*::before,*::after (специфічність 0) програє
   будь-якому класу з transition/animation У ТОМУ Ж шарі utilities
   (.btn--small:active, .dialog-item тощо). Ф2b: винесено НЕПОШАРОВАНИМ
   (поза @layer) в кінець файлу — непошарований author CSS переважає
   БУДЬ-ЯКИЙ layered rule незалежно від специфічності (CSS Cascade
   Layers §6), тому !important тут уже зайвий. Один блок замість двох,
   об'єднані властивості обох. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
    transition-duration: 0.01ms;
    scroll-behavior: auto;
  }
}

/* Переклад листа (render.js, RichTr). Довгий лист читається перекладом:
   звичайний розмір, без курсиву, абзаци збережені. Оригінал згорнуто під
   кнопкою «Оригінал» у рядку над перекладом. */
.message__translation--pending { font-style: normal; color: var(--ds-text-muted); border-top: 0; }
/* `font-variant-emoji: emoji` (30-chat-messages.css) малює цифри значками
   клавіш: номер сертифіката в листі розсипався на кольорові квадрати. */
.message__translation--rich {
  font-variant-emoji: normal;
  font-size: inherit; font-style: normal; color: var(--ds-text-primary);
  margin: 0 0 0.5rem; padding: 0 0 0.5rem; border-top: 0;
  border-bottom: 0.0625rem solid var(--ds-border-subtle);
}
/* Вкладки «Оригінал | Русский» над листом: сегментований перемикач. */
.message__translation-tabs {
  display: inline-flex; gap: 0.125rem; padding: 0.125rem; margin-bottom: 0.625rem;
  background: var(--ds-surface-hover); border-radius: 999px;
  font-style: normal;
}
.message__translation-tab {
  display: inline-flex; align-items: center; gap: 0.375rem;
  min-height: 2rem; padding: 0 0.875rem;
  font: inherit; font-size: var(--ds-meta-size); color: var(--ds-text-muted);
  background: transparent; border: 0; border-radius: 999px; cursor: pointer;
  transition: background-color 150ms ease, color 150ms ease;
}
.message__translation-tab[aria-selected="true"] {
  color: var(--ds-text-primary); background: var(--ds-surface-raised);
  box-shadow: 0 0.0625rem 0.125rem rgba(0,0,0,.25);
}
.message__translation-tab:focus-visible { outline: 0.125rem solid var(--ds-accent); outline-offset: 0.0625rem; }
.message__translation-code { font-size: 0.75em; letter-spacing: 0.04em; opacity: 0.7; }
@media (pointer: coarse) { .message__translation-tab { min-height: 2.75rem; } }
@media (prefers-reduced-motion: reduce) { .message__translation-tab { transition: none; } }
.message__translation-body { white-space: pre-wrap; overflow-wrap: anywhere; }
.message__text--folded:not(.message__text--shown) { display: none; }

/* Акаунт Telegram для відправки (шапка чату). Дотик не менше 44 px. */
.tg-account-pick {
  min-height: 2.75rem;
  max-width: 11rem;
  padding: 0 var(--ds-space-2);
  font: inherit;
  font-size: var(--ds-meta-size);
  color: var(--ds-text-secondary);
  background: var(--ds-surface-overlay);
  border: 0.0625rem solid var(--ds-border-strong);
  border-radius: 0.375rem;
  text-overflow: ellipsis;
}

/* ── 97-orders.css ── */
/* ЗАМОВЛЕННЯ WooCommerce (landmann)
 *
 * У шарі components, але у вихідному аркуші стоїть ПІСЛЯ utilities.
 * Порядок між різними шарами не важить, порядок усередині шару —
 * важить, тому цей файл мусить лишатись останнім блоком components
 * перед 99-overrides.css.
 *
 * Порядок правил усередині шару = порядок у старому static/style.css
 * (він лежить поруч як style.css.pre-split). Не переставляти: у CSS
 * при рівній специфічності виграє останнє правило. Файли зшиваються
 * у /static/css/style.css за іменами, тому номер у назві = позиція.
 */

/* ── Замовлення WooCommerce (landmann) ───────────────────────────────
   Панель живе на місці списку діалогів. Ролі кольорів ті самі, що всюди:
   accent = робочий стан, warning = чекає уваги, danger = зірвалось. */
@layer components {
  /* Рядок-вхід над списком: «Замовлення», «Нова кімната». Другий такий
     самий рядок став приводом дати правилу спільне імʼя замість копії. */
  .list-entry,
  .orders-entry {
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
    width: 100%; padding: 0.625rem 0.75rem;
    background: var(--ds-surface-raised);
    border: none; border-bottom: 0.0625rem solid var(--ds-border-subtle);
    color: var(--ds-text-secondary);
    font-family: var(--ds-font-mono);
    font-size: 0.6875rem; letter-spacing: 0.1em; text-transform: uppercase;
    cursor: pointer; text-align: left;
  }
  .list-entry:hover,
  .orders-entry:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }
  .list-entry:focus-visible,
  .orders-entry:focus-visible { outline: 0.125rem solid var(--ds-accent); outline-offset: -0.125rem; }

  /* Форма створення кімнати розкривається під рядком-входом. */
  .room-new-form {
    display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem;
    background: var(--ds-surface-raised);
    border-bottom: 0.0625rem solid var(--ds-border-subtle);
  }
  .room-new-form[hidden] { display: none; }
  .room-new-form .input { flex: 1; min-width: 0; }
  .orders-entry__badge,
  .orders-head__badge {
    font-family: var(--ds-font-mono);
    /* --ds-on-accent найближча роль «темний текст на кольоровій плашці»;
       точного --ds-on-warning нема. Було фіксовано #1a0f05 в обох темах;
       у dark --ds-on-accent = #06140d (майже те саме), у light = #ffffff
       (біле на амбер #f0883e — контраст гірший за фіксований темний).
       Задумана зміна, задокументовано у звіті. */
    background: var(--ds-feedback-warning); color: var(--ds-on-accent);
    padding: 0 0.375rem; min-width: 1.125rem; text-align: center;
    font-weight: 600; font-variant-numeric: tabular-nums;
  }
  /* Бейдж не згасає сам: він знімається лише коли картку відкрили. */
  .orders-entry[data-alarm="1"] .orders-entry__badge { animation: orders-alarm 1.6s ease-in-out infinite; }
  @keyframes orders-alarm { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
  @media (prefers-reduced-motion: reduce) {
    .orders-entry[data-alarm="1"] .orders-entry__badge { animation: none; }
  }

  .orders-pane { display: flex; flex-direction: column; height: 100%; min-height: 0; }
  .orders-head {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.625rem 0.75rem;
    background: var(--ds-surface-raised);
    border-bottom: 0.0625rem solid var(--ds-border-subtle);
    font-family: var(--ds-font-mono);
    font-size: 0.6875rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ds-text-secondary);
  }
  .orders-head__back {
    margin-left: auto; background: transparent;
    border: 0.0625rem solid var(--ds-border-default);
    color: var(--ds-text-secondary);
    font: inherit; padding: 0.25rem 0.5rem; cursor: pointer;
  }
  .orders-head__back:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }
  .orders-body { overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; }
  .orders-empty {
    padding: 1.5rem 0.75rem; color: var(--ds-text-muted);
    font-size: 0.8125rem; line-height: 1.5;
  }

  .order-card {
    background: var(--ds-surface-raised);
    border: 0.0625rem solid var(--ds-border-subtle);
    padding: 0.625rem; cursor: pointer;
  }
  .order-card:hover { border-color: var(--ds-border-strong); }
  /* Непрочитане = тональний крок поверхні, без кольорових кромок. */
  .order-card--unseen { background: var(--ds-surface-overlay); }
  .order-card__top { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.375rem; }
  .order-card__num {
    font-family: var(--ds-font-mono);
    font-weight: 600; color: var(--ds-text-primary); font-size: 0.8125rem;
  }
  .order-status {
    font-family: var(--ds-font-mono);
    font-size: 0.625rem; letter-spacing: 0.06em; text-transform: uppercase;
    padding: 0.0625rem 0.375rem; white-space: nowrap;
  }
  /* Плашки-тінти через color-mix() на семантичних токенах, а не hex:
     warning/danger незмінні в обох темах (світла тема їх не перевизначає
     у src/css.rs) — тінт співпадає з попереднім rgba пікселем-у-піксель.
     accent (work) У СВІТЛІЙ темі стає --p-green-700 (глибший, для AA),
     а не фіксований #2ea66e, що й раніше рендерився в rgba(46,166,110,.16)
     незалежно від теми — задумана зміна: у dark кольори збігаються,
     у light тінт стає темнішим (readable, не гірше). idle: замість
     rgba(255,255,255,.06) (у світлій темі білий тінт на кремовому тлі
     не читався) — --ds-border-subtle, той самий рівень «трохи фактури»,
     теми-коректний. */
  .order-status--wait { background: color-mix(in srgb, var(--ds-feedback-warning) 16%, transparent); color: var(--ds-feedback-warning); }
  .order-status--work { background: color-mix(in srgb, var(--ds-accent) 16%, transparent); color: var(--ds-accent); }
  .order-status--dead { background: color-mix(in srgb, var(--ds-feedback-danger) 16%, transparent); color: var(--ds-feedback-danger); }
  .order-status--idle { background: var(--ds-border-subtle); color: var(--ds-text-muted); }

  .order-card__items { list-style: none; margin: 0 0 0.375rem; padding: 0; font-size: 0.78rem; color: var(--ds-text-secondary); }
  .order-card__items li { display: flex; gap: 0.5rem; justify-content: space-between; padding: 0.0625rem 0; }
  .order-card__items b { font-family: var(--ds-font-mono); font-weight: 400; color: var(--ds-text-muted); white-space: nowrap; }
  .order-card__total {
    font-family: var(--ds-font-mono);
    font-size: 0.9375rem; font-weight: 600; color: var(--ds-text-primary);
    margin-bottom: 0.5rem;
  }
  .order-card__meta { margin: 0 0 0.5rem; font-size: 0.75rem; }
  .order-row { display: flex; gap: 0.5rem; padding: 0.0625rem 0; }
  .order-row dt { flex: 0 0 5.5rem; color: var(--ds-text-muted); }
  .order-row dd { margin: 0; color: var(--ds-text-secondary); overflow-wrap: anywhere; }
  .order-row a { color: var(--ds-accent); text-decoration: none; }
  .order-row a:hover { text-decoration: underline; }

  .order-card__actions { display: flex; flex-wrap: wrap; gap: 0.25rem; }
  .order-btn {
    font-size: 0.7rem; line-height: 1; padding: 0.375rem 0.5rem;
    background: transparent; border: 0.0625rem solid var(--ds-border-default);
    color: var(--ds-text-secondary); cursor: pointer; text-decoration: none;
    font-family: inherit; white-space: nowrap;
  }
  .order-btn:hover { background: var(--ds-surface-hover); color: var(--ds-text-primary); }
  .order-btn:focus-visible { outline: 0.125rem solid var(--ds-accent); outline-offset: 0.0625rem; }
}
