/* ======================================================
   STAGE — ровно две точки привязки:
   A: .users-area  — центр ripple === центр аватара
   B: .panel-area  — top одинаковый у input и у controls
   Между ними фикс зазор. Высоты блоков НЕ меняются,
   значит общий центр стека стабилен при join/leave.
====================================================== */

.stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0;
    min-width: 0;
    min-height: 0;
    height: 100%;
    width: 100%;
    box-sizing: border-box;
    transition:
        padding-right var(--t-med),
        padding-bottom 0.44s cubic-bezier(0.32, 0.72, 0, 1);
}

.stage-stack {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------- ТОЧКА A: пользователи ---------- */

.users-area {
    --avatar-size: 6rem;

    width: 100%;
    height: var(--avatar-size);
    display: grid;
    place-items: center;
}

/* Слот фикс размером с аватар — тут оба слоя в одной точке. */
.users-slot {
    position: relative;
    width: var(--avatar-size);
    height: var(--avatar-size);
}

/* === Ripple === */
.lobby-ripple {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--fg-3);
    pointer-events: none;
    transition: opacity var(--t-med);
}

.app[data-mode="room"] .lobby-ripple {
    opacity: 0;
}

.lobby-ripple-ring {
    --ring-start: 1rem;
    grid-area: 1 / 1;
    box-sizing: border-box;
    width: var(--ring-start);
    height: var(--ring-start);
    border: 1px solid currentColor;
    border-radius: 50%;
    opacity: 0;
    animation: lobby-ripple-wave 3.6s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
}

.lobby-ripple-ring:nth-child(2) {
    animation-delay: -1.8s;
}

@keyframes lobby-ripple-wave {
    0% {
        transform: scale(0.5);
        opacity: 0.55;
    }

    100% {
        /* max диаметр ~50px (14px * 3.6) — заметно меньше аватара 84px */
        transform: scale(3.6);
        opacity: 0;
    }
}

/* === Participants ===
   Раскладка — дуга-«корона» вокруг self, считается в JS (layoutParticipants,
   public/js/participants.js). Контейнер — якорь: его центр = центр .users-slot =
   позиция self. Участники позиционируются АБСОЛЮТНО (офсет --px/--py, размер --sz),
   корона выходит за пределы слота → overflow visible. */
.participants {
    position: absolute;
    inset: 0;
    overflow: visible;
}

.participant {
    /* Размер задаёт движок через --sz (fallback — базовый --avatar-size). */
    --size: var(--sz, var(--avatar-size));

    position: absolute;
    left: 50%;
    top: 50%;
    width: var(--size);
    height: var(--size);
    /* Позиция = центр контейнера + офсет (--px/--py); -50% центрирует бокс на
       этой точке. Перемещение и сужение — плавные (одноразовые: вход/выход/resize,
       не непрерывные → без пиксель-джиттера текста). */
    transform: translate(calc(-50% + var(--px, 0px)), calc(-50% + var(--py, 0px)));
    transition:
        transform var(--t-med),
        width var(--t-med),
        height var(--t-med);
    opacity: 0;
    will-change: transform;
}

/* Мгновенная установка позиции без переезда (новый блоб / resize / ui-scale). */
.participant.no-anim {
    transition: none;
}

/* Вход/выход: участник несёт позиционный transform, поэтому pop-scale живёт на
   аватаре (иначе перетёр бы позицию). Участник — только фейд. */
