/* ============================================================================
 * Chat Thread Panel — Tree-structured thread UI (Reddit/YouTube style)
 *
 * Spec: docs/TZ_CHAT_PANEL_TREE_UIUX.md
 * DOM contract: chat-node > (rail + bubble + toggle + children)
 *
 * Namespace: chat-thread-*, chat-node-*, chat-bubble-*
 * Tokens: 100% design system. Zero hardcoded colors.
 * ============================================================================ */

/* === Chat-level CSS variables === */
:root {
    --chat-root-gap: 12px;
    --chat-branch-gap: 6px;
    --chat-depth-step: 18px;
    --chat-depth-cap: 4;
}

/* === Thread list === */

.chat-thread {
    list-style: none;
    /* No top padding: SUBJECT banner is `position: sticky; top: 0`
       and a sticky element pins to its scroll container's content box.
       Top padding pushes the pin point down, leaving a transparent
       strip between the chat panel header and the banner where
       scrolled messages bleed through. Flushing the top makes the
       banner butt against the header — no see-through gap. */
    padding: 0 var(--space-2) var(--space-2);
    margin: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    flex: 1;
}

/* Restore breathing room for the FIRST root chat-node when there is
   no SUBJECT banner above it (banner is rendered inside .chat-node
   in ChatThreadPanel._renderNode). Without this, a subject-less
   first message hugs the chat header. With a banner present, the
   banner has its own padding so this rule doesn't add a visible gap. */
.chat-thread > .chat-node:first-child:not(:has(> .chat-bubble__subject)) {
    margin-top: var(--space-2);
}

/* === Node (single message + children) === */

.chat-node {
    position: relative;
    margin-bottom: var(--chat-branch-gap);
}

/* Root nodes get larger gap */
.chat-thread > .chat-node {
    margin-bottom: var(--chat-root-gap);
}

/* Separator between root nodes */
.chat-thread > .chat-node + .chat-node {
    padding-top: var(--chat-root-gap);
    border-top: 1px solid var(--border-primary);
}

/* === Tree connectors — drawn by JS via SVG overlay ===
 * See ChatThreadPanel._drawTreeConnectors() — measures real avatar
 * coordinates via getBoundingClientRect() and renders SVG paths.
 * No CSS positioning math. */

.chat-thread-svg {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    z-index: 0;
    overflow: visible;
}

.chat-thread-svg path {
    fill: none;
    stroke: var(--border-primary);
    stroke-width: 1;
}

.chat-thread {
    position: relative;
}

/* === Bubble (message content) === */

.chat-bubble {
    display: block;
    padding: var(--space-1);
    position: relative;
    /* Override the legacy chat-panel.css `max-width: 85%` that still
       cascades into the tree-style bubbles and reserves a ~15% gutter
       on the right. The chat panel container already constrains width. */
    max-width: none;
}

/* Subject lives ABOVE the avatar+body row as a full-width title
   banner (UX 2026-05-11). The row reproduces the legacy avatar+body
   flex so the avatar stays aligned with the author header line. */
.chat-bubble__row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}

/* Avatar — smaller, aligned with header text baseline */
.chat-bubble__avatar {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 7px;
    font-weight: var(--font-weight-semibold);
    color: white;
    flex-shrink: 0;
    z-index: 2; /* above rail */
    position: relative;
    align-self: flex-start;
    margin-top: 0;
}

/* Only the agent's OWN avatar — strict direct-child chain so nested
   reply bubbles from human users inside an agent thread keep their
   round avatars. UX 2026-05-11. */
.chat-node--agent > .chat-bubble > .chat-bubble__row > .chat-bubble__avatar {
    border-radius: var(--radius-sm);
}

/* Body — fill the row minus avatar + gap. The earlier max-width: 78%
   left a ~22% blank gutter on the right of every bubble; pointless
   when the chat panel is already constrained by its container width. */
.chat-bubble__body {
    flex: 1;
    min-width: 0;
}

