/* ds-new.css — недостающие компоненты дизайн-системы. Полностью на токенах
   (ds-tokens.css), поэтому автоматически работают в светлой и тёмной темах
   без единой строки тёмного оверрайда — демонстрация выгоды токенов. */

/* ===== Алерты / баннеры ===== */
.ds-alert { display:flex; gap:12px; align-items:flex-start; padding:12px 14px;
  border-radius:var(--r-md); border:1px solid; font-size:var(--fs-md); line-height:var(--lh-normal); width:100%; }
.ds-alert__icon { flex:0 0 auto; width:20px; height:20px; margin-top:1px; }
.ds-alert__title { font-weight:var(--fw-semi); margin:0 0 2px; }
.ds-alert__body { margin:0; color:var(--text); min-width:0; overflow-wrap:anywhere; }
/* min-width:0 обязателен: без него текстовый блок во flex-контейнере не ужимается ниже
   своей минимальной ширины и вылезает за узкий экран вместе со всей страницей. */
.ds-alert--info    { background:var(--c-info-bg);    border-color:var(--c-info);    color:var(--c-info); }
.ds-alert--success { background:var(--c-success-bg); border-color:var(--c-success); color:var(--c-success); }
.ds-alert--warning { background:var(--c-warning-bg); border-color:var(--c-warning); color:var(--c-warning); }
.ds-alert--danger  { background:var(--c-danger-bg);  border-color:var(--c-danger);  color:var(--c-danger); }
.ds-alert .ds-alert__body { color:var(--text); }

/* ===== Тосты ===== */
.ds-toast { display:flex; gap:10px; align-items:center; min-width:260px; max-width:380px;
  padding:12px 14px; background:var(--surface); border:1px solid var(--border);
  border-left:3px solid var(--c-success); border-radius:var(--r-md); box-shadow:var(--sh-lg); color:var(--text); }
.ds-toast--danger  { border-left-color:var(--c-danger); }
.ds-toast--info    { border-left-color:var(--c-info); }
.ds-toast__icon { flex:0 0 auto; width:20px; height:20px; }
.ds-toast__text { font-size:var(--fs-md); flex:1; }
.ds-toast__text small { display:block; color:var(--text-muted); font-size:var(--fs-sm); }
.ds-toast__close { flex:0 0 auto; background:none; border:0; color:var(--text-subtle); cursor:pointer; font-size:16px; line-height:1; }
.ds-toast__close:hover { color:var(--text); }

/* ===== Пустое состояние ===== */
.ds-empty { text-align:center; padding:36px 20px; max-width:420px; margin:0 auto; }
.ds-empty__icon { width:56px; height:56px; margin:0 auto 14px; border-radius:var(--r-pill);
  display:flex; align-items:center; justify-content:center; background:var(--surface-3); color:var(--text-subtle); }
.ds-empty__title { font-size:var(--fs-lg); font-weight:var(--fw-semi); margin:0 0 4px; color:var(--text); }
.ds-empty__text { font-size:var(--fs-md); color:var(--text-muted); margin:0 0 16px; }

/* ===== Скелетоны загрузки ===== */
.ds-skel { position:relative; overflow:hidden; background:var(--surface-3); border-radius:var(--r-sm); }
.ds-skel::after { content:""; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.09), transparent); animation:ds-shimmer 1.3s infinite; }
html:not([data-bs-theme="dark"]) .ds-skel::after { background:linear-gradient(90deg, transparent, rgba(0,0,0,.05), transparent); }
.ds-skel--line { height:12px; }
.ds-skel--title { height:18px; width:60%; }
.ds-skel--avatar { width:40px; height:40px; border-radius:var(--r-pill); }
.ds-skel--btn { height:36px; width:120px; border-radius:var(--r-md); }
@keyframes ds-shimmer { 100% { transform:translateX(100%); } }

/* ===== Аватары ===== */
.ds-avatar { position:relative; display:inline-flex; align-items:center; justify-content:center;
  border-radius:var(--r-pill); background:var(--surface-3); color:var(--text-subtle);
  font-weight:var(--fw-semi); overflow:visible; flex:0 0 auto; }
