/* Партнёрская панель — своя светлая тема.
 *
 * КОНТРАКТ НАПРАВЛЕНИЯ
 *
 * ТЕЗИС. Кабинет узнаётся как свой продукт, а не как системный экран
 * мессенджера: тёплый бежевый лист, белые карточки, золотой акцент.
 * Отказ от предыдущего решения – цвета из темы клиента, где панель
 * выглядела ровно так же, как любая другая Mini App.
 * СВОЙ МИР. Палитра фиксирована и от темы Telegram не зависит: лист
 * #faf8f5, карточка #ffffff, линия #e8e1d8, акцент #c19a4b, мягкий
 * акцент #f6efe0. Язык компонентов – группа секций с заголовком
 * снаружи, строки от 44px с разделителем от текста, уровни сети –
 * раскрывающиеся группы (открыт не больше одного).
 * ИСТОРИЯ. Партнёр открывает кабинет, видит срок доступа, забирает
 * свою ссылку, проверяет счёт, смотрит, сколько пришло за период, и
 * разбирает сеть по уровням.
 * ПЕРВЫЙ ЭКРАН. Профиль с аватаром 60px и сроком, сразу под ним –
 * ссылка одной строкой с многоточием в середине и кнопки «Скопировать» и
 * «Поделиться» во всю ширину.
 * ФОРМА. Список секций, закреплён брифом владельца.
 * ДВИЖЕНИЕ. Кабинет – рабочий экран, и движение здесь работает на
 * отклик, а не на показ: вдавливание кнопки 120 мс, подсветка строки,
 * раскрытие уровня 200 мс, заход плашки 240 мс. Авторский момент один –
 * подтверждение копирования: ссылка ушла в невидимый буфер, и кроме
 * галочки в кнопке партнёру опереться не на что. Стартовые состояния
 * живут только внутри @keyframes, иначе выключенная анимация оставила
 * бы экран пустым.
 *
 * Внешних CDN нет: панель уходит на российский сервер и показывает
 * деньги. Шрифт системный, Google Fonts подключать нельзя.
 */

:root {
    --bg: #faf8f5;
    --section: #ffffff;
    --bg-soft: #f3efe9;

    --text: #2f2a24;
    --hint: #7d7266;
    --line: #e8e1d8;

    --accent: #c19a4b;
    --accent-soft: #f6efe0;
    --accent-text: #ffffff;
    /* Текстовый вариант акцента – темнее заливочного. Золотой #c19a4b
       на белом даёт всего 2.3:1, и ссылки им читались бы с трудом. */
    --accent-ink: #8a6a2c;

    /* Срок вышел, буфер отказал – приглушённый предупреждающий тон.
       Красный тут крик: партнёр ничего не сломал. */
    --warn: #a4573a;

    --shadow: 0 2px 12px rgba(63, 51, 36, 0.06);

    --radius: 14px;
    --pad-x: 16px;

    --safe-top: var(--tg-content-safe-area-inset-top, 0px);
    --safe-bottom: var(--tg-safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    padding: 0;
}

body {
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    font-size: 16px;
    line-height: 1.4;
    -webkit-font-smoothing: antialiased;
    /* Горизонтальный отступ живёт на секциях, а не на body: чипы уровней
       скроллятся от края до края, обрезать их полем нельзя. */
    padding: calc(12px + var(--safe-top)) 0 calc(28px + var(--safe-bottom));
}

.app {
    max-width: 560px;
    margin: 0 auto;
}

/* ===== Секции ===== */

/* Граница обязательна, а не декоративна: белая карточка на листе #faf8f5
   различается едва-едва, и без линии группы расплываются в один фон. */
.section {
    background: var(--section);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    margin: 0 var(--pad-x) 8px;
    overflow: hidden;
}

/* Заголовок стоит над карточкой, а не внутри неё: так группы читаются
   с одного взгляда по левому краю.

   Цвет – акцентный, а не приглушённый серо-коричневый: заголовки держат
   каркас экрана, и в них партнёр целится глазом, пролистывая панель.
   Берётся текстовый вариант акцента (--accent-ink): заливочный #c19a4b
   на листе даёт 2.3:1 и мелким кеглем в 13px читается плохо. */
.section-title {
    margin: 20px var(--pad-x) 7px;
    padding: 0 4px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-ink);
}

/* Пояснение под группой – тем же кеглем, что и заголовок. */
.footnote {
    margin: 7px calc(var(--pad-x) + 4px) 0;
    font-size: 13px;
    line-height: 1.35;
    color: var(--hint);
}

/* ===== Переключатель «Кабинет» / «Админ-панель» ===== */

/* Та же дорожка на мягком фоне, что у переключателя периода в «Движении
   денег» (.periods) – это два экрана одного и того же приложения, а не
   разные виджеты, и переключатель между ними должен выглядеть одинаково.
   Здесь он не внутри карточки: показывается персоналу до всего остального
   контента, а не как часть конкретной секции. */