/* Header — same height as avatar for perfect alignment */
.chat-bubble__header {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-bottom: 1px;
    min-height: 18px;
    line-height: 18px;
}

.chat-bubble__author {
    font-size: 11px;
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-node--agent .chat-bubble__author {
    color: var(--brand-primary);
}

.chat-bubble__time {
    font-size: 10px;
    color: var(--text-tertiary);
    white-space: nowrap;
    flex-shrink: 0;
}

/* Header-level thread actions — Create Activity / Close discussion.
   Promoted from the bottom action-bar so the user spots them on the
   root immediately. Right-aligned, icon-only, become visible on bubble
   hover (same trigger as the bottom action bar). */
.chat-bubble__header-actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.chat-bubble:hover .chat-bubble__header-actions {
    opacity: 1;
}

/* Inside the SUBJECT banner the actions are always visible — the
   banner is lifted out of the bubble (sticky), so `chat-bubble:hover`
   doesn't fire when the user hovers the pinned subject. UX 2026-05-11. */
.chat-bubble__subject .chat-bubble__header-actions {
    opacity: 1;
}

.chat-bubble__header-btn {
    background: none;
    border: none;
    padding: 2px;
    border-radius: var(--radius-sm);
    color: var(--text-tertiary);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    transition:
        color var(--transition-fast),
        background-color var(--transition-fast);
}

.chat-bubble__header-btn:hover {
    color: var(--text-primary);
    background-color: var(--bg-hover);
}

.chat-bubble__header-btn--danger {
    color: var(--color-error);
}

.chat-bubble__header-btn--danger:hover {
    color: var(--color-error);
    background-color: rgba(var(--color-error-rgb, 239, 68, 68), 0.1);
}

.chat-bubble__header-btn .material-icons {
    font-size: 14px;
}

.chat-bubble__edited {
    font-size: 10px;
    color: var(--text-tertiary);
    font-style: italic;
}

/* R20 soft-deleted message body. */
.chat-bubble__deleted {
    color: var(--text-tertiary);
    font-style: italic;
    user-select: none;
}

.chat-reply-bar__text--deleted {
    color: var(--text-tertiary);
    font-style: italic;
}

/* Text */
.chat-bubble__text {
    font-size: var(--font-size-xs);
    line-height: 1.45;
    color: var(--text-primary);
    white-space: pre-wrap;
    word-wrap: break-word;
    margin-bottom: 1px;
}

/* Вложения реплики — СТРОКАМИ, не кнопками (канон PO о перечнях).
   [2026-09-08] Р5, пункты 2/4 ТЗ docs/TZ_CHAT_FIXES_2026-09-06.md §6: ответ
   сверх предела и крупные версии истории правок хранятся вложением, и панель
   обязана их показывать — до сих пор их не было видно вовсе.
   Chat: chat-p5-attachments-20260908. */
.chat-attachments {
    list-style: none;
    margin: 4px 0 2px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.chat-attachment {
    display: flex;
    align-items: baseline;
    gap: var(--space-6);
    padding: 2px 4px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--font-size-xs);
}

.chat-attachment:hover {
    background-color: var(--bg-hover);
}

.chat-attachment__name {
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Кнопка остановки идущего хода агента — у самого индикатора работы.
   [2026-09-08] Р6, пункт 1 ТЗ docs/TZ_CHAT_FIXES_2026-09-06.md §7: остановить
   работающего агента было нечем. Chat: chat-p6-lifecycle-20260908. */
.chat-typing-interrupt {
    margin-left: auto;
    padding: 2px 8px;
    border: 1px solid var(--border-color, var(--text-secondary));
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--font-size-xs);
    cursor: pointer;
}

.chat-typing-interrupt:hover:not(:disabled) {
    background-color: var(--bg-hover);
}

.chat-typing-interrupt:disabled {
    opacity: 0.6;
    cursor: default;
}

.chat-typing-note {
    margin-left: var(--space-6);
    color: var(--text-secondary);
    font-size: var(--font-size-xs);
}

/* Предупреждение о неполноте списка — на месте действия, не модалкой.
   [2026-09-08] Chat: chat-p5-attachments-20260908. */
.chat-attachment--warn .chat-attachment__meta {
    color: var(--color-warning, var(--text-secondary));
}

.chat-attachment__meta {
    color: var(--text-secondary);
    white-space: nowrap;
}

/* Own messages — no background tint. Visual ownership is already
   conveyed by the avatar colour and the author name; the tinted block
   added noise to the feed (UX 2026-05-11). */
.chat-node--own > .chat-bubble {
    background-color: transparent;
}

/* Failed */
.chat-node--failed > .chat-bubble {
    border-left: 2px solid var(--color-error);
    padding-left: var(--space-2);
}

/* Pending */
.chat-node--pending > .chat-bubble {
    opacity: 0.55;
}

/* === Markdown rendering inside bubble text === */

.chat-bubble__text p {
    margin: 0 0 4px 0;
}
.chat-bubble__text p:last-child {
    margin-bottom: 0;
}
.chat-bubble__text h1,
.chat-bubble__text h2,
.chat-bubble__text h3,
.chat-bubble__text h4,
.chat-bubble__text h5,
.chat-bubble__text h6 {
    margin: 6px 0 3px 0;
    font-weight: var(--font-weight-semibold);
    font-size: 13px;
}
.chat-bubble__text ul,
.chat-bubble__text ol {
    margin: 2px 0;
    padding-left: 18px;
}
.chat-bubble__text li {
    margin: 0;
}
.chat-bubble__text code {
    background-color: var(--bg-tertiary);
    padding: 1px 4px;
    border-radius: 3px;
    font-family: var(--font-mono, 'Menlo', 'Monaco', 'Consolas', monospace);
    font-size: 11px;
    color: var(--text-primary);
}
.chat-bubble__text pre {
    background-color: var(--bg-tertiary);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    overflow-x: auto;
    margin: 4px 0;
}
.chat-bubble__text pre code {
    background: none;
    padding: 0;
    font-size: 11px;
    display: block;
    white-space: pre;
}
.chat-bubble__text blockquote {
    border-left: 2px solid var(--border-primary);
    padding-left: var(--space-2);
    margin: 4px 0;
    color: var(--text-secondary);
}
.chat-bubble__text a {
    color: var(--brand-primary);
    text-decoration: underline;
}
.chat-bubble__text a:hover {
    color: var(--brand-primary-hover);
}
.chat-bubble__text hr {
    border: none;
    border-top: 1px solid var(--border-primary);
    margin: 6px 0;
}
.chat-bubble__text table {
    border-collapse: collapse;
    margin: 4px 0;
}
.chat-bubble__text th,
.chat-bubble__text td {
    border: 1px solid var(--border-primary);
    padding: 2px 6px;
    font-size: 11px;
}

/* === Inline Show more / Show less === */

.chat-bubble__expand {
    background: none;
    border: none;
    padding: 0;
    color: var(--brand-primary);
    font-size: inherit;
    font-weight: var(--font-weight-medium);
    cursor: pointer;
    display: inline;
    vertical-align: baseline;
}

.chat-bubble__expand:hover {
    color: var(--brand-primary-hover);
    text-decoration: underline;
}

/* === Actions === */

.chat-bubble__actions {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.chat-bubble:hover .chat-bubble__actions {
    opacity: 1;
}

.chat-bubble__action {
    background: none;
    border: none;
    padding: 1px var(--space-1);
    border-radius: var(--radius-sm);
    font-size: 11px;
    color: var(--text-tertiary);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    transition:
        color var(--transition-fast),
        background-color var(--transition-fast);
}

.chat-bubble__action:hover {
    color: var(--text-primary);
    background-color: var(--bg-hover);
}

.chat-bubble__action--danger {
    color: var(--color-error);
}

.chat-bubble__action--danger:hover {
    color: var(--color-error);
    background-color: rgba(var(--color-error-rgb, 239, 68, 68), 0.1);
}

.chat-bubble__action .material-icons {
    font-size: 13px;
}

/* === Menu (by button, not hover) FR-5 === */

.chat-bubble__menu-trigger {
    background: none;
    border: none;
    padding: 2px;
    border-radius: var(--radius-sm);
    color: var(--text-tertiary);
    cursor: pointer;
    display: flex;
    align-items: center;
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.chat-node:hover > .chat-bubble .chat-bubble__menu-trigger {
    opacity: 1;
}

.chat-bubble__menu-trigger:hover {
    background-color: var(--bg-hover);
    color: var(--text-primary);
}

.chat-bubble__menu-trigger .material-icons {
    font-size: 16px;
}

.chat-bubble__menu {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    z-index: var(--z-dropdown);
    padding: var(--space-1) 0;
    min-width: 140px;
}

.chat-bubble__menu--open {
    display: block;
}

.chat-bubble__menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    font-size: 12px;
    color: var(--text-primary);
    cursor: pointer;
    background: none;
    border: none;
    width: 100%;
    text-align: left;
    transition: background-color var(--transition-fast);
}

.chat-bubble__menu-item:hover {
    background-color: var(--bg-hover);
}

.chat-bubble__menu-item .material-icons {
    font-size: 16px;
    color: var(--text-secondary);
}

.chat-bubble__menu-item--danger {
    color: var(--color-error);
}

.chat-bubble__menu-item--danger .material-icons {
    color: var(--color-error);
}

/* === Thread toggle (§FR-3) — round chevron on connector line ===
   The wrap pulls the button left into the avatar column so its centre
   lands on the SVG connector line drawn through avatar.cx. The button
   itself is solid bg + z-index above the SVG, so the line visually
   "passes through" it without crossing. UX 2026-05-11. */

.chat-bubble__toggle-replies-wrap {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    /* avatar (18px) + row gap (var(--space-2) ≈ 8px) = ~26px shift to
       reach avatar.left from body.left; minus half the button width
       (9px) lands the centre on avatar.cx. Keep this in sync with the
       avatar size in `.chat-bubble__avatar`. */
    margin-left: -27px;
}

.chat-bubble__toggle-replies {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 1px solid var(--brand-primary);
    background: var(--bg-primary);
    color: var(--brand-primary);
    cursor: pointer;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    position: relative;
    z-index: 3; /* above .chat-thread-svg */
    transition:
        background-color var(--transition-fast),
        color var(--transition-fast);
}

.chat-bubble__toggle-replies:hover {
    background: var(--brand-primary);
    color: var(--bg-primary);
}

.chat-bubble__toggle-replies .material-icons {
    font-size: 14px;
    line-height: 1;
    /* Smooth 180° flip on collapse/expand. transform-origin defaults to
       centre, so the chevron rotates in place. */
    transform: rotate(0deg);
    transition: transform 220ms ease;
}

.chat-bubble__toggle-replies[aria-expanded='false'] .material-icons {
    transform: rotate(180deg);
}

.chat-bubble__toggle-replies-count {
    font-size: 11px;
    color: var(--brand-primary);
    font-weight: var(--font-weight-medium);
    white-space: nowrap;
}

/* === Children container === */

.chat-node__children {
    list-style: none;
    padding: 0;
    margin: 0;
    padding-left: var(--chat-depth-step);
}

/* Depth cap: max visual indent — all ul's from cap onward have zero indent */
.chat-node__children--capped,
.chat-node__children--capped .chat-node__children {
    padding-left: 0;
}

/* Collapsed children */
.chat-node__children--collapsed {
    display: none;
}

/* Continued thread label (beyond depth cap) */
.chat-node__continued {
    font-size: 10px;
    color: var(--text-tertiary);
    font-style: italic;
    padding-left: 28px;
    margin-bottom: var(--chat-branch-gap);
}

/* === Orphan badge === */

/* Orphan badge: text comes from the locale via JS so it's translatable
   (was hard-coded English `orphan reply`). The JS pipeline puts the
   localized string in `.chat-node--orphan`'s data-orphan-label, and the
   CSS pulls it via attr(). UX 2026-05-11. */
.chat-node--orphan > .chat-bubble::before {
    content: attr(data-orphan-label);
    font-size: 9px;
    color: var(--text-tertiary);
    background-color: var(--bg-tertiary);
    padding: 0 4px;
    border-radius: var(--radius-sm);
    position: absolute;
    top: -6px;
    right: 0;
}

/* === Failed retry === */

.chat-bubble__retry {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: 2px;
    font-size: 11px;
    color: var(--color-error);
}

.chat-bubble__retry-btn {
    padding: 1px var(--space-2);
    border: 1px solid var(--color-error);
    border-radius: var(--radius-sm);
    background: none;
    color: var(--color-error);
    font-size: 11px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

.chat-bubble__retry-btn:hover {
    background-color: var(--color-error);
    color: var(--text-inverse);
}

/* === States === */

.chat-thread-empty,
.chat-thread-loading {
    text-align: center;
    padding: var(--space-6);
    color: var(--text-tertiary);
    font-size: var(--font-size-xs);
}

/* === Skeleton loader === */

.chat-skeleton {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-1);
    list-style: none;
}

.chat-skeleton__avatar {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--bg-tertiary);
    position: relative;
    overflow: hidden;
}

.chat-skeleton__body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.chat-skeleton__line {
    height: 9px;
    border-radius: var(--radius-sm);
    background: var(--bg-tertiary);
    position: relative;
    overflow: hidden;
    width: 100%;
}

.chat-skeleton__line--header {
    width: 35%;
    height: 8px;
    margin-bottom: 2px;
}

.chat-skeleton__avatar::after,
.chat-skeleton__line::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--brand-primary-rgb), 0.08) 50%, transparent 100%);
    animation: chat-skeleton-shimmer 1.4s ease-in-out infinite;
    transform: translateX(-100%);
}