.participant.pop-in {
    animation: participant-fade-in 520ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.participant.pop-in .participant-avatar {
    animation: avatar-pop-in 520ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Вход отыграл: .pop-in снимается (_markMounted в js/participants.js), а
   видимость дальше держит этот класс — сам .participant по умолчанию opacity:0,
   чтобы не мелькнуть до раскладки.

   ЗАЧЕМ вообще снимать: .pop-in несёт `animation` на аватаре. Пока класс висел
   на блобе всю его жизнь, снятие ЛЮБОЙ другой анимации с того же аватара
   (пульс речи скринкастера, screencast.css) возвращало свойство к avatar-pop-in
   — и браузер честно запускал mount-анимацию заново: блоб схлопывался в
   scale(0.4) и всплывал на каждое «замолчал». Одноразовая анимация не должна
   оставаться в каскаде после того, как отыграла.

   При уходе класс НЕ снимается: participant-fade-out не задаёт `from`, поэтому
   гаснет из текущего значения — с opacity:1 отсюда. Без него базовой была бы
   opacity:0 у .participant, и фейд ухода шёл бы из нуля в ноль (блоб просто
   исчезал). Анимация в каскаде сильнее обычного объявления, так что opacity:1
   ей не мешает. */
.participant.is-in {
    opacity: 1;
}

.participant.pop-out {
    animation: participant-fade-out 260ms ease forwards;
}

.participant.pop-out .participant-avatar {
    animation: avatar-pop-out 260ms ease both;
}

@keyframes participant-fade-in { to { opacity: 1; } }
@keyframes participant-fade-out { to { opacity: 0; } }
@keyframes avatar-pop-in { from { transform: scale(0.4); } to { transform: scale(1); } }
@keyframes avatar-pop-out { to { transform: scale(0.4); } }

@media (prefers-reduced-motion: reduce) {
    .lobby-ripple-ring {
        animation: none;
        opacity: 0.45;
        transform: none;
    }

    .lobby-ripple-ring:nth-child(2) {
        display: none;
    }

    /* ВАЖНО: не гасить transform у .participant — он несёт ПОЗИЦИЮ (--px/--py),
       не только pop. Убираем только анимации/переходы. */
    .participant {
        transition: none;
    }

    .participant.pop-in {
        animation: none;
        opacity: 1;
    }

    .participant.pop-in .participant-avatar,
    .participant.pop-out .participant-avatar {
        animation: none;
    }

    .participant.pop-out {
        animation: none;
        opacity: 0;
    }
}

.participant-avatar {
    position: relative;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 50%, rgba(230, 230, 232, 0.06) 0%, rgba(230, 230, 232, 0.02) 55%, transparent 75%);
    display: flex;
    align-items: center;
    justify-content: center;
}

.participant-avatar::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 0.71rem;
    height: 0.71rem;
    margin: 0;
    border-radius: 50%;
    background: var(--fg-2);
    transform: translate(-50%, -50%);
    transition: transform var(--t-fast), background var(--t-fast), box-shadow var(--t-med);
}

.participant-avatar::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1px solid rgba(230, 230, 232, 0);
    transform: scale(0.7);
    transition: border-color var(--t-fast), transform var(--t-med);
}

.participant.speaking:not(.muted):not(.deaf) .participant-avatar::before {
    transform: translate(-50%, -50%) scale(1.4);
    background: var(--signal);
    box-shadow: 0 0 24px rgba(230, 230, 232, 0.45);
}

.participant.speaking:not(.muted):not(.deaf) .participant-avatar::after {
    border-color: rgba(230, 230, 232, 0.18);
    transform: scale(1);
}

.participant-name {
    /* абсолютно под аватаром — НЕ сдвигает его центр */
    position: absolute;
    top: calc(100% + 0.6rem);
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.1rem;
    color: var(--fg-2);
    /* Ник масштабируется вместе с блобом (--sz): от 0.62rem у мелких до 0.86rem
       у крупных. Согласовано с оценкой _nameHeight в participants.js. */
    font-size: clamp(0.62rem, calc(var(--sz, var(--avatar-size)) * 0.16), 0.86rem);
    letter-spacing: var(--tracking-wide);
    text-transform: lowercase;
    text-align: center;
    max-width: 12rem;
    transition: font-size var(--t-med);
}

.participant-name-line {
    display: block;
    line-height: 1.25;
    white-space: nowrap;
}

.participant.self .participant-avatar {
    box-shadow: inset 0 0 0 1px rgba(230, 230, 232, 0.2);
}

.participant.muted .participant-name,
.participant.deaf .participant-name {
    color: var(--fg-4);
}

/* ---------- Premium nicks (contributors) ----------
   Тёплое матовое золото с медленным мерцанием. Не сатурированное «цыганское»
   жёлтое (#FFD700 запрещено), а пшенично-янтарная палитра. Свечение даём
   через drop-shadow на filter — text-shadow не работает поверх
   background-clip:text. Анимация 7s + cubic-bezier чтобы блик ощущался
   живым, но не отвлекал в комнате на пять человек. */