.view-switch {
    display: flex;
    gap: 4px;
    margin: 0 var(--pad-x) 12px;
    padding: 3px;
    background: var(--bg-soft);
    border-radius: 12px;
}

.view-switch-btn {
    flex: 1;
    min-height: 40px;
    padding: 0 6px;
    border: 0;
    border-radius: 9px;
    background: none;
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
    color: var(--hint);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, transform 0.12s ease;
}

.view-switch-btn[aria-checked="true"] {
    background: var(--section);
    color: var(--text);
    box-shadow: 0 1px 3px rgba(63, 51, 36, 0.1);
}

.view-switch-btn:active {
    transform: scale(0.97);
}

.view-switch-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ===== Строки ===== */

.rows {
    list-style: none;
    margin: 0;
    padding: 0;
}

.row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    /* 44px – минимальная цель под палец. */
    min-height: 44px;
    padding: 10px 16px;
    position: relative;
}

/* Разделитель начинается от текста, а не от края карточки: край держит
   форму группы, отступ показывает, что строки внутри одной. */
.row:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 16px;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--line);
}

.row-label {
    color: var(--hint);
    font-size: 15px;
    flex-shrink: 0;
}

.row-value {
    font-size: 16px;
    text-align: right;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ===== Профиль ===== */

.profile {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
}

.avatar-wrap {
    position: relative;
    flex-shrink: 0;
    width: 60px;
    height: 60px;
}

.avatar,
.avatar-placeholder {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    display: block;
}

.avatar {
    object-fit: cover;
    background: var(--bg-soft);
    border: 2px solid var(--line);
}

.avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    border: 2px solid var(--line);
    color: var(--accent-ink);
    font-size: 24px;
    font-weight: 600;
    line-height: 1;
}

/* Кнопка меньше 44px намеренно: она сидит на аватаре, и увеличение
   перекрыло бы само фото. Промах не ведёт в пустоту — клик открытия файла
   висит на всём .avatar-wrap (app.js), эта кнопка в разметке нужна ради
   фокуса и подписи для скринридера, не как единственная цель. */
.avatar-edit {
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--accent-text);
    border: 2px solid var(--section);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    cursor: pointer;
    transition: opacity 0.15s ease, transform 0.12s ease;
}

.avatar-edit:active {
    opacity: 0.75;
    transform: scale(0.9);
}

/* Отправка фото по мобильной сети идёт до двух минут, и всё это время кнопка
   правки выключена (app.js). Без видимого признака выключенная кнопка
   читается как зависшая панель, поэтому аватар притухает, а карандаш
   пульсирует. Приглушённое состояние живёт вне @keyframes: при выключенном
   движении анимация схлопывается, и признак загрузки должен остаться. */
.avatar-wrap.is-uploading .avatar,
.avatar-wrap.is-uploading .avatar-placeholder {
    opacity: 0.45;
}

.avatar-wrap.is-uploading .avatar-edit {
    opacity: 0.5;
    animation: avatar-uploading 1.4s ease-in-out infinite;
}

@keyframes avatar-uploading {
    50% { opacity: 1; }
}

/* Строка срока идёт после блока профиля и своим ::after разделителя не
   получает (она последняя в секции). Без этой линии профиль и срок
   слипаются в один блок. */
.profile + .row::before {
    content: "";
    position: absolute;
    left: 16px;
    right: 0;
    top: 0;
    height: 1px;
    background: var(--line);
}

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