@keyframes chat-skeleton-shimmer {
    0% {
        transform: translateX(-100%);
    }
    100% {
        transform: translateX(100%);
    }
}

/* === Meta row (reactions + read receipts on one line) === */

.chat-bubble__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 12px;
    margin-top: 4px;
}

/* === Reactions === */

.chat-bubble__reactions {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.chat-reaction {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 6px;
    border-radius: 10px;
    border: 1px solid var(--border-primary);
    background-color: var(--bg-secondary);
    font-size: 11px;
    cursor: pointer;
    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast);
}

.chat-reaction:hover {
    background-color: var(--bg-hover);
    border-color: var(--brand-primary);
}

.chat-reaction--mine {
    background-color: rgba(var(--brand-primary-rgb), 0.12);
    border-color: var(--brand-primary);
}

.chat-reaction__count {
    font-size: 10px;
    color: var(--text-secondary);
    font-weight: var(--font-weight-medium);
}

.chat-reaction--mine .chat-reaction__count {
    color: var(--brand-primary);
}

/* Reaction picker (floating) */
.chat-reaction-picker {
    display: flex;
    gap: 2px;
    padding: 4px 6px;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}

.chat-reaction-picker__btn {
    background: none;
    border: none;
    font-size: 18px;
    cursor: pointer;
    padding: 4px 6px;
    border-radius: var(--radius-sm);
    transition: background-color var(--transition-fast);
}