.ds-avatar img { width:100%; height:100%; border-radius:inherit; object-fit:cover; }
.ds-avatar--sm { width:28px; height:28px; font-size:var(--fs-xs); }
.ds-avatar--md { width:40px; height:40px; font-size:var(--fs-md); }
.ds-avatar--lg { width:56px; height:56px; font-size:var(--fs-lg); }
.ds-avatar__status { position:absolute; right:-1px; bottom:-1px; width:11px; height:11px;
  border-radius:var(--r-pill); border:2px solid var(--surface); background:var(--text-subtle); }
.ds-avatar__status--online { background:var(--c-success); }
.ds-avatar__status--busy { background:var(--c-danger); }

/* ===== Прогресс ===== */
.ds-progress { height:8px; border-radius:var(--r-pill); background:var(--surface-3); overflow:hidden; width:100%; }
/* display:block обязателен. Сегмент часто верстают строчным тегом (<i>, <span>), а строчному
   элементу высота в процентах не применяется вовсе: полоса получает нулевую высоту и
   пропадает — без ошибки, без пустой дорожки, просто ничего. Ловилось уже дважды. */
.ds-progress__bar { display:block; height:100%; border-radius:inherit; background:var(--accent); transition:width .3s; }
.ds-progress__bar--success { background:var(--c-success); }
.ds-progress__bar--danger { background:var(--c-danger); }
/* Предупреждение — состояние между «всё хорошо» и «уже поздно»: расход подошёл к лимиту,
   но лимит ещё не исчерпан. Без него шкала расхода прыгает из зелёного сразу в красный,
   и человек узнаёт о кончающемся лимите в момент отказа. */
.ds-progress__bar--warning { background:var(--c-warning); }
/* Приглушённый сегмент — «занято, но это не тревога» (заморожено, в холде, доля от лидера). */
.ds-progress__bar--muted { background:var(--text-subtle); }

/* Составная шкала: несколько сегментов в одной дорожке (например «свободно» + «в холде»).
   Отдельного компонента не заводим — это модификатор той же шкалы. Без display:flex второй
   сегмент уходит на новую строку и обрезается по overflow, то есть молча исчезает. */
.ds-progress--split { display:flex; }
.ds-progress--split .ds-progress__bar { flex:0 0 auto; }

/* ===== Стопка: вертикальный ритм задаёт контейнер ===== */
/* Внутри карточки блоки идут стопкой — подсказка, плашка, форма, ряд кнопок — и расстояние
   между ними должно принадлежать КОНТЕЙНЕРУ, а не каждому блоку.
   Пока ритм держали собственными margin у блоков, любой забытый отступ давал слипание, и
   выглядело это как приклеенная кнопка: «Привязать Telegram» вплотную к плашке «Защитите
   аккаунт», кнопка удаления аккаунта вплотную к предупреждению. Дефект появлялся каждый раз
   заново — стоило вставить новый блок в середину.
   Собственные margin прямых детей гасим здесь же: сложившись с зазором, они снова ломают строй. */
.ds-stack { display:flex; flex-direction:column; gap:var(--sp-3); }
.ds-stack > * { margin-top:0; margin-bottom:0; }
.ds-stack--tight { gap:var(--sp-2); }
.ds-stack--loose { gap:var(--sp-4); }

/* Список однотипных строк (сеансы, платежи, заявки) внутри стопки. Строки сами рисуют
   разделители и держат свой шаг, поэтому в стопку список входит ОДНИМ блоком: иначе зазор
   контейнера растащит строки, и линии повиснут в пустоте. */
.ds-stack__group { min-width:0; }

/* Ряд кнопок — один блок стопки, а не несколько. Своего отступа сверху у него нет намеренно:
   расстояние до соседа задаёт стопка, а собственный margin сложился бы с её зазором. */
.ds-stack__row { display:flex; align-items:center; flex-wrap:wrap; gap:var(--sp-3); }

/* <details> ритм собой не задаёт. Всё, кроме <summary>, браузер складывает в служебный
   ::details-content, поэтому display:flex на самом <details> расставляет зазор только между
   заголовком и содержимым — внутри содержимого блоки снова слипаются. Стопку вешаем на
   внутреннюю обёртку, а не на <details>. */
.ds-details__inner { display:flex; flex-direction:column; gap:var(--sp-3); margin-top:var(--sp-3); }
.ds-details__inner > * { margin-top:0; margin-bottom:0; }