.participant-name.premium {
    background: linear-gradient(
        110deg,
        #b89254 0%,
        #e8c97a 28%,
        #f6e4ad 50%,
        #e8c97a 72%,
        #b89254 100%
    );
    background-size: 220% 100%;
    background-position: 0% 50%;
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
    filter:
        drop-shadow(0 0 4px rgba(232, 201, 122, 0.35))
        drop-shadow(0 0 12px rgba(232, 201, 122, 0.18));
    animation: premium-shimmer 7s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}

@keyframes premium-shimmer {
    0%, 100% { background-position:   0% 50%; }
    50%      { background-position: 100% 50%; }
}

/* Muted/deaf: гасим премиум-блеск, чтобы статус «выключен» читался,
   но золотой характер остаётся — это всё ещё контрибьютор. */
.participant.muted .participant-name.premium,
.participant.deaf  .participant-name.premium {
    opacity: 0.5;
    filter: drop-shadow(0 0 3px rgba(232, 201, 122, 0.18));
}

@media (prefers-reduced-motion: reduce) {
    .participant-name.premium {
        animation: none;
        background-position: 50% 50%;
    }
}

.participant.muted .participant-avatar::before,
.participant.deaf .participant-avatar::before {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0);
    box-shadow: none;
    pointer-events: none;
}

.participant.deaf .participant-avatar::after {
    border-color: rgba(138, 138, 144, 0.42);
    border-style: dashed;
    transform: scale(1);
}

/* ---------- Состояние-иконки в центре аватара (mute / deaf) ----------
   Заменяют пропавшую центральную точку. deaf всегда co-occur с muted
   (toggleSound гасит и mic), поэтому deaf по приоритету перекрывает mic-иконку:
   при deaf — наушники, при только muted — микрофон. */
.participant-state-icon {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 1.5rem;
    height: 1.5rem;
    display: grid;
    place-items: center;
    color: var(--fg-2);
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.7);
    pointer-events: none;
    transition: opacity var(--t-fast), transform var(--t-med);
}

.participant-state-icon svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.participant-state-icon svg * {
    vector-effect: non-scaling-stroke;
}

/* Перечёркивание (mute/deaf): направление «/» как на кнопках панели.
   Толстая bg-линия вырезает зазор, чтобы страйк не сливался с глифом. */
.participant-state-icon svg .strike-bg {
    stroke: var(--bg-0);
    stroke-width: 2.8;
}

/* Глиф наушников занимает меньше viewBox, чем микрофон, и визуально мельче —
   чуть увеличиваем контейнер. Штрих non-scaling, толщина линий не меняется. */
.participant-state-icon--deaf {
    width: 1.72rem;
    height: 1.72rem;
}

.participant.muted .participant-state-icon--mic {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
}

/* deaf перекрывает mic-иконку (deaf ⊇ muted). */
.participant.deaf .participant-state-icon--mic {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.7);
}

.participant.deaf .participant-state-icon--deaf {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
}

.participant:not(.self) {
    cursor: pointer;
}

/* Unified toast/hint host: одна позиционная точка для всех неблокирующих
   уведомлений (бывший invite-hint, entryError, roomToast). Висит
   АБСОЛЮТНО внутри .footer.footer-meta над футером. На chat-open футер
   получает padding-right: var(--chat-rail), но box-sizing: border-box
   оставляет padding-box ту же ширину — left:50% указывает в центр старого
   box'а, а флекс-контент уезжает влево вместе с уменьшившимся
   content-area. Чтобы плашка ехала ВМЕСТЕ с видимым контентом, вычитаем
   из transform половину --chat-rail. На мобиле rail = 0px → no-op.
   Стейт-машина — в js/toasts.js: data-kind="hint"|"toast",
   data-priority="error|warn|info" варьирует цвет. */
.toast-host {
    position: absolute;
    bottom: calc(100% + 0.57rem);
    left: 50%;
    transform: translateX(calc(-50% - var(--chat-rail, 0px) / 2)) translateY(-0.29rem);
    max-width: calc(100% - 2.29rem);
    color: var(--fg-3);
    font-size: 0.79rem;
    letter-spacing: var(--tracking-wide);
    text-align: center;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-med), transform var(--t-med), color var(--t-fast);
    z-index: 1;
}