.chat-reaction-picker__btn:hover {
    background-color: var(--bg-hover);
}

/* === Read receipts === */

.chat-bubble__read {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    font-size: 10px;
    color: var(--text-tertiary);
}

.chat-bubble__read .material-icons {
    font-size: 13px;
    color: var(--brand-primary);
}

/* === Unread marker === */

.chat-unread-marker {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-3) 0;
    font-size: 10px;
    font-weight: var(--font-weight-semibold);
    color: var(--color-error);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    user-select: none;
    list-style: none;
}

.chat-unread-marker::before,
.chat-unread-marker::after {
    content: '';
    flex: 1;
    height: 1px;
    background-color: var(--color-error);
    opacity: 0.5;
}

.chat-unread-marker span {
    padding: 0 var(--space-2);
}

/* === Date separator === */

.chat-thread-date {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-3) 0;
    color: var(--text-tertiary);
    font-size: 10px;
    font-weight: var(--font-weight-medium);
    user-select: none;
}

.chat-thread-date::before,
.chat-thread-date::after {
    content: '';
    flex: 1;
    height: 1px;
    background-color: var(--border-primary);
}

/* === Jump-to-parent highlight === */

.chat-node--highlight > .chat-bubble {
    background-color: rgba(var(--brand-primary-rgb), 0.1);
    border-radius: var(--radius-sm);
    animation: chat-highlight-fade 2s ease-out;
}