/* ===== Пагинация ===== */
.ds-pagination { display:inline-flex; gap:4px; align-items:center; }
.ds-pagination a, .ds-pagination span { display:inline-flex; align-items:center; justify-content:center;
  min-width:34px; height:34px; padding:0 8px; border-radius:var(--r-md); font-size:var(--fs-md);
  color:var(--text); text-decoration:none; border:1px solid transparent; transition:background .12s,border-color .12s; }
.ds-pagination a:hover { background:var(--surface-3); }
.ds-pagination .is-active { background:var(--accent); color:var(--on-accent); font-weight:var(--fw-semi); }
.ds-pagination .is-disabled { color:var(--text-subtle); pointer-events:none; }
.ds-pagination .ds-pagination__dots { pointer-events:none; color:var(--text-subtle); }

/* ===== Валидация форм ===== */
.ds-field { display:flex; flex-direction:column; gap:6px; }
.ds-field__label { font-size:var(--fs-md); font-weight:var(--fw-med); color:var(--text); }
.ds-field .form-control { transition:border-color .12s, box-shadow .12s; }
.ds-field .form-control:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--focus-ring); outline:none; }
.ds-field .form-control.is-invalid { border-color:var(--c-danger); }
.ds-field .form-control.is-invalid:focus { box-shadow:0 0 0 3px var(--c-danger-bg); }
.ds-field .form-control.is-valid { border-color:var(--c-success); }
.ds-hint { font-size:var(--fs-sm); color:var(--text-muted); }
.ds-error { font-size:var(--fs-sm); color:var(--c-danger); display:flex; align-items:center; gap:5px; }
.ds-error__ico { width:14px; height:14px; flex:0 0 auto; }

/* ===== Тултип по hover ===== */
.ds-tip { position:relative; display:inline-flex; align-items:center; }
.ds-tip__bubble { position:absolute; bottom:calc(100% + 8px); left:50%; transform:translateX(-50%);
  background:var(--text); color:var(--bg); font-size:var(--fs-sm); padding:6px 10px; border-radius:var(--r-md);
  white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .12s; box-shadow:var(--sh-md); z-index:var(--z-dropdown); }
.ds-tip__bubble::after { content:""; position:absolute; top:100%; left:50%; transform:translateX(-50%);
  border:5px solid transparent; border-top-color:var(--text); }
.ds-tip:hover .ds-tip__bubble { opacity:1; }

/* ===== Хлебные крошки ===== */
.ds-breadcrumb { display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:var(--fs-md); }
.ds-breadcrumb a { color:var(--text-muted); text-decoration:none; }
.ds-breadcrumb a:hover { color:var(--text); }
.ds-breadcrumb__sep { color:var(--text-subtle); }
.ds-breadcrumb__current { color:var(--text); font-weight:var(--fw-med); }

/* ===== Мелкая утилита стека для превью ===== */
.dsx-stack { display:flex; flex-direction:column; gap:12px; width:100%; }

/* ===== Разбор очереди модерации: плитка превью, размытие, панель массового действия =====
   Живут в системе, а не в разделе: этих деталей просят уже три раздела модерации (промахи,
   взрослые каналы, дальше наркотики с документами), и третья копия размытия — гарантированный
   расход: где-то оно снимается наведением, где-то кнопкой, где-то забудут выключить в таблице. */

.ds-tiles { display:grid; grid-template-columns:repeat(auto-fill, minmax(210px, 1fr)); gap:var(--sp-3); }
.ds-tile { border:1px solid var(--border); border-radius:var(--r-lg); overflow:hidden; background:var(--surface); }
.ds-tile.is-selected { outline:2px solid var(--accent); outline-offset:-1px; background:var(--surface-2); }
.ds-tile__frames { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:2px;
  aspect-ratio:2 / 1.35; background:var(--surface-3); }
/* grid-template-rows обязателен: без него высоту строк задают сами картинки, соотношение
   сторон не соблюдается и карточки в ряду получаются разной высоты. */