.toast-host.is-visible {
    opacity: 1;
    transform: translateX(calc(-50% - var(--chat-rail, 0px) / 2)) translateY(0);
}

.toast-host.is-clickable {
    pointer-events: auto;
    cursor: pointer;
}

.toast-host[data-kind="toast"] {
    font-size: 0.86rem;
    text-transform: lowercase;
}

.toast-host[data-priority="error"] {
    color: var(--signal-warn);
}

.toast-host[data-priority="warn"] {
    color: var(--fg-2);
}

@media (prefers-reduced-motion: reduce) {
    .toast-host {
        transition-duration: 0.01ms;
    }
}

.participant[data-conn="poor"]::after {
    content: "";
    position: absolute;
    top: -2px;
    right: -2px;
    width: 0.57rem;
    height: 0.57rem;
    border-radius: 50%;
    background: var(--warn);
    z-index: 3;
    pointer-events: none;
}

/* Баннер-приглашение (web-only) при заходе по ?room=-ссылке: «вас пригласили
   в комнату #КОД» + выбор [открыть в приложении] / [продолжить в браузере].
   НЕ авто-входим. Запуск void:// и веб-вход — по клику (showInviteBanner в
   js/app.js). В desktop-сборке не показывается. */
.invite-banner {
    position: fixed;
    top: 0.86rem;
    left: 50%;
    transform: translate(-50%, -0.6rem);
    z-index: 300;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.29rem;
    max-width: calc(100vw - 1.71rem);
    padding: 1rem 2.14rem;
    background: rgba(16, 16, 18, 0.96);
    border: 1px solid var(--fg-4);
    border-radius: 2px;
    overflow: hidden;                 /* клип утекающей полосы по скруглению */
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-med), transform var(--t-med);
}

.invite-banner.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
    pointer-events: auto;
}

.invite-banner-text {
    color: var(--signal);
    font-size: 0.93rem;
    letter-spacing: var(--tracking);
    text-transform: lowercase;
    text-align: center;
    transition: opacity 170ms ease;
}

/* Промежуточное состояние при смене текста (fade out перед подменой). */
.invite-banner-text.is-fading {
    opacity: 0;
}

/* Код комнаты — заглавными (не наследует lowercase родителя). */
.invite-banner-code {
    text-transform: uppercase;
}

.invite-banner-actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
    /* fit-content + stretch: контейнер по ширине самой длинной кнопки, обе
       кнопки равны этой ширине (а не на весь баннер). */
    width: fit-content;
    max-width: 100%;
}

.invite-banner-open,
.invite-banner-web {
    flex-shrink: 0;
    box-sizing: border-box;
    /* flex-центрирование + явный line-height → обе кнопки строго равной высоты
       (иначе наследуемый line-height мог давать разницу). */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.43rem 0.93rem;
    border-radius: 2px;
    font: inherit;
    font-size: 0.79rem;
    line-height: 1.15;
    letter-spacing: var(--tracking-wide);
    text-transform: lowercase;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}

.invite-banner-open {
    background: transparent;
    color: var(--screen);
    border: 1px solid var(--screen);
}

.invite-banner-open:hover {
    background: var(--screen);
    color: #0a0a0b;
}

.invite-banner-web {
    background: transparent;
    color: var(--fg-2);
    border: 1px solid var(--fg-4);
}

.invite-banner-web:hover {
    color: var(--signal);
    border-color: var(--fg-2);
}

/* Утекающая полоса по нижней границе — таймер автозакрытия (14с). scaleX 1→0
   слева направо. Пауза при наведении на баннер. Само закрытие — по animationend
   (см. js/app.js), поэтому pause на hover откладывает и закрытие. */
.invite-banner-timer {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 2px;
    background: rgba(255, 255, 255, 0.16);
    transform-origin: left center;
    animation: invite-banner-drain 14s linear forwards;
}

.invite-banner:hover .invite-banner-timer {
    animation-play-state: paused;
}

@keyframes invite-banner-drain {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}

@media (prefers-reduced-motion: reduce) {
    .invite-banner {
        transition-duration: 0.01ms;
    }
}