@keyframes chat-highlight-fade {
    0% {
        background-color: rgba(var(--brand-primary-rgb), 0.2);
    }
    100% {
        background-color: transparent;
    }
}

/* === Mobile (§10) === */

@media (max-width: 480px) {
    :root {
        --chat-depth-step: 12px;
        --chat-depth-cap: 3;
    }

    .chat-bubble__body {
        max-width: 100%;
    }

    .chat-bubble__actions {
        opacity: 1; /* always visible on mobile */
    }

    .chat-bubble__menu-trigger {
        opacity: 1;
    }
}

/* === Agent typing indicator (chat-mention webhook in progress) === */
.chat-typing-row {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    padding: var(--space-1) var(--space-2);
    list-style: none;
    margin: 2px 0;
    animation: chatTypingFadeIn 200ms ease-out;
}

/* [2026-09-18] ВХОД НОВОГО СООБЩЕНИЯ (аудит §5.3, Ч1).
   Замер на боевой странице (фрейм «TCO Sulfur Pumps Project», ветка из 100 сообщений, открытое
   окно): у узла сообщения `animation-name: none`, `transition-duration: 0s`, `opacity: 1` —
   сообщение возникает рывком, тогда как индикатор печати рядом въезжает плавно (правило
   `chatTypingFadeIn` ниже). Получалось, что «призрак» печати анимирован, а настоящее сообщение
   нет.
   Метку ставит ТОЛЬКО на новые узлы `ChatThreadPanel._markMessageEntering` (после первой
   отрисовки ветки), поэтому сотня сообщений истории не превращается в каскад анимаций.
   Длительность 200 мс — та же, что у соседнего индикатора печати: одна панель, одна скорость.
   Движутся только `opacity` и `transform`.
   Ref: docs/AUDIT_UI_MOTION_2026-09-17.md §5.3 Ч1. Chat: ui-motion-fixes-2026-09-17. */