.ds-tile__frames img { width:100%; height:100%; min-height:0; object-fit:cover; display:block; }
.ds-tile__frames--single { grid-template-columns:1fr; }
.ds-tile__empty { grid-column:1 / -1; display:grid; place-items:center; color:var(--text-subtle); font-size:var(--fs-xs); }
.ds-tile__meta { padding:var(--sp-2) var(--sp-3); }
.ds-tile__title { font-size:var(--fs-sm); font-weight:var(--fw-med); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ds-tile__sub { font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px; }

/* Размытие включается на КОНТЕЙНЕРЕ и снимается наведением на конкретный кадр.
   Смысл: листать экранами открытую порнографию модератор не обязан, а проверить конкретный
   канал — должен. Поэтому размытие не «настройка красоты», а свойство рабочего места. */
.ds-blur .ds-tile__frames img { filter:blur(10px) saturate(.6); }
.ds-blur .ds-tile:hover .ds-tile__frames img { filter:none; }
@media (prefers-reduced-motion: no-preference) {
  .ds-tile__frames img { transition:filter .12s ease-out; }
}

/* Панель массового действия: всегда на виду, потому что действие затрагивает много строк. */
.ds-bulkbar { display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap;
  padding:var(--sp-3); border:1px solid var(--border); border-radius:var(--r-md); background:var(--surface-2); }
.ds-bulkbar__count { font-weight:var(--fw-med); }
.ds-bulkbar__spacer { flex:1; }

/* Селект: класс использовался разделами модерации, но в системе его не было — поля оставались
   без стилей. Заводим здесь, чтобы разметка не расходилась с системой. */
.ds-select { appearance:none; border:1px solid var(--border); border-radius:var(--r-md);
  background:var(--surface); color:var(--text); font-size:var(--fs-md); padding:6px 28px 6px 10px;
  background-image:linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
                   linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
  background-position:calc(100% - 15px) 52%, calc(100% - 10px) 52%;
  background-size:5px 5px, 5px 5px; background-repeat:no-repeat; }
.ds-select:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }

/* ===== Личный кабинет: строка лимита, плитка показателя, поле со ссылкой, меню разделов =====
   Компоненты сняты с переработанного кабинета (DEV-1485) и вынесены сюда, чтобы вторая
   страница кабинета не изобретала их заново: меню разделов стоит на каждой странице,
   строка лимита нужна и «Лимитам», и разделу API, плитка показателя — везде, где есть числа.
   Всё на токенах, поэтому обе темы работают без единой строки тёмного оверрайда. */

/* --- Строка лимита -------------------------------------------------------------------
   Показывает РАСХОД: слева название, справа «использовано N из M», под ними шкала и остаток.
   Дроби со слэшем («187 / 300») в строке нет намеренно — её читали то как расход, то как
   остаток (DEV-1297), поэтому и то и другое названо словами. */
.ds-limitrow { display:grid; gap:var(--sp-2); padding:var(--sp-3) 0; }
.ds-limitrow + .ds-limitrow { border-top:1px solid var(--border); }
.ds-limitrow__head,
.ds-limitrow__foot { display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-4); }
.ds-limitrow__name { display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap;
  font-size:var(--fs-md); font-weight:var(--fw-med); color:var(--text); }
.ds-limitrow__value { font-size:var(--fs-md); font-weight:var(--fw-semi); color:var(--text);
  white-space:nowrap; text-align:right; font-variant-numeric:tabular-nums; }
.ds-limitrow__foot { font-size:var(--fs-sm); color:var(--text-muted); }
.ds-limitrow__foot a { color:var(--link); text-decoration:none; white-space:nowrap; }
.ds-limitrow__foot a:hover { text-decoration:underline; }
/* Возможности нет на тарифе — строку не прячем, а гасим: скрытая строка не расскажет, что
   такая возможность вообще бывает и на каком тарифе появляется. */
.ds-limitrow--off .ds-limitrow__name { color:var(--text-muted); }
.ds-limitrow--off .ds-limitrow__value { color:var(--text-subtle); font-weight:var(--fw-med); }
/* На узком экране число уезжает под название: «Использовано 4 512 из 10 000» рядом с
   названием в 390px не помещается и рвёт строку в трёх местах. */
@media (max-width:575px) {
  .ds-limitrow__head,
  .ds-limitrow__foot { flex-direction:column; align-items:flex-start; gap:3px; }
  .ds-limitrow__value { text-align:left; }
}

/* --- Плитка показателя ---------------------------------------------------------------
   Подпись, крупное число, необязательная сноска под ним. Числа набраны табличными цифрами:
   плитки стоят в ряд и их сравнивают взглядом, а пропорциональные цифры сдвигают разряды. */