.profile-name {
    font-size: 20px;
    font-weight: 600;
    line-height: 1.25;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.profile-username {
    margin: 2px 0 0;
    font-size: 15px;
    color: var(--hint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.row-value.ended {
    color: var(--warn);
}

/* ===== Реферальная ссылка ===== */

.link-box {
    padding: 12px 16px 14px;
}

/* Намеренно не <a>: поле нужно, чтобы скопировать и передать другому.
   Кликабельная ссылка – ловушка, по которой партнёр случайно пройдёт по
   собственному приглашению. Выделение руками оставлено: буфер в старых
   webview срабатывает не всегда. */
/* Ссылка держится в одной строке: целиком она партнёру не нужна – он её
   копирует кнопкой, а в три строки по символам она разносила карточку.
   Многоточие ставится в середине, а не в конце: в хвосте реферальный код,
   и обрезать надо адрес, который у всех одинаковый. Отсюда флекс из двух
   частей – голова ужимается, хвост остаётся целым. */
.link-value {
    display: flex;
    font-size: 15px;
    line-height: 1.35;
    color: var(--text);
    user-select: all;
    -webkit-user-select: all;
}

.link-head {
    /* min-width: 0 обязателен: без него флекс-элемент не сжимается меньше
       своего содержимого и многоточие не появляется вовсе. */
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.link-tail {
    white-space: nowrap;
    flex-shrink: 0;
}

.link-actions {
    display: flex;
    gap: 8px;
    margin-top: 12px;
}

.btn {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-height: 44px;
    padding: 0 12px;
    border: 1px solid transparent;
    border-radius: 10px;
    font-family: inherit;
    font-size: 15px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s ease, opacity 0.15s ease, transform 0.12s ease;
}

/* Кнопка вдавливается под пальцем. Палец закрывает её целиком, и одна
   прозрачность под ним не читается – короткое сжатие видно по краям. */
.btn:active {
    opacity: 0.8;
    transform: scale(0.97);
}

.btn-primary {
    background: var(--accent);
    color: var(--accent-text);
}

.btn-secondary {
    background: var(--bg-soft);
    border-color: var(--line);
    color: var(--accent-ink);
}

.btn-secondary:active {
    background: var(--accent-soft);
}

.btn:focus-visible,
.level-head:focus-visible,
.avatar-edit:focus-visible,
.member-link:focus-visible,
.staff-tagfilter-chip:focus-visible,
.staff-list-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Заголовок группы прижат к краям карточки, и рамка фокуса со смещением
   наружу срезалась бы её overflow: hidden. */
.level-head:focus-visible {
    outline-offset: -2px;
}

/* Галочка подменяет иконку внутри той же кнопки: подтверждение не должно
   двигать соседние элементы. */
.btn .icon-done,
.btn.done .icon-copy {
    display: none;
}

/* Единственный авторский момент панели. Копирование – главное действие
   кабинета, и результат его не виден: ссылка ушла в буфер, на экране не
   изменилось ничего. Поэтому подтверждение играет само – галочка
   приходит в кнопку, кнопка темнеет. Дальше движения нет нигде. */
.btn.done .icon-done {
    display: block;
    animation: check-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.btn-primary.done {
    background: var(--accent-ink);
}

@keyframes check-in {
    from {
        opacity: 0;
        transform: scale(0.6);
    }
}

/* Строка остаётся только под отказ буфера: успех подтверждает сама кнопка,
   и держать под неё пустой резерв значит вешать на карточку лишние 28px
   воздуха на всё время работы. :empty убирает строку вместе с отступом. */
/* Заходит так же, как плашка состояния, и по той же причине: строка
   появляется в ответ на неудачное копирование и раздвигает карточку.
   Это отказная ветка того самого действия, которому досталось
   подтверждение галочкой, – она не должна возникать рывком. */
.link-hint {
    display: block;
    margin-top: 10px;
    font-size: 13px;
    line-height: 1.35;
    color: var(--warn);
    animation: banner-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.link-hint:empty {
    display: none;
}

/* ===== Баланс ===== */

/* Счета показываются одинаково, строка за строкой. Выделенного «главного»
   баланса нет: начисления в разных валютах приходят на разные счета, и
   крупная цифра одного из них читалась бы как весь баланс партнёра. */
/* Сумма стоит в левой колонке строки, поэтому цвет задаётся явно: слот
   .row-label по умолчанию красит текст в hint, а цифра денег – основной текст. */
.balance-amount {
    color: var(--text);
    font-size: 17px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* Валюта – не подпись к сумме, а имя счёта: цвет обычного текста, не hint. */
.balance-currency {
    color: var(--text);
    font-size: 16px;
    font-weight: 500;
    flex-shrink: 0;
}

/* Валюта и кнопка вывода — один флекс-ребёнок строки, чтобы у .row
   осталось ровно два: слева сумма, справа всё остальное. Иначе третий
   элемент разбил бы justify-content: space-between между двумя парами. */
.balance-right {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}

/* Кнопка появляется только когда сервер прислал can_withdraw: порог живёт
   в /api/balance, а не здесь, — фронт просто рисует то, что разрешено.
   44px – тот же минимум тап-цели, что у .row и .btn, а не отдельная мерка
   для компактной кнопки: это главное денежное действие партнёра. */
.withdraw-btn {
    flex-shrink: 0;
    min-height: 44px;
    padding: 0 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--accent-soft);
    color: var(--accent-ink);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, transform 0.12s ease, opacity 0.15s ease;
}

.withdraw-btn:active {
    opacity: 0.8;
    transform: scale(0.96);
}

.withdraw-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Форма заявки разворачивается прямо под строкой счёта — не системный
   prompt(): webview Telegram его не всегда поддерживает, а modal в панели
   больше нигде не встречается. Заходит тем же приёмом, что и плашки
   состояния. */
.withdraw-form {
    padding: 6px 16px 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    animation: banner-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.withdraw-form-inner {
    display: flex;
    gap: 8px;
}

.withdraw-input {
    flex: 1;
    min-width: 0;
    min-height: 40px;
    padding: 0 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    font-family: inherit;
    font-size: 15px;
    background: var(--bg-soft);
    color: var(--text);
}

.withdraw-input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.withdraw-submit {
    flex: 0 0 auto;
    min-width: 96px;
}

/* Строка сообщения держит место только когда есть что сказать — как
   .link-hint рядом: пустая строка не должна раздвигать карточку. */
.withdraw-message {
    display: block;
    font-size: 13px;
    line-height: 1.35;
    color: var(--hint);
}

.withdraw-message.error {
    color: var(--warn);
}

.withdraw-message:empty {
    display: none;
}

/* Заявка отправлена – строка под своим счётом. Отступ слева тот же, что у
   строк счетов, поэтому она читается как продолжение своей строки, а не как
   подпись ко всему списку. Разделителя сверху нет: он отрезал бы пометку от
   счёта, к которому она относится. */
.withdraw-status {
    position: relative;
    padding: 0 16px 12px;
    font-size: 13px;
    line-height: 1.35;
    color: var(--hint);
}

/* Строку счёта, под которой стоит пометка, разделитель не отрезает: линия
   между ними отдала бы пометку следующему счёту. Класс вешает JS – :has() в
   старых webview Telegram не работает, а промахнуться тут нельзя. */
.row.row-flush::after {
    content: none;
}

/* Снятая линия не исчезает, а съезжает под пометку: она отделяет счёт вместе
   с его заявкой от следующего счёта. Убери совсем – и два баланса слиплись бы
   в один блок, а группа строк держится именно на этих линиях. */
.withdraw-status:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 16px;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--line);
}

/* Запасная строка про закрытое окно – под всем списком счетов. Нужна, когда
   счёта заявки на экране нет (заблокированный отсеивается на выдаче): без неё
   кнопка пропала бы без объяснения. */
.withdraw-note {
    margin: 0;
    padding: 0 16px 14px;
    font-size: 13px;
    line-height: 1.35;
    color: var(--hint);
}

/* ===== Движение денег ===== */

/* Переключатель периода. Дорожка на мягком фоне, выбранный — белой плашкой:
   тот же приём, что у плиток счётчиков, только наоборот. Считать её карточкой
   в карточке не за что — это один элемент управления, а не вложенная группа. */
.periods {
    display: flex;
    gap: 4px;
    margin: 12px 16px 0;
    padding: 3px;
    background: var(--bg-soft);
    border-radius: 12px;
}

.period {
    flex: 1;
    /* 40 + 3 + 3 отступа дорожки — цель под палец дотягивает до 46px. */
    min-height: 40px;
    padding: 0 6px;
    border: 0;
    border-radius: 9px;
    background: none;
    font-family: inherit;
    /* 13px, а не 14: на экране 320px «Этот месяц» в 14px не влезал в треть
       дорожки и переносился на вторую строку, раздувая переключатель вдвое. */
    font-size: 13px;
    white-space: nowrap;
    font-weight: 500;
    color: var(--hint);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, transform 0.12s ease;
}

.period[aria-checked="true"] {
    background: var(--section);
    color: var(--text);
    box-shadow: 0 1px 3px rgba(63, 51, 36, 0.1);
}

.period:active {
    transform: scale(0.97);
}

.period:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ===== Динамика ===== */

.chart {
    padding: 12px 16px;
}

.chart-bars {
    display: flex;
    align-items: stretch;
    gap: 3px;
    height: 64px;
}

/* Дорожка столбика видна всегда: день без начислений — это тоже факт, и
   пропуск в ряду читается лучше, чем отсутствующий столбик. */
.chart-bar {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: flex-end;
    background: var(--bg-soft);
    border-radius: 4px;
    overflow: hidden;
}

/* Столбики вырастают снизу — единственное движение в этой секции. Высота
   идёт трансформом, а не свойством height: анимировать раскладку нельзя.
   Стартовое состояние живёт в keyframes, иначе при выключенном движении
   график остался бы плоским навсегда. */
.chart-fill {
    width: 100%;
    background: var(--accent);
    border-radius: 4px;
    transform-origin: bottom;
    animation: bar-in 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(var(--i, 0) * 12ms);
}

@keyframes bar-in {
    from {
        transform: scaleY(0);
    }
}

/* Края периода и пик. Без пика столбики остаются картинкой без масштаба:
   партнёр видит форму, но не знает, сколько это в деньгах. */
.chart-legend {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 7px;
    font-size: 12px;
    color: var(--hint);
}

.chart-legend > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Без явных долей flexbox сжимает все три подписи пропорционально их
       контенту — а самый длинный текст (пик) от такого сжатия теряет
       больше всего пикселей и обрезается первым на узком экране. Даты по
       краям короткие и второстепенные, поэтому им хватает готовой ширины. */
    flex: 0 1 auto;
}

.chart-legend > span:first-child,
.chart-legend > span:last-child {
    flex: 0 1 22%;
}

.chart-legend > span:nth-child(2) {
    color: var(--accent-ink);
    font-weight: 500;
    text-align: center;
    flex: 1 1 auto;
}

/* ===== Список операций ===== */

/* Линия отделяет блок от соседнего во всю ширину: начисления, списания,
   график и прочие валюты — разные блоки внутри одной карточки, а не соседние
   строки одного списка. Без неё строка «USDT +40,00» прилипает к столбикам и
   читается как подпись к графику, которым она не является. */
.ledger-group,
#earnings-extra:not(:empty) {
    border-top: 1px solid var(--line);
}

/* Заголовок группы – подпись к столбцу цифр, а не отдельная строка списка:
   поэтому кегль и цвет заголовка секции, а не строки. Акцент здесь общий
   с .section-title: «От кого пришло» и «Списания» – ровно такие же
   заголовки групп, и приглуши один из них – разница читалась бы как
   разный смысл, а не как разное оформление. */
.ledger-head {
    padding: 12px 16px 6px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-ink);
}

/* Свёрнутый хвост списка. display: none, а не прозрачность: невидимая
   строка не должна оставаться в таб-порядке и у экранного диктора. */
.entry-hidden {
    display: none;
}

/* Раскрытые строки проявляются ступенькой – только прозрачностью: высота
   списка в этот же момент растёт, и сдвиг снизу спорил бы с ней. Стартовое
   состояние живёт в keyframes: пропиши opacity: 0 здесь – и при отключённом
   движении хвост остался бы невидимым навсегда. */
.entry-revealed {
    animation: row-in 0.2s ease-out both;
    animation-delay: calc(var(--i, 0) * 20ms);
}

/* Кнопка на всю ширину группы: цель под палец тут вся полоса, а не подпись
   в её середине. */
.ledger-more {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 12px 16px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 15px;
    color: var(--accent-ink);
    text-align: left;
    cursor: pointer;
    position: relative;
    transition: background 0.15s ease;
}

/* Подсветка гаснет плавно, а зажигается сразу – отклик должен опережать
   палец, а его уход не мигать. */
.ledger-more:active {
    background: var(--bg-soft);
    transition-duration: 0s;
}

/* Линия над кнопкой начинается от текста: она отделяет её от последней
   строки внутри одной группы, а не делит группы между собой. */
.ledger-more::before {
    content: "";
    position: absolute;
    left: 16px;
    right: 0;
    top: 0;
    height: 1px;
    background: var(--line);
}

.ledger-more:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.entry-main {
    min-width: 0;
}

/* Знак несёт смысл сам, поэтому цветом списание не выделяется: красный на
   выводе собственных денег читается как ошибка. Приглушение достаточно. */
.entry-amount {
    font-size: 16px;
    font-weight: 600;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.entry-amount.out {
    font-weight: 500;
    color: var(--hint);
}

.entry-comment {
    display: block;
    margin-top: 1px;
    font-size: 13px;
    line-height: 1.3;
    color: var(--hint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.entry-date {
    font-size: 14px;
    color: var(--hint);
    flex-shrink: 0;
}

/* Заявка, по которой деньги ещё не ушли. Мягкая подложка отделяет её от
   состоявшихся списаний: это состояние счёта, а не операция. Сумма без знака
   и приглушённая по той же причине — минус читался бы как ушедшие деньги.
   Полоска слева даёт второй признак: цвет не должен быть единственным. */
.entry-pending {
    background: var(--accent-soft);
}

.entry-pending::before {
    content: "";
    position: absolute;
    left: 0;
    top: 6px;
    bottom: 6px;
    width: 3px;
    border-radius: 0 3px 3px 0;
    background: var(--accent);
}

.entry-pending .entry-amount {
    font-weight: 500;
    color: var(--hint);
}

/* ===== Счётчики сети ===== */

.stats {
    display: flex;
    gap: 10px;
    padding: 12px;
}

/* Мягкий акцент под плитками – прежняя черта панели. Он же снимает нужду
   в разделителе между ними: две подложки читаются как две величины. */
.stat {
    flex: 1;
    padding: 12px;
    background: var(--accent-soft);
    border-radius: 12px;
    text-align: center;
}

.stat-value {
    font-size: 26px;
    font-weight: 700;
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
}

/* Плитка с деньгами. Стоит сразу за базовым правилом намеренно: вес селектора
   тот же, и вынеси её выше по файлу — 26px из .stat-value перебили бы кегль.
   Единственное место панели с плавающим кеглем, и причина в данных: длина
   суммы заранее неизвестна, а на 320px даже 21px рвал строку между цифрами и
   знаком рубля. */
.stat-value.stat-money {
    font-size: clamp(16px, 4.9vw, 22px);
    letter-spacing: -0.01em;
}

.stat-label {
    margin-top: 3px;
    font-size: 13px;
    color: var(--hint);
}

/* ===== Уровни ===== */

/* Сеть пустая – групп нет вовсе, и карточка забирать место не должна:
   под заголовком иначе висит пустой белый прямоугольник. */
.levels:empty {
    display: none;
}

/* Граница во всю ширину, а не отступающая: она делит группы, а не строки
   внутри одной. Отступ слева остаётся признаком «это одна группа». */
.level-group + .level-group {
    border-top: 1px solid var(--line);
}

.level-head {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 48px;
    padding: 10px 16px;
    background: none;
    border: 0;
    font-family: inherit;
    color: var(--text);
    text-align: left;
    cursor: pointer;
    position: relative;
    transition: background 0.15s ease;
}

/* Подсветка гаснет плавно, а зажигается сразу: отклик на касание должен
   опережать палец, а его уход – не мигать. */
.level-head:active {
    background: var(--bg-soft);
    transition-duration: 0s;
}

/* Разделитель между заголовком и его списком – только у раскрытой группы:
   у закрытой линию уже даёт граница следующей группы, и вместе они дали бы
   двойную черту. */
.level-head[aria-expanded="true"]::after {
    content: "";
    position: absolute;
    left: 16px;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--line);
}

.level-title {
    flex: 1;
    min-width: 0;
    font-size: 16px;
    font-weight: 500;
}

.level-count {
    font-size: 16px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.level-chevron {
    flex-shrink: 0;
    color: var(--hint);
    transition: transform 0.2s ease;
}

.level-head[aria-expanded="true"] .level-chevron {
    transform: rotate(180deg);
    color: var(--accent-ink);
}

/* Раскрытие через grid-template-rows: высота списка заранее неизвестна, а
   0fr → 1fr анимируется без замера в JS. Старый webview анимацию не знает
   и просто показывает список сразу – деградация, а не поломка.
   visibility обязателен: без него ссылки закрытой группы остаются в
   таб-порядке и клавиатура уводит фокус в невидимый список. */
.level-body {
    display: grid;
    grid-template-rows: 0fr;
    visibility: hidden;
    transition: grid-template-rows 0.2s ease, visibility 0.2s;
}

/* Прямой ребёнок сетки обязан уметь сжиматься до нуля, иначе минимальная
   высота содержимого не даст группе закрыться. Правило по «>» на любой узел,
   а не по .rows: тем же раскрытием живут заявки на вывод в Админ-панели, и
   там внутри тела не список, а карточка с кнопками. */
.level-body > * {
    min-height: 0;
    overflow: hidden;
}

.level-head[aria-expanded="true"] + .level-body {
    grid-template-rows: 1fr;
    visibility: visible;
}

/* Строки проявляются вместе с ростом группы – только прозрачностью.
   Сдвиг снизу здесь спорил бы с самим раскрытием: контейнер в эти же
   200 мс растёт, и строка ехала бы в движущемся окне с обрезкой по
   нижнему краю. Стартовое состояние живёт только в keyframes: пропиши
   opacity: 0 в самом правиле – и при отключённой анимации партнёр
   получил бы навсегда невидимый список. Задержка ступеньки копеечная и
   упирается в потолок: у сотни строк лесенка превратилась бы в ожидание. */
.level-body .row {
    animation: row-in 0.2s ease-out both;
    animation-delay: calc(var(--i, 0) * 20ms);
}

@keyframes row-in {
    from {
        opacity: 0;
    }
}

/* Служебная строка группы – «Загружаем», «никого», отказ запроса. */
.level-message .row-label {
    font-size: 15px;
}

/* ===== Участники ===== */

.member-name {
    font-size: 16px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Отрицательный внешний отступ гасит внутренний: ссылка дорастает до
   44px по вертикали, а высота строки не меняется. Без этого цель под
   палец – только 22px высоты самого текста. */
.member-link {
    color: var(--accent-ink);
    text-decoration: none;
    cursor: pointer;
    padding: 11px 0;
    margin: -11px 0;
    transition: opacity 0.12s ease;
}

.member-link:active {
    opacity: 0.6;
}

.member-date {
    font-size: 14px;
    color: var(--hint);
    flex-shrink: 0;
}

/* ===== Состояния ===== */

.empty {
    padding: 22px 16px;
    text-align: center;
    font-size: 15px;
    line-height: 1.4;
    color: var(--hint);
}

/* Плашка приходит после ответа сервера, когда партнёр уже смотрит на
   экран. Возникни она мгновенно – всё под ней прыгнуло бы вниз без
   объяснения; короткий заход сверху показывает, что она встала на своё
   место, а не подвинула страницу сама по себе. */
.banner {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 var(--pad-x) 10px;
    padding: 12px 14px;
    border-radius: var(--radius);
    font-size: 15px;
    line-height: 1.35;
    animation: banner-in 0.24s cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes banner-in {
    from {
        opacity: 0;
        transform: translateY(-6px);
    }
}

/* Палитра своя и известна заранее, поэтому фон плашки красим честно.
   Иконка при этом остаётся: цвет не должен быть единственным признаком
   состояния. */
.banner-error {
    background: #fdf0ee;
    border: 1px solid #f0cdc6;
}

.banner-error .banner-icon {
    color: var(--warn);
}

.banner-notice {
    background: var(--accent-soft);
    border: 1px solid #e6d5ae;
}

.banner-notice .banner-icon {
    color: var(--accent-ink);
}

.banner-icon {
    flex-shrink: 0;
    /* Оптическая подгонка к первой строке текста в 15px. */
    margin-top: 1px;
}

.skeleton {
    color: transparent !important;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--bg-soft) 25%, #ece6de 50%, var(--bg-soft) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.4s linear infinite;
}

@keyframes shimmer {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

.hidden,
[hidden] {
    display: none !important;
}

/* ===== Админ-блок ===== */

/* Значение переменной, кнопка правки и кнопка удаления рядом — .row-value
   сам по себе однострочный текст, второй ребёнок внутри него держит их
   вместе, не ломая контракт .row (два прямых потомка, space-between). */
.staff-var-value {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* Само значение жмётся первым: имя переменной слева и обе кнопки справа
   нужны целиком, а длинное значение честнее обрезать многоточием, чем
   вытолкнуть кнопку «Изменить» за край экрана. */
.staff-var-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Полоса внутри карточки: поле поиска, форма добавления, группа тегов. Своим
   заголовком блок больше не подписывается — с 12.08.2026 «Заявки на вывод»,
   «Контакты» и «Администраторы» разведены по отдельным секциям, и подпись
   даёт .section-title над карточкой, как во всей остальной панели. */
.staff-block {
    padding: 14px 16px;
}

.staff-block:not(:last-child) {
    border-bottom: 1px solid var(--line);
}

/* Форма добавления под списком отделяется линией: у последней строки списка
   своего разделителя нет (.row:not(:last-child)::after), и без этой границы
   поля ввода слипались бы с последней записью. */
.rows + .staff-block {
    border-top: 1px solid var(--line);
}

/* Пустой список переменных всё равно занимает место – иначе линия выше
   встала бы второй чертой вплотную к верхней границе карточки, а карточка
   осталась бы без единого слова о том, что в ней ничего нет. */
.staff-vars:empty::after {
    content: "Переменных нет";
    display: block;
    padding: 14px 16px;
    color: var(--hint);
    font-size: 14px;
}

/* Возврат из карточки контакта к списку блоков. Стоит над заголовком и
   выровнен по тому же левому краю, что и он: кнопка относится ко всему
   экрану, а не к первой секции под ней. */
.staff-back {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin: 4px var(--pad-x) 0;
    padding: 6px 8px 6px 4px;
    min-height: 36px;
    border: 0;
    border-radius: 8px;
    background: none;
    font-family: inherit;
    font-size: 15px;
    color: var(--accent-ink);
    cursor: pointer;
    transition: background 0.15s ease, transform 0.12s ease;
}

.staff-back:active {
    background: var(--bg-soft);
    transform: scale(0.97);
}

.staff-back:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Имя контакта — заголовок карточки, и здесь он не служебная подпись
   секции, а название экрана: без капса и в человеческом кегле. */
.staff-card-title {
    margin-top: 10px;
    font-size: 18px;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text);
}

.staff-card-sub {
    margin: 0 var(--pad-x) 10px;
    padding: 0 4px;
    font-size: 13px;
    line-height: 1.35;
    color: var(--hint);
}

.staff-card-sub:empty {
    display: none;
}

.staff-row {
    display: flex;
    gap: 8px;
}

.staff-row .withdraw-input {
    flex: 1;
    min-width: 0;
}

/* Теги — чипы вперемешку, а не список: их читают все разом, а не по одному. */
.staff-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.staff-tags:empty::after {
    content: "Тегов нет";
    color: var(--hint);
    font-size: 14px;
}

.staff-tag {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 6px 5px 10px;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent-ink);
    font-size: 13px;
}

.staff-tag-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 24px – минимум цели по WCAG 2.5.8 для второстепенных действий в
       плотном списке; 44px как у основных кнопок здесь раздуло бы ряд
       тегов на всю ширину карточки. */
    width: 24px;
    height: 24px;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 16px;
    line-height: 1;
    padding: 0;
    cursor: pointer;
}

.staff-tag-remove:active {
    background: rgba(0, 0, 0, 0.08);
}

.staff-tag-remove:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* Удаление администратора отзывает доступ персоналу — не тот же вес
   действия, что снятие безобидного тега или переменной, хотя кнопка та же
   по форме. Цвет — тот же приглушённый --warn, что у «Отклонить» заявку на
   вывод: решение серьёзное, но не авария. */
.staff-admin-remove {
    color: var(--warn);
}

.staff-admin-remove:active {
    background: rgba(164, 87, 58, 0.12);
}

/* Строка контакта в списке — кликабельна целиком, поэтому кнопка, а не
   <li> с обработчиком на дочернем элементе. */
.staff-list-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    min-height: 44px;
    padding: 10px 16px;
    border: none;
    background: transparent;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.staff-list-item:active {
    background: var(--bg-soft);
}

.staff-list-item:not(:last-child) {
    border-bottom: 1px solid var(--line);
}

.staff-list-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Правый столбец строки — @username и (через точку) первые теги контакта. */
.staff-list-preview {
    color: var(--hint);
    font-size: 13px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Хвост свёрнутого списка. display: none, а не прозрачность: невидимая
   строка не должна оставаться в таб-порядке. Класс вешает JS – сколько строк
   видно, знает только он (VISIBLE_CONTACTS), и nth-child здесь развёл бы
   одно число по двум файлам. */
.staff-list-hidden {
    display: none;
}

/* Число найденных контактов над списком — тот же приглушённый тон, что у
   подписи в других местах панели, не отдельный акцент. */
.staff-list-count {
    color: var(--hint);
    font-size: 13px;
    margin: 4px 0 8px;
}

/* Фильтр по тегам — переключаемые чипы, не выпадающий список: панель
   открывается в Telegram webview, где клик по строке за пределами попапа
   не всегда закрывает его предсказуемо (см. заметку про :has() в CLAUDE.md
   про такие же старые webview). Чипы читаются целиком и не требуют
   отдельного закрывающего жеста. */
.staff-tagfilter {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 10px 0;
}

.staff-tagfilter:empty {
    display: none;
}

.staff-tagfilter-chip {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 12px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: transparent;
    color: var(--hint);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.staff-tagfilter-chip[aria-pressed="true"] {
    background: var(--accent-soft);
    border-color: var(--accent-soft);
    color: var(--accent-ink);
}

.staff-tagfilter-chip:active {
    transform: scale(0.97);
}

/* ===== Заявки на вывод (админ-панель) ===== */

/* Заявка — та же раскрывающаяся группа, что уровень сети: .level-group,
   .level-head, .level-body и вся их механика переиспользуются как есть.
   Здесь ниже только то, что живёт внутри раскрытого тела. */

/* Получатель в шапке жмётся первым, сумма справа остаётся целиком: по ней
   владелец и решает, а длинный @username честнее обрезать. */
.staff-withdrawal-who {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Сумма в шапке рядом с числом уровня по кеглю, но не по ширине: у неё ещё
   код валюты, и tabular-nums от .level-count тут кстати. */
.staff-withdrawal-who + .level-count {
    flex-shrink: 0;
}

/* Отступов у самой карточки нет намеренно: она – прямой ребёнок сетки
   .level-body, и при grid-template-rows: 0fr высота схлопывается до нуля, а
   padding при box-sizing: border-box остаётся и держит 24 лишних пикселя под
   каждой закрытой заявкой. Поэтому поля живут на детях – их обрезает
   overflow: hidden вместе со всем содержимым. */
.withdraw-request-date,
.withdraw-request-balance {
    padding: 0 16px;
    color: var(--hint);
    font-size: 13px;
    line-height: 1.35;
}

.withdraw-request-date {
    padding-top: 10px;
}

.withdraw-request-actions {
    display: flex;
    gap: 8px;
    margin: 12px 16px 14px;
}

.withdraw-request-actions .btn {
    flex: 1;
    min-height: 40px;
}

/* Отклонить — не системный «danger»-красный (в этой панели он читался бы
   как «партнёр что-то сломал», см. --warn выше), а тот же приглушённый тон,
   что у предупреждения об истёкшем сроке доступа: решение серьёзное, но не
   авария. */
.btn-warn {
    background: transparent;
    border-color: var(--warn);
    color: var(--warn);
}

.btn-warn:active {
    background: rgba(164, 87, 58, 0.08);
}

/* Первый клик по «Одобрить» только взводит подтверждение, второй списывает
   деньги. Цвет меняется на тот же приглушённый --warn, что у «Отклонить»:
   это точка невозврата, и она не должна выглядеть как обычный тап. */
.btn-primary.confirming {
    background: var(--warn);
}

/* Движение выключено системной настройкой. Анимации не отменяются, а
   схлопываются в мгновение: у появления строк и плашек стартовое
   состояние прозрачное, и «animation: none» оставило бы их видимыми
   только по счастливой случайности. Задержку ступеньки тоже обнуляем –
   иначе список ждал бы её вхолостую. */
@media (prefers-reduced-motion: reduce) {
    .skeleton {
        animation: none;
    }

    /* Пульсация карандаша тоже отменяется целиком: приглушённые аватар и
       кнопка остаются признаком отправки и без движения. */
    .avatar-wrap.is-uploading .avatar-edit {
        animation: none;
    }

    * {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
    }
}

/* Панель светлая намеренно и тему клиента не наследует. При тёмном
   оформлении Telegram чуть притемняем лист, чтобы белые карточки не
   резали глаз контрастом на чёрном фоне вокруг. */
@media (prefers-color-scheme: dark) {
    :root {
        --bg: #f0ece5;
    }
}