.chat-node--entering {
    animation: chat-node-in 200ms var(--ease-out, ease-out) both;
}

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

@media (prefers-reduced-motion: reduce) {
    /* Сдвиг убираем, появление оставляем: иначе сообщение снова возникает рывком. */
    .chat-node--entering {
        animation: chat-node-in-fade 120ms ease-out both;
    }

    @keyframes chat-node-in-fade {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }
}

@keyframes chatTypingFadeIn {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.chat-typing-avatar {
    width: 18px;
    height: 18px;
    border-radius: var(--radius-md);
    background: var(--brand-primary);
    color: var(--text-inverse);
    font-size: 9px;
    font-weight: var(--font-weight-semibold);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.chat-typing-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.chat-typing-name {
    font-size: 11px;
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
    padding: 0 var(--space-1);
}

.chat-typing-bubble {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 6px 10px;
    background-color: var(--bg-secondary);
    border-radius: 12px 12px 12px 4px;
    width: fit-content;
}

.chat-typing-bubble span {
    width: 5px;
    height: 5px;
    background: var(--text-tertiary);
    border-radius: 50%;
    animation: chatTypingDot 1.2s infinite ease-in-out;
}

.chat-typing-bubble span:nth-child(2) {
    animation-delay: 0.15s;
}
.chat-typing-bubble span:nth-child(3) {
    animation-delay: 0.3s;
}

@keyframes chatTypingDot {
    0%,
    60%,
    100% {
        opacity: 0.3;
        transform: translateY(0);
    }
    30% {
        opacity: 1;
        transform: translateY(-3px);
    }
}

/* === Subject (Outlook-style) — composer input + bubble pill === */
.chat-panel-subject {
    width: 100%;
    box-sizing: border-box;
    border: 0;
    border-bottom: 1px dashed var(--border-primary);
    background: transparent;
    color: var(--text-primary);
    font-size: 12px;
    font-weight: var(--font-weight-medium);
    padding: 6px var(--space-2);
    outline: none;
    margin-bottom: 4px;
    transition: border-color var(--transition-fast);
}
.chat-panel-subject::placeholder {
    color: var(--text-tertiary);
    font-weight: var(--font-weight-normal);
    font-style: italic;
}
.chat-panel-subject:focus {
    border-bottom-color: var(--brand-primary);
}

/* SUBJECT banner — full-width, colored bar + SUBJECT label, acts as
   the thread title (UX 2026-05-11). Lifted out of the bubble into the
   LI by ChatThreadPanel._renderNode so `position: sticky` scope spans
   the whole subtree (bubble + replies UL), pinning the subject to the
   top of the chat panel until the next root scrolls in. Solid bg (not
   the 8% tint) so reply text underneath doesn't bleed through while
   the banner is stuck. */
.chat-bubble__subject {
    /* Two-row layout: row 1 = SUBJECT label + thread title, row 2 =
       thread-level actions (create-activity / revoke; future slot
       for dates / assignee / status). Column flex lets each row
       size independently while the banner stays a single sticky
       block. */
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0 0 6px;
    padding: 8px 12px;
    /* Opaque equivalent of the old rgba(brand, 0.08) tint — the tint
       is layered over the panel bg so the banner stays solid when
       sticky and reply content can't bleed through underneath. */
    background:
        linear-gradient(
            rgba(var(--brand-primary-rgb, 59, 130, 246), 0.08),
            rgba(var(--brand-primary-rgb, 59, 130, 246), 0.08)
        ),
        var(--bg-primary);
    border-left: 3px solid var(--brand-primary);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-size: 13px;
    line-height: 1.35;
    width: 100%;
    box-sizing: border-box;
    overflow: hidden;
    position: sticky;
    top: 0;
    z-index: 6;
}

.chat-bubble__subject-row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0; /* lets the text row ellipsis-truncate */
}

.chat-bubble__subject-label {
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: var(--font-weight-semibold);
    color: var(--brand-primary);
    font-size: 9px;
    flex-shrink: 0;
}
/* ACTIVITY label sits below SUBJECT — same look, slightly muted tone
   so the two rows read as related but distinct (UX 2026-05-16). */
.chat-bubble__subject-label--activity {
    color: var(--text-tertiary);
}
.chat-bubble__subject-text {
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.chat-edit-subject {
    display: block;
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--border-primary);
    border-left: 3px solid var(--brand-primary);
    background: rgba(var(--brand-primary-rgb, 59, 130, 246), 0.05);
    color: var(--text-primary);
    font-size: 11px;
    font-weight: var(--font-weight-medium);
    padding: 4px 8px;
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    outline: none;
    margin-bottom: 4px;
}
.chat-edit-subject::placeholder {
    color: var(--text-tertiary);
    font-style: italic;
    font-weight: var(--font-weight-normal);
}
.chat-edit-subject:focus {
    border-color: var(--brand-primary);
    background: rgba(var(--brand-primary-rgb, 59, 130, 246), 0.1);
}

/* [2026-09-07] Строка «агент не ответил»: модель отработала, её ответ потерян
   при обрыве, и повторно её не зовут (решение PO — повторный прогон модели
   запрещён). Пользователь обязан видеть, что ждать нечего, иначе он ждёт
   молча и впустую. Цвета — токены темы, своих значений не вводим.
   Ref: docs/TZ_CHAT_FIXES_2026-09-06.md §4 (Р3, пункт 6).
   Chat: chat-p3-integrity-20260906. */
.chat-agent-reply-lost-note {
    font-size: var(--font-size-xs);
    line-height: 1.35;
    color: var(--text-secondary);
    background: var(--bg-hover);
    border-left: 2px solid var(--color-error);
    border-radius: var(--radius-sm);
    padding: var(--space-1) var(--space-2);
    max-width: 42ch;
}

/* [2026-09-08] Р6, пункт 6 ТЗ docs/TZ_CHAT_FIXES_2026-09-06.md §7 (решение PO
   Р-0.3): пометка «вопрос снят». Задание остаётся открытым, поэтому пометка
   спокойная — строка над репликой, а не тревожная плашка: ответить всё ещё
   можно, и пугать исполнителя нечем. Chat: chat-p6-lifecycle-20260908. */
.chat-bubble__withdrawn {
    padding: 2px var(--space-6);
    font-size: var(--font-size-xs);
    color: var(--text-secondary);
    font-style: italic;
}

/* [2026-09-10] Р11, пункт 3 ТЗ docs/TZ_CHAT_FIXES_2026-09-06.md §11-бис
   (замечание Codex, chat-p11-pair-20260910): «вопрос не доставлен» — задача
   пары исчерпана по ёмкости, вторую открытую задачу канон Р5 заводить не
   разрешает. Пометка стоит НА МЕСТЕ действия, у своей реплики, а не модалкой
   (канон PO о статусе на месте). Тон тревожный, но не пугающий: реплика
   сохранена, остальные адресаты вопрос получили. */
.chat-bubble__blocked {
    padding: 2px var(--space-6);
    font-size: var(--font-size-xs);
    color: var(--color-danger, var(--text-secondary));
}

.chat-bubble__blocked-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.chat-bubble__blocked-who {
    font-weight: 600;
}

.chat-bubble__blocked-hint {
    color: var(--text-secondary);
}

/* [2026-09-10] Р10, пункты 2 и 10 ТЗ docs/TZ_CHAT_FIXES_2026-09-06.md §11.
   Строка «историю не прочитали» с повтором — на месте списка, а не модалкой:
   пустой чат и недоступная история должны выглядеть по-разному. Заглушка
   удалённой реплики — приглушённым курсивом: она остаётся в ветке, чтобы
   ответы не теряли родителя. Chat: chat-p10-ui-20260910. */
.chat-thread-error {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-4);
    color: var(--text-secondary);
    font-size: var(--font-size-sm);
}

.chat-thread-error__retry {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    background: none;
    border: 1px solid var(--border-color, var(--text-tertiary));
    border-radius: var(--radius-sm);
    padding: 2px var(--space-2);
    color: var(--text-primary);
    cursor: pointer;
}

.chat-bubble__text--deleted {
    color: var(--text-tertiary);
    font-style: italic;
}

/* [2026-09-10] Р10, пункт 8: объявление границы текстового поиска. Строка
   спокойная, не тревожная: поиск работает, просто по загруженной части.
   Chat: chat-p10-ui-20260910. */
.chat-search-scope {
    padding: 2px var(--space-4);
    font-size: var(--font-size-xs);
    color: var(--text-tertiary);
}