.ds-stats { display:grid; grid-template-columns:repeat(auto-fit, minmax(190px, 1fr)); gap:var(--sp-3); }
.ds-stat { min-width:0; padding:var(--sp-3) var(--sp-4); background:var(--surface);
  border:1px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--sh-sm); }
.ds-stat__label { font-size:var(--fs-sm); color:var(--text-muted); }
.ds-stat__value { margin-top:2px; font-size:var(--fs-2xl); font-weight:var(--fw-bold);
  line-height:var(--lh-tight); color:var(--text); font-variant-numeric:tabular-nums; }
.ds-stat__note { margin-top:2px; font-size:var(--fs-xs); color:var(--text-muted); }
.ds-stat__note--up { color:var(--c-success); }
.ds-stat__note--down { color:var(--c-danger); }
/* Без рамки — когда плитка стоит внутри карточки и вторая рамка была бы лишней. */
.ds-stat--bare { padding:0; background:none; border:0; box-shadow:none; }

/* --- Поле «ссылка + скопировать» -----------------------------------------------------
   Такую ссылку копируют, а не читают: поле только для чтения, а кнопка рядом — единственный
   надёжный способ забрать её на телефоне, где выделение пальцем промахивается.
   Обработчик кнопки — в витрине (index.html, секция «Кабинет»), он же и есть контракт. */
.ds-copyfield { display:flex; align-items:stretch; width:100%; min-width:0; overflow:hidden;
  background:var(--surface-2); border:1px solid var(--border-strong); border-radius:var(--r-md); }
.ds-copyfield:focus-within { border-color:var(--accent); box-shadow:0 0 0 3px var(--focus-ring); }
/* Шрифт поля не мельче 16px (--fs-lg): на iOS фокус в поле с мелким шрифтом увеличивает
   страницу, и масштаб обратно не возвращается. Это ограничение, а не решение о размере. */
.ds-copyfield__input { flex:1 1 auto; min-width:0; padding:9px 12px; border:0; background:transparent;
  color:var(--text-muted); font-family:inherit; font-size:var(--fs-lg); text-overflow:ellipsis; }
.ds-copyfield__input:focus { outline:0; color:var(--text); }
.ds-copyfield__btn { flex:0 0 auto; display:inline-flex; align-items:center; gap:6px; padding:0 var(--sp-3);
  border:0; border-left:1px solid var(--border); background:var(--surface-3); color:var(--text);
  font-family:inherit; font-size:var(--fs-sm); font-weight:var(--fw-semi); white-space:nowrap;
  cursor:pointer; transition:background .12s, color .12s; }
.ds-copyfield__btn:hover { background:var(--surface-sunken); }
.ds-copyfield__btn:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
/* Ответ на нажатие обязателен: без него кнопку жмут второй и третий раз, не понимая, сработала ли. */
.ds-copyfield.is-copied .ds-copyfield__btn { background:var(--c-success-bg); color:var(--c-success); }

/* --- Меню разделов кабинета ----------------------------------------------------------
   Вертикальный список разделов с активным пунктом и счётчиком. Пункты сгруппированы:
   без групп список из десяти строк читается как свалка. */
.ds-sidenav { padding:var(--sp-3); background:var(--surface); border:1px solid var(--border);
  border-radius:var(--r-lg); box-shadow:var(--sh-sm); }
.ds-sidenav--sticky { position:sticky; top:var(--sp-3); }
.ds-sidenav__head { display:flex; align-items:center; gap:var(--sp-3); min-width:0;
  padding:var(--sp-2) var(--sp-2) var(--sp-4); }
.ds-sidenav__group + .ds-sidenav__group { border-top:1px solid var(--border); }
.ds-sidenav__title { padding:var(--sp-3) var(--sp-2) var(--sp-1); font-size:var(--fs-xs);
  font-weight:var(--fw-semi); text-transform:uppercase; letter-spacing:.08em; color:var(--text-subtle); }
.ds-sidenav__item { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2);
  padding:9px 10px; border-radius:var(--r-md); font-size:var(--fs-md); color:var(--text-muted);
  text-decoration:none; transition:background .12s, color .12s; }
.ds-sidenav__item:hover { background:var(--surface-3); color:var(--text); }
/* .is-active стоит ПОСЛЕ :hover и той же силы: иначе наведение на текущий пункт стирало бы
   подсветку, и человек на секунду терял, где он находится. Порядок здесь — часть поведения. */
.ds-sidenav__item.is-active { background:var(--accent-subtle); color:var(--text); font-weight:var(--fw-semi); }
.ds-sidenav__item:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.ds-sidenav__label { display:inline-flex; align-items:center; gap:9px; min-width:0; }
.ds-sidenav__label svg,
.ds-sidenav__label i { flex:0 0 auto; }
/* Счётчик, а не украшение: показываем, только когда число зовёт зайти в раздел. */
.ds-sidenav__badge { flex:0 0 auto; min-width:20px; padding:1px 7px; border-radius:var(--r-pill);
  background:var(--surface-3); color:var(--text-muted); font-size:var(--fs-xs); font-weight:var(--fw-semi);
  text-align:center; font-variant-numeric:tabular-nums; }
.ds-sidenav__badge--alarm { background:var(--c-warning-bg); color:var(--c-warning); }

/* --- Плитки выбора --------------------------------------------------------------------
   Ряд плиток, где каждая одновременно переключатель и показатель: человек видит значения
   всех метрик сразу и переключает не вслепую. Отличие от .ds-stat (плитка показателя) в том,
   что эта кликабельна и имеет выбранное состояние; отличие от сегмента .ds-netseg в том, что
   на сегменте помещается только подпись, а здесь под подписью стоит число.
   Разметка: <button> внутри группы role="tablist", выбранная — aria-selected="true". */
.ds-picker { display:grid; grid-template-columns:repeat(auto-fit, minmax(148px, 1fr)); gap:var(--sp-2); }
.ds-picker__tile { display:flex; flex-direction:column; gap:2px; min-width:0; padding:9px 11px;
  border:1px solid var(--border); border-radius:var(--r-md); background:var(--surface-2);
  color:var(--text-muted); font-family:inherit; text-align:left; cursor:pointer;
  transition:border-color .12s, background .12s; }
.ds-picker__tile:hover { border-color:var(--border-strong); }
.ds-picker__tile:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
/* Рамка выбранной плитки удвоена внутренней тенью, а не border-width: иначе плитка на пиксель
   подрастает и весь ряд дёргается при каждом переключении. */
.ds-picker__tile[aria-selected="true"] { background:var(--surface); border-color:var(--accent);
  box-shadow:inset 0 0 0 1px var(--accent); }
.ds-picker__label { font-size:var(--fs-xs); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ds-picker__value { font-size:var(--fs-md); font-weight:var(--fw-semi); color:var(--text);
  font-variant-numeric:tabular-nums; }

/* --- Интерактивная шкала --------------------------------------------------------------
   Ползунок на несколько именованных делений: тарифы, объёмы, глубина. Заменяет ряд колонок,
   когда делений больше четырёх и колонки перестают помещаться по ширине.
   Заполненную часть трека красит переменная --ds-scale-fill (проценты), её ставит скрипт
   страницы: градиента, который сам знает позицию ползунка, в input[type=range] нет.
   Деления сделаны кнопками: по ним попадают чаще, чем возят сам ползунок. */
.ds-scale__input { -webkit-appearance:none; appearance:none; width:100%; height:8px; margin:0;
  border-radius:var(--r-pill); outline:0;
  background:linear-gradient(to right, var(--accent) 0 var(--ds-scale-fill, 0%),
                             var(--surface-3) var(--ds-scale-fill, 0%) 100%); }
.ds-scale__input::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:26px; height:26px;
  border:3px solid var(--surface); border-radius:50%; background:var(--accent);
  box-shadow:var(--sh-md); cursor:pointer; }
.ds-scale__input::-moz-range-thumb { width:26px; height:26px; border:3px solid var(--surface);
  border-radius:50%; background:var(--accent); box-shadow:var(--sh-md); cursor:pointer; }
.ds-scale__input:focus-visible { outline:2px solid var(--accent); outline-offset:4px; }
.ds-scale__ticks { display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:var(--sp-1);
  margin-top:var(--sp-2); }
.ds-scale__tick { display:flex; flex-direction:column; gap:1px; padding:5px 2px; border:0;
  border-radius:var(--r-sm); background:transparent; color:var(--text-muted); font-family:inherit;
  line-height:1.2; cursor:pointer; }
.ds-scale__tick:hover { background:var(--surface-2); }
.ds-scale__tick:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.ds-scale__value { font-size:var(--fs-sm); font-weight:var(--fw-med); color:var(--text-muted);
  font-variant-numeric:tabular-nums; }
.ds-scale__name { font-size:var(--fs-xs); }
.ds-scale__tick[aria-current="true"] .ds-scale__value,
.ds-scale__tick[aria-current="true"] .ds-scale__name { color:var(--text); }
.ds-scale__tick[aria-current="true"] .ds-scale__name { font-weight:var(--fw-med); }
/* На узком экране деления не подписать: шесть подписей в ряд сливаются, а попасть пальцем в
   шестую часть ширины нельзя. Поэтому там это сетка кнопок с зоной нажатия от 56px. */
@media (max-width: 640px) {
  .ds-scale__ticks { grid-auto-flow:row; grid-template-columns:repeat(3, 1fr); gap:var(--sp-2); }
  .ds-scale__tick { align-items:center; justify-content:center; min-height:56px;
    border:1px solid var(--border); background:var(--surface-2); }
  .ds-scale__tick[aria-current="true"] { border-color:var(--accent); box-shadow:inset 0 0 0 1px var(--accent); }
}

/* --- Подсказка «на деление выше» --------------------------------------------------------
   Предложение перейти на следующее деление: сколько доплатить и что это даёт. Пунктирная рамка
   и приглушённый текст выбраны намеренно — это подсказка, а не действие страницы; сплошная
   рамка ставит её вровень с кнопкой оплаты и перетягивает нажатия на себя.
   На последнем делении блок скрывают целиком (hidden), а не показывают пустым. */
.ds-stepup { display:block; width:100%; padding:11px 14px; border:1px dashed var(--border-strong);
  border-radius:var(--r-md); background:transparent; color:var(--text-muted); font-family:inherit;
  font-size:var(--fs-md); text-align:left; cursor:pointer; transition:border-color .12s, background .12s; }
.ds-stepup:hover { border-color:var(--text-subtle); background:var(--surface-2); }
.ds-stepup:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.ds-stepup__cost { color:var(--text); font-weight:var(--fw-semi); }

/* --- Полоса покупки ------------------------------------------------------------------
   Итог и кнопка, прилипшие к низу экрана. Нужна там, где корзина на десктопе стоит сбоку и
   остаётся на виду, а на мобильном уезжает под карточку: без полосы человек листает страницу
   вверх, чтобы вспомнить, сколько платит. Модификатор --mobile прячет полосу на широких
   экранах, где корзина видна и без неё. */
.ds-buybar { position:sticky; bottom:0; z-index:var(--z-sticky); display:flex; align-items:center;
  justify-content:space-between; gap:var(--sp-3); padding:var(--sp-3);
  padding-bottom:calc(var(--sp-3) + env(safe-area-inset-bottom));
  background:var(--surface); border-top:1px solid var(--border); box-shadow:var(--sh-lg); }
.ds-buybar__sum { font-size:var(--fs-lg); font-weight:var(--fw-semi); font-variant-numeric:tabular-nums; }
.ds-buybar__note { display:block; font-size:var(--fs-xs); color:var(--text-muted); }
@media (min-width: 641px) { .ds-buybar--mobile { display:none; } }

/* --- Блок, не меняющий высоту --------------------------------------------------------
   Обёртка для текста, который подменяется на месте: пояснение к выбранной метрике, статус,
   подпись под числом. Без неё страница дёргается на каждом переключении, потому что текст
   другой длины занимает другое число строк и всё нижележащее прыгает.
   Высоту задаёт страница переменной --ds-steady-h под самый длинный из своих текстов. Тексты
   при этом всё равно выравнивают по длине: min-height страхует, а не заменяет редактуру. */
.ds-steady { min-height:var(--ds-steady-h, 64px); }
@media (max-width: 640px) { .ds-steady { min-height:var(--ds-steady-h-sm, 104px); } }

/* --- Раскрывающаяся группа фильтров ---------------------------------------------------
   Раздел панели фильтров: заголовок-кнопка, счётчик заданных условий и содержимое, которое
   скрывается атрибутом hidden. Заведён после того, как выяснилось, что один и тот же элемент
   на двух страницах биржи выглядел по-разному: в каталоге каналов — серой полосой во всю
   ширину панели заглавными буквами, в поиске постов — обычной строкой с чертой снизу.
   Человек ходит между страницами в одном сеансе и читает это как две разные панели, поэтому
   вид у групп один, и он здесь.

   Что важно не сломать при переиспользовании:

   - раздел отличает ЧЕРТА и вес заголовка, а не заливка: заливка во всю ширину крадёт
     ширину у содержимого и на телефоне оставляет полям меньше половины экрана;
   - у последней группы черты нет — под панелью обычно свой подвал с границей, и две линии
     подряд читаются как пустая строка;
   - шеврон — линия, а не залитый треугольник: залитый в закрытом состоянии читается как
     маркер списка, а не как «можно раскрыть»;
   - расстояние между полями задаёт зазор группы, а не отступ каждого поля, иначе отступ и
     зазор складываются в двойной при первой же вставке поля в середину;
   - счётчик прячут целиком, когда условий нет: ноль в кружке выглядит сломанным счётчиком,
     а не словами «ничего не выбрано»;
   - цифры в счётчике табличные — он пересчитывается на лету, и без этого заголовок дёргается.

   Раскрытие вешает страница: ds-fgroup__b[hidden] + класс is-open на корне (он поворачивает
   шеврон). Своего скрипта у компонента нет — и вместе с этими двумя состояниями страница обязана
   держать в согласии третье: `aria-expanded` у кнопки. Без него чтение с экрана объявляет
   обычную кнопку, и раскрытие группы остаётся незамеченным; кнопка ссылается на тело через
   `aria-controls`, поэтому у тела нужен `id`.

   Отбивка заголовка (13px) и нижняя (14px) намеренно вне шкалы --sp-*: ровно эти значения уже
   стоят в двух продуктовых панелях, и ради попадания в шкалу менять высоту заголовка на обеих
   страницах дороже, чем оставить два числа с этим объяснением. */
/* Черта СТОИТ МЕЖДУ группами, а не под каждой: `:last-child` с нижней границей отказывает, как
   только после последней группы появляется что-то ещё — подвал панели со своей границей даёт
   двойную линию. Соседний селектор от порядка в разметке не зависит. */
.ds-fgroup + .ds-fgroup { border-top:1px solid var(--border); }
.ds-fgroup__h { display:flex; width:100%; align-items:center; gap:var(--sp-2);
  padding:13px 2px; border:0; background:none; color:var(--text);
  font:inherit; font-size:var(--fs-md); font-weight:var(--fw-semi); text-align:left; cursor:pointer; }
.ds-fgroup__h:focus { outline:none; }
.ds-fgroup__h:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.ds-fgroup__n { display:inline-flex; min-width:var(--sp-5); height:var(--sp-5); align-items:center;
  justify-content:center; padding:0 6px; border-radius:var(--r-pill);
  background:var(--accent); color:var(--on-accent); font-size:var(--fs-xs);
  font-weight:var(--fw-bold); font-variant-numeric:tabular-nums; }
.ds-fgroup__n[hidden] { display:none; }
/* Шеврон: Tabler-иконка (`<i class="ti ti-chevron-down">`) либо inline-svg — так сделаны обе
   продуктовые панели. Глифу нужен `inline-flex`: к строчному элементу `transform` не применяется
   и поворот при раскрытии молча не срабатывает. */
.ds-fgroup__chev { display:inline-flex; flex:none; margin-left:auto; color:var(--text-muted);
  font-size:16px; transition:transform .18s ease; }
.ds-fgroup.is-open .ds-fgroup__chev { transform:rotate(180deg); }
.ds-fgroup__b { display:flex; flex-direction:column; gap:var(--sp-3); padding:2px 2px 14px; }
.ds-fgroup__b[hidden] { display:none; }
/* Пояснение группы висит прямым ребёнком тела, а не внутри поля: свой верхний отступ оно
   складывало бы с зазором группы. */
.ds-fgroup__b > .ds-fgroup__note { margin:0; font-size:var(--fs-xs); color:var(--text-muted); line-height:1.45; }
