/* ═══════════════════════════════════════════════════════════════════════════
   Scale VPN Mini App — Neo-Minimalist Fintech UI
   Apple HIG · Soft Glassmorphism · Liquid Glass · лёгкий Claymorphism
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Плавная смена темы ────────────────────────────────────────────────────
   Растворяем весь экран целиком через View Transitions: браузер сам снимает кадр
   «до», применяет тему и кроссфейдит его в кадр «после». Меняется ВСЁ разом —
   фон, карточки, тени, стекло, текст, — поэтому рассинхрона в принципе быть не
   может.

   До этого была попытка интерполировать цветовые токены через @property. Она и
   сделала хуже: плавно ехали только ЦВЕТА, а тени (--shadow-*) и стекло
   (--glass-*, --drop-*) — составные значения, интерполировать их нельзя, и они
   щёлкали мгновенно. Половина интерфейса плыла, половина прыгала.

   Где View Transitions не поддержан (WebKit до 18) — просто мгновенное
   переключение: некрасиво, но синхронно. Хром Telegram живёт вне страницы и в
   кроссфейд не попадает — его доводит JS теми же миллисекундами (см. applyTheme). */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .3s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

/* ── Токены ── */
:root {
  --r-xl: 34px;
  --r-lg: 28px;
  --r-md: 22px;
  --r-sm: 16px;

  --acc-1: #0990DE;               /* лазурный, глубже */
  --acc-2: #045FA6;               /* тёмно-голубой */
  --grad-acc: linear-gradient(160deg, #1FA9F0 0%, #0A8CD8 48%, #045A9E 100%);
  --grad-acc-soft: color-mix(in srgb, var(--acc-1) 11%, transparent);  /* плоский светлый тон (без градиента) */

  --gold-grad: linear-gradient(135deg, #FFDF97 0%, #F7BE45 100%);
  --silver-grad: linear-gradient(135deg, #F0F3F9 0%, #C4CEDD 100%);
  --bronze-grad: linear-gradient(135deg, #F2C9A0 0%, #D99A5B 100%);

  --ok: #2EBD85;
  --warn: #F5A623;
  --bad: #F0506E;

  --sheet-top: 10px;   /* просвет над раскрытой на весь экран шторкой */
  --backdrop: rgba(12, 15, 30, .4);                 /* затемнение под шторкой */
  --sheet-shadow: 0 -12px 44px rgba(20, 28, 66, .20); /* тень над шторкой */
  /* Прогресс раскрытия шторки (0..1). Здесь — значение по умолчанию, чтобы
     calc(... * var(--x)) в компонентах работал и вне шторок; внутри .sheet его
     покадрово переписывает жест. */
  --x: 0;

  --bg: #FFFFFF;
  --surface: #FFFFFF;
  --surface-2: #F2F4F7;
  /* Плейсхолдеры заметно темнее surface-2: на белом фоне светлой темы скелетоны
     на нём почти не читались. Блик — отдельной переменной, он разной силы по темам. */
  --sk: #E3E7EE;
  --sk-shine: rgba(255, 255, 255, .55);
  --text: #0C0E1A;
  --text-2: #6A7085;
  --text-3: #9AA0B4;
  --line: rgba(20, 28, 66, .07);

  --shadow-soft: 0 10px 34px rgba(24, 34, 84, .09), 0 2px 8px rgba(24, 34, 84, .05);
  --shadow-float: 0 18px 48px rgba(24, 34, 84, .16), 0 4px 12px rgba(24, 34, 84, .07);

  /* ═══ LIQUID GLASS (таббар) ═══
     Прозрачный центр + лёгкий blur с высокой насыщенностью (контент читается
     сквозь стекло, а не тонет в «морозе»), объёмная фаска и спекулярное
     кольцо по кромке. Только широко поддерживаемый CSS — одинаково в
     WebKit/Chromium; для WebView без backdrop-filter есть --glass-fallback. */
  --glass-surface: rgba(255, 255, 255, 0.22);
  /* Центр стекла: почти прозрачный, лёгкое преломляющее «дрожание» */
  --glass-filter: blur(2.5px) saturate(1.8) brightness(1.04);
  /* Кромка-линза (фолбэк WebKit, где SVG-преломление невозможно):
     кольцо с сильным blur + подсветкой — толстый край стекла, собирающий свет */
  /* ТОЛЬКО blur, без saturate/brightness: кромка обязана быть ТОГО ЖЕ ЦВЕТА, что
     и центр — иначе кольцо видно как светлую полосу-контур. Цвет/насыщенность
     уже дал сам элемент (--glass-filter), а кромка лишь СИЛЬНЕЕ преломляет
     (больше blur). Так виден варп, а не граница. */
  /* ⚠️ ЭТИ ДВЕ РУЧКИ И ЕСТЬ «варп кромки» НА iPHONE.
     SVG-преломление (liquid-glass.js) рендерит ТОЛЬКО настоящий Chromium, а
     Telegram на iOS — это WKWebView. Там всю дисторсию даёт ::before: маска
     градиентом + этот blur. Значит менять надо здесь; правка bezel/thickness в
     liquid-glass.js на айфоне не изменит НИЧЕГО (она для Android/Desktop).
       --glass-rim-w      — ШИРИНА полосы преломления, тот самый «диапазон»;
       --glass-rim-filter — СИЛА размытия внутри неё.
     iOS 26 гнёт фон заметно шире, поэтому полоса поднята 14 → 22px, а blur
     13 → 18px. Обе цифры подобраны на глаз и правятся независимо: «слишком
     широко» и «слишком сильно» — разные жалобы. */
  --glass-rim-filter: blur(18px);
  --glass-rim-w: 22px;
  --glass-outer:
      0 2px 6px rgba(24, 34, 84, .07),
      0 14px 34px rgba(24, 34, 84, .13);
  --glass-bezel:  /* фаска: светлый свод сверху, глубина снизу, свечение внутрь */
      inset 0 1.5px 3px -1px rgba(255, 255, 255, .65),
      inset 0 -2px 4px -2px rgba(24, 34, 84, .18),
      inset 0 0 14px rgba(255, 255, 255, .12);
  --glass-rim: rgba(255, 255, 255, .85);   /* спекулярная дуга сверху */
  --glass-rim-2: rgba(255, 255, 255, .40); /* эхо-дуга снизу */
  --glass-fallback: rgba(255, 255, 255, .88);
  /* Капля-линза: полупрозрачная стеклянная плашка. «Стеклянность» держится на
     СОБСТВЕННОЙ заливке, кромке и тени (см. .drop-glass), а не на backdrop-filter
     — вложенный backdrop-filter капли в WebKit не работает. Грани тонкие. */
  --drop-shadow:
      inset 0 1px 1px rgba(255, 255, 255, .75),
      inset 0 -1px 1.5px rgba(255, 255, 255, .35),
      inset 0 0 0 1px rgba(255, 255, 255, .22),
      0 2px 5px rgba(24, 34, 84, .07),
      0 8px 20px rgba(24, 34, 84, .09);

  --tabbar-h: 72px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  /* ── Единая пружина шторок ────────────────────────────────────────────────
     Одна кривая на ВСЁ, что связано со шторками: выезд самой шторки, масштаб
     главной под ней, затемнение, доводка после перетаскивания и разжимание
     админ-панели. Раньше это был cubic-bezier(.22,.9,.28,1) — обычный ease-out;
     теперь настоящая пружина Apple `Spring(duration: .55, bounce: 0)`, ТА ЖЕ
     математика, что в js/motion.js (чтобы движение, которое ведёт JS, и
     движение, которое ведёт CSS, были неотличимы).

     Почему 0.47s, если «длительность» пружины 0.40s: у Apple duration — это
     ВОСПРИНИМАЕМОЕ время, а не время до полной остановки. Хвост экспоненты
     срезан на 99.5% пути (остаток — меньше 2px на 400px-шторке), и получившееся
     реальное время хода — 0.47s. К 0.30s пройдено уже 96% пути, поэтому
     ощущается заметно быстрее, чем говорит число.

     ФОРМА КРИВОЙ ОТ ДЛИТЕЛЬНОСТИ НЕ ЗАВИСИТ (при bounce 0 это чистое
     масштабирование по времени: x зависит только от ω·t), поэтому точки linear()
     ниже переписывать НЕ НУЖНО — скорость правится ОДНИМ значением здесь.
     Связь: реальное время = воспринимаемое × 1.183.
     История: .42s (старый ease-out) → .652s (медленно) → .54s (всё ещё) → .47s.
     Шаг меньше ~10% на глаз не читается — мельчить смысла нет. */
  --dur-sheet: .47s;
  /* Навигация админки и складывание дока. Кривая та же `--ease-spring`: при
     bounce 0 форма пружины НЕ ЗАВИСИТ от длительности (чистое масштабирование по
     времени), поэтому одни и те же точки linear() годятся для любого хода.
     --dur-page ДЕРЖАТЬ РАВНЫМ ADM_ANIM_MS в app.js. */
  --dur-page: .40s;
  --dur-dock: .46s;
  /* Switch. The .16s taken from a third-party "iOS-style" recreation snapped
     far too abruptly — Apple's switch settles, it does not jump. Same
     `--ease-spring` as navigation, so the knob eases like everything else. */
  --dur-switch: .3s;
  /* ⚠️ ЗДЕСЬ ДОЛЖЕН БЫТЬ cubic-bezier, А НЕ linear(). ЭТО И ЕСТЬ ПРИЧИНА 60 Гц.
     На ProMotion в 120 Гц идут только УСКОРЕННЫЕ (композиторные) анимации;
     остальная страница обновляется в 60. Ускорить анимацию Core Animation может
     лишь тогда, когда её кривую удаётся выразить через `CAMediaTimingFunction`,
     а это линейная, ease-* и КУБИЧЕСКАЯ БЕЗЬЕ — и только.
     `linear()` с девятнадцатью точками так выразить нельзя, поэтому WebKit
     считал такую анимацию на основном потоке, то есть в 60 Гц. Прокрутка при
     этом шла в 120 (её ведёт нативный скроллер), отчего разница и бросалась в
     глаза.

     Кривая ниже — приближение той же критически задемпфированной пружины,
     подобранное перебором по сетке контрольных точек: **макс. отклонение 2.5 %**
     (для сравнения: `.33,1,.35,1` даёт 13 %, прежняя `.22,.9,.28,1` — 23 %).
     Проверено, что x(t) строго монотонна (мин. наклон 0.43) — иначе кривая
     сложилась бы сама в себя и анимация бы дёргалась; и что обе y-точки ≤ 1,
     то есть перелёта за конечное положение нет (иначе въезжающий экран показал
     бы полоску того, что под ним).

     НЕ МЕНЯТЬ на linear() ради точности формы: цена — вдвое меньшая частота. */
  --ease-spring: cubic-bezier(.35, .5, .1, .94);

  color-scheme: light;
}

/* Точная пружина — ТОЛЬКО для сравнения на устройстве (панель диагностики
   переключает `data-easing` на <html>). Форма честнее, но анимация перестаёт
   быть ускоренной и падает в 60 Гц — см. предупреждение у `--ease-spring`.
   В бою не использовать; оставлено, чтобы разницу можно было увидеть, а не
   обсуждать на словах. */
:root[data-easing="precise"] {
  --ease-spring: linear(0, 0.0662, 0.2032, 0.3543, 0.4955, 0.6157, 0.7129,
    0.789, 0.847, 0.8902, 0.9224, 0.946, 0.963, 0.9753, 0.9841, 0.9903,
    0.9947, 0.9978, 1);
}

/* ── Тёмно-серая тема (переключатель на главной). Меняем только токены —
   компоненты перекрашиваются сами, т.к. используют переменные. ────────── */
:root[data-theme="dark"] {
  --bg: #191919;
  --surface: #232323;
  --surface-2: #2F2F2F;
  /* Затемнение и тень шторки — НЕЙТРАЛЬНО-ЧЁРНЫЕ: синеватые из светлой темы
     подсинивали фон главной под шторкой */
  --backdrop: rgba(0, 0, 0, .52);
  --sheet-shadow: 0 -12px 44px rgba(0, 0, 0, .5);
  --sk: #313131;                        /* плейсхолдеры чуть светлее поверхности */
  --sk-shine: rgba(255, 255, 255, .07);
  --text: #F1F1F1;
  --text-2: #A1A1A1;
  --text-3: #6E6E6E;
  --line: rgba(255, 255, 255, .10);

  --shadow-soft: 0 6px 20px rgba(0, 0, 0, .45), inset 0 0 0 1px rgba(255, 255, 255, .05);
  --shadow-float: 0 16px 40px rgba(0, 0, 0, .55), inset 0 0 0 1px rgba(255, 255, 255, .06);

  --grad-acc-soft: color-mix(in srgb, var(--acc-1) 24%, transparent);

  /* Стекло в тёмной теме — БЕЗ белой графики: объём дают тёмная глубина,
     преломление фона (backdrop-filter) и едва заметная нейтральная кромка. */
  --glass-surface: rgba(26, 26, 26, 0.45);
  --glass-filter: blur(3px) saturate(1.45) brightness(1.1);
  --glass-rim-filter: blur(18px);   /* только blur — кромка того же цвета, что центр (см. светлую тему) */
  --glass-outer:
      0 2px 6px rgba(0, 0, 0, .3),
      0 14px 34px rgba(0, 0, 0, .38);
  --glass-bezel:
      inset 0 4px 8px -4px rgba(0, 0, 0, .45),
      inset 0 -4px 8px -4px rgba(0, 0, 0, .3);
  --glass-rim: rgba(255, 255, 255, .09);   /* только тонкая кромка, не блик */
  --glass-rim-2: rgba(255, 255, 255, .04);
  --glass-fallback: rgba(30, 30, 30, .94);

  /* Капля в тёмной теме: без светлого контура (по просьбе) — только мягкая
     тень для объёма. Видимость даёт полупрозрачная заливка + зажжённая вкладка
     под каплей (вариант A). Раньше тут были светлые inset-блик и 1px-кромка. */
  --drop-shadow:
      0 2px 6px rgba(0, 0, 0, .22),
      0 6px 16px rgba(0, 0, 0, .2);

  color-scheme: dark;
}

/* ── База ── */
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body { height: 100%; }
/* Фон и на КОРНЕ (не только на body): корень красится первым, поэтому тема
   применяется мгновенно и до отрисовки body — без белой вспышки на запуске. */
html { background: var(--bg); }

body {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  /* NEVER taller than the visible viewport — see the note above #app. */
  min-height: 100vh;                                              /* old engines */
  min-height: min(var(--tg-viewport-stable-height, 100vh), 100dvh);
  overflow-x: hidden;
  /* Резервируем место под вертикальный скроллбар ВСЕГДА. Иначе при открытии
     шторки (body.sheet-open → overflow:hidden) скроллбар пропадал, страница
     становилась шире, и фон дёргался: имя профиля показывало лишнюю букву перед
     «…», кнопки съезжали. На мобильных (overlay-скроллбары) эффекта нет. */
  scrollbar-gutter: stable;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  user-select: none;
  transition: background .35s ease, color .35s ease;
}

.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.icon {
  width: 24px; height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

/* touch-action: manipulation — убирает ожидание двойного тапа: браузер
   отдаёт click мгновенно, нажатия ощущаются моментальными */
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; touch-action: manipulation; }
input { font: inherit; color: inherit; }

/* ═══ LIQUID GLASS ═══
   Слои (все — детерминированный CSS, одинаковый в WebKit и Chromium):
   1) сам элемент: почти прозрачная подложка + backdrop-filter (лёгкий blur,
      высокая насыщенность — контент «живёт» под стеклом) + наружная тень;
   2) ::before — фаска: светлый свод сверху, глубина снизу (объём линзы);
   3) ::after — кольцо кромки: симметричные спекулярные дуги сверху/снизу.
   overflow:hidden клипует blur по радиусу (лечит квадратный blur на части
   Android WebView — главный источник «на устройствах по-разному»). */
.liquid-glass {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-xl);
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  box-shadow: var(--glass-outer);
}
/* Кромка-линза: преломление НАРАСТАЕТ к краю и ПЛАВНО СХОДИТ НА НЕТ к центру.
   Почему именно градиентная маска: раньше кольцо вырезалось жёсткой маской
   (content-box XOR) — на стыке размытого кольца и резкого центра получалась
   РЕЗКАЯ ЛИНИЯ, и её было видно как контур («края видны»), даже когда цвет
   кольца совпал с центром. Градиент убирает стык: blur втекает от кромки внутрь
   без единой границы. Две линейки (по X и по Y) объединяются (mask-composite:
   add) — получается мягкая рамка по всему периметру.
   БЕЗ box-shadow: var(--glass-bezel) — это нарисованная фаска, т.е. буквально
   видимый контур; оставляем чистое преломление. */
.liquid-glass::before {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  -webkit-backdrop-filter: var(--glass-rim-filter);
  backdrop-filter: var(--glass-rim-filter);
  -webkit-mask:
    linear-gradient(to right, #000, transparent var(--glass-rim-w), transparent calc(100% - var(--glass-rim-w)), #000),
    linear-gradient(to bottom, #000, transparent var(--glass-rim-w), transparent calc(100% - var(--glass-rim-w)), #000);
  -webkit-mask-composite: source-over;
  mask:
    linear-gradient(to right, #000, transparent var(--glass-rim-w), transparent calc(100% - var(--glass-rim-w)), #000),
    linear-gradient(to bottom, #000, transparent var(--glass-rim-w), transparent calc(100% - var(--glass-rim-w)), #000);
  mask-composite: add;
}
/* Кольцо кромки: conic-градиент маскируется в рамку толщиной ~1.2px.
   Ярче в зените, мягкое эхо в надире, прозрачно по бокам — симметрично. */
.liquid-glass::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  padding: 1.2px;
  background: conic-gradient(
      var(--glass-rim) 0%,
      transparent 16%,
      transparent 34%,
      var(--glass-rim-2) 50%,
      transparent 66%,
      transparent 84%,
      var(--glass-rim) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
/* WebView без backdrop-filter: плотная подложка вместо прозрачной,
   чтобы стекло выглядело максимально похоже, а не исчезало */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .liquid-glass { background: var(--glass-fallback); }
}
/* Кромка-линза (::before, преломляющий blur по краю) остаётся ВСЕГДА — это
   надёжное преломление кромки на ЛЮБОМ движке. Раньше её гасили на .lg-live,
   надеясь на SVG-фильтр в backdrop-filter, но он рендерится только в НАСТОЯЩЕМ
   Chromium — во встроенных вебвью Telegram (Qt/WebView) SVG в backdrop-filter
   часто не работает, и тогда у панели НЕ БЫЛО искажения кромки вообще («нет
   дисторшна на границах»). Теперь ::before всегда рисует край, а SVG (где реально
   работает) добавляется поверх бонусом. Гасим только тонкое conic-кольцо (::after)
   — на части устройств оно «лезло» жёсткой линией. */
.liquid-glass.lg-live::after,
.liquid-glass.lg-static::after { display: none; }
/* ── Каркас ── */
/* ⚠️ `--tg-viewport-stable-height` is NOT a safe min-height on its own.

   It is Telegram's number, and on Telegram Desktop it can come back LARGER than
   the webview actually shows — notably after the window is resized, where it is
   not lowered back. Used bare, it made both #app and body taller than the window
   by that difference, and the page got a stretch of scroll holding NOTHING: pure
   min-height, no content. On a Mac that read as «the scrollbar moves, the page
   stands still» (measured 2026-07-29).

   `min(…, 100dvh)` keeps Telegram's value while it is sane and clamps it to the
   real visible height when it is not. Clamping DOWN is safe: the background is
   painted on `html` too, so a short #app still cannot show a white edge.
   The plain `100vh` line above it is the fallback for engines without dvh/min(). */
#app {
  position: relative;
  z-index: 1;
  max-width: 480px;
  margin: 0 auto;
  min-height: 100vh;                                              /* old engines */
  min-height: min(var(--tg-viewport-stable-height, 100vh), 100dvh);
  display: flex;
  flex-direction: column;
}

/* ── Шапка главной: профиль слева, кнопка темы справа (не закреплены) ── */
.home-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.theme-btn {
  width: 56px; height: 56px; flex: none;
  border-radius: 50%;
  display: grid; place-items: center;
  color: var(--text-2);
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  transition: transform .16s ease, color .2s ease;
}
.theme-btn:active { transform: scale(.9); }
.theme-btn .icon { width: 22px; height: 22px; stroke-width: 1.9; }
/* Мягкая смена иконки при переключении */
.theme-btn .icon { animation: theme-swap .3s cubic-bezier(.34,1.56,.64,1); }
@keyframes theme-swap {
  from { transform: rotate(-40deg) scale(.6); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .theme-btn .icon { animation: none; } }

/* ── Профильная «таблетка» (главная, слева сверху, скроллится с контентом) ── */
.profile-chip {
  display: inline-flex; align-items: center; gap: 12px;
  min-width: 0;
  padding: 7px 15px 7px 7px;
  border-radius: 100px;
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  transition: transform .16s ease;
  max-width: 58%;   /* оставляем место под кнопку «О нас» справа */
  flex: 0 1 auto;
}
.profile-chip:active { transform: scale(.97); }
.profile-ava {
  width: 42px; height: 42px; flex: none;
  border-radius: 50%;
  overflow: hidden;
  display: grid; place-items: center;
  font-weight: 700; font-size: 17px; color: #fff;
  background: var(--acc-1);
}
.profile-ava img { width: 100%; height: 100%; object-fit: cover; display: block; grid-area: 1 / 1; opacity: 0; transition: opacity .45s ease; }
.profile-ava img.on { opacity: 1; }

/* ── Скелетоны загрузки главной + плавное появление контента ─────────────── */
.sk { display: block; background: var(--sk); border-radius: 16px; position: relative; overflow: hidden; }
.sk::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--sk-shine), transparent);
  animation: sk-shimmer 1.25s infinite;
}
@keyframes sk-shimmer { to { transform: translateX(100%); } }
/* Размеры плейсхолдеров = реальным блокам главной; отступы даёт gap:14px экрана,
   поэтому margin у скелетонов НЕ ставим (иначе двойной интервал и «съезд»). */
.sk-chip  { width: 150px; height: 56px; border-radius: 28px; }
.sk-about { flex: 1 1 auto; height: 56px; border-radius: 28px; }
.sk-tbtn  { width: 56px; height: 56px; border-radius: 50%; }
.sk-hero  { height: 224px; border-radius: var(--r-xl); }
.sk-card  { height: 168px; border-radius: var(--r-lg); }
.sk-row   { height: 74px;  border-radius: var(--r-lg); }
/* Слой морфинга: шиммеры лежат поверх готовой главной и перетекают в её реальные
   блоки (_morphHome в js/app.js), после чего гаснут — и из-под них проступает
   контент. Слой ПРОЗРАЧНЫЙ: контент проявляется прямо под ним, поэтому мигания
   пустотой нет. Координаты, размеры, скругления и переходы шиммеров ставит JS. */
.reveal-ov {
  position: absolute; inset: 0; z-index: 4;
  pointer-events: none;
}
.reveal-ov .sk { position: absolute; }
@media (prefers-reduced-motion: reduce) {
  .sk::after { animation: none; }
  .reveal-ov .sk { transition: opacity .2s ease !important; }
  .screen { transition: none !important; }
}
.profile-name {
  font-size: 17px; font-weight: 700; letter-spacing: -.3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.profile-chip .chev { color: var(--text-3); width: 19px; height: 19px; flex: none; }

/* Кнопка «О нас» — в стиле профиля, растягивается между профилем и темой */
.about-chip {
  flex: 1 1 auto;
  /* Не даём кнопке схлопнуться: при длинном имени профиля она сжималась почти в
     ноль и «О нас» вставало в две строки друг на друга. Ужимается пусть профиль —
     у него имя с многоточием. */
  min-width: 96px;
  white-space: nowrap;
  height: 56px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 14px;
  border-radius: 100px;
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  color: var(--text);
  font-size: 16px; font-weight: 700; letter-spacing: -.2px;
  transition: transform .16s ease;
}
.about-chip:active { transform: scale(.96); }

/* ── Экраны ── */
#view {
  flex: 1; padding: calc(14px + var(--safe-t)) 16px calc(var(--tabbar-h) + var(--safe-b) + 42px);
  /* Глубина под шторкой, как в iOS: страница чуть отъезжает назад. transform на
     #view безопасен — фиксированные таббар и шторки живут вне него. */
  transform-origin: 50% 0;
  transition: transform var(--dur-sheet) var(--ease-spring);
  /* Все экраны лежат в ОДНОЙ grid-ячейке (.screen → grid-area:1/1). Поэтому при
     смене вкладки/страницы уходящий может гаснуть НАД входящим без absolute и без
     «мигания». align-items:start — чтобы экраны не растягивались до высоты соседа.
     grid-template-columns: minmax(0,1fr) — ОБЯЗАТЕЛЬНО: без него неявная колонка
     auto-размера раздувается до max-content самого широкого блока (длинное имя,
     ссылка) и растягивает страницу по горизонтали. minmax(0,…) держит колонку по
     ширине #view, а контент переносится/скроллится внутри блока, как и раньше. */
  display: grid; grid-template-columns: minmax(0, 1fr); align-items: start;
}
body.sheet-open #view { transform: scale(.955) translateY(4px); }
/* Кривая cubic-bezier(.16,1,.3,1) — фирменная «эппловская» плавность (экспо-
   затухание): контент быстро появляется и мягко успокаивается на месте. */
.screen { position: relative; display: flex; flex-direction: column; gap: 14px; grid-area: 1 / 1; min-width: 0; animation: screen-in .42s cubic-bezier(.16, 1, .3, 1); }
.screen[hidden] { display: none; }
/* Появление, а не «вылет»: мягкое проявление с почти незаметным приближением */
@keyframes screen-in {
  from { opacity: 0; transform: scale(.992); }
  to   { opacity: 1; transform: none; }
}
/* Уходящий экран при смене страницы плавно гаснет НАД входящим (см. #view grid),
   а не исчезает мгновенно — зеркало screen-in. forwards держит конечный кадр,
   пока JS не спрячет экран. */
.screen.screen-out { pointer-events: none; animation: screen-out .34s cubic-bezier(.16, 1, .3, 1) forwards; }
@keyframes screen-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(.992); }
}
@media (prefers-reduced-motion: reduce) { .screen, .screen.screen-out { animation: none; } }

.section-title {
  font-size: 14px; font-weight: 700;
  color: var(--text-2);
  letter-spacing: .2px;
  margin: 8px 4px 0;
  display: flex; align-items: center; gap: 7px;
}
.section-title .icon { width: 17px; height: 17px; stroke-width: 2; }

/* ── Карточки ── */
.card {
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: 18px;
  box-shadow: var(--shadow-soft);
  position: relative;
  overflow: hidden;
}

/* Hero-карта тарифа (Apple Wallet-стиль) */
.hero-card {
  border-radius: var(--r-xl);
  padding: 20px;
  color: #fff;
  background: var(--grad-acc);
  box-shadow: var(--shadow-soft), inset 0 1px 1.5px rgba(255,255,255,.45);
  position: relative;
  overflow: hidden;
}
.hero-card::before {
  content: '';
  position: absolute;
  width: 300px; height: 300px;
  top: -150px; right: -90px;
  border-radius: 50%;
  /* Без filter: blur — на старых Chromium/Qt (Telegram Desktop, macOS) filter
     у ребёнка прорывает клип по скруглённому углу карты (overflow:hidden +
     border-radius), и блик вылезает за правый верхний угол. Градиент и так
     мягкий, поэтому blur не нужен. */
  background: radial-gradient(circle at 35% 35%, rgba(255,255,255,.32), rgba(255,255,255,.05) 60%, transparent 70%);
}
.hero-card::after {
  content: '';
  position: absolute;
  width: 210px; height: 210px;
  bottom: -120px; left: -70px;
  border-radius: 50%;
  background: radial-gradient(circle at 60% 40%, rgba(255,255,255,.16), transparent 65%);
}
.hero-top { display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1; }
.hero-plan { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15.5px; letter-spacing: -.1px; }
.hero-plan .icon { width: 19px; height: 19px; stroke-width: 2; }
.hero-days { position: relative; z-index: 1; margin-top: 20px; font-size: 40px; font-weight: 800; letter-spacing: -1.4px; line-height: 1; }
.hero-days small { font-size: 17px; font-weight: 600; letter-spacing: 0; opacity: .82; margin-left: 6px; }
.hero-sub { position: relative; z-index: 1; margin-top: 7px; font-size: 13.5px; opacity: .86; font-weight: 500; }
.hero-foot {
  position: relative; z-index: 1;
  margin-top: 18px; padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, .22);
  display: flex; gap: 16px;
  font-size: 12.5px; font-weight: 600; opacity: .92;
}
.hero-foot span { display: flex; align-items: center; gap: 6px; }
.hero-foot .icon { width: 15px; height: 15px; stroke-width: 2.1; }
.hero-shield {
  position: absolute; right: 14px; bottom: -26px; z-index: 0;
  width: 120px; height: 120px;
  color: rgba(255, 255, 255, .13);
}
.hero-shield .icon { width: 100%; height: 100%; stroke-width: 1.1; }

/* Чипы */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px;
  border-radius: 100px;
  font-size: 12px; font-weight: 700;
  letter-spacing: .2px;
}
.chip .icon { width: 13px; height: 13px; stroke-width: 2.4; }
/* Без backdrop-filter: у элемента с backdrop-filter внутри карты со
   скруглением + overflow:hidden блюр не клипуется по радиусу и вылезает
   квадратом за угол (в тёмной теме особенно заметно справа сверху).
   Матовость даём полупрозрачной заливкой и внутренним бликом. */
.chip-glass {
  color: #fff;
  background: rgba(255, 255, 255, .18);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.35);
}
.chip-ok   { color: var(--ok);   background: color-mix(in srgb, var(--ok) 13%, transparent); }
.chip-warn { color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent); }
.chip-bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 12%, transparent); }
.chip-acc  { color: var(--text); background: color-mix(in srgb, var(--acc-1) 11%, transparent); }
/* «Популярно» — контрастный бейдж (тёмный фон + светлый текст), чтобы не
   сливался с карточкой тарифа. Тема-зависимый: в тёмной теме инвертируется. */
.chip-popular { color: var(--surface); background: var(--text); box-shadow: 0 2px 6px rgba(20,28,66,.18); }

/* Карточка тарифа «Трафик» */
.plan-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.plan-name { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 15.5px; }
.plan-ico {
  width: 40px; height: 40px;
  border-radius: 14px;
  display: grid; place-items: center;
  color: var(--text);
  background: var(--grad-acc-soft);
}
.plan-ico .icon { width: 21px; height: 21px; }
.plan-sub { font-size: 12.5px; color: var(--text-2); font-weight: 500; margin-top: 2px; }

/* Кнопки */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 18px;
  border-radius: var(--r-sm);
  font-weight: 700; font-size: 14.5px;
  transition: transform .16s ease, box-shadow .2s ease, opacity .2s ease;
}
.btn:active { transform: scale(.965); }
.btn .icon { width: 18px; height: 18px; stroke-width: 2.1; }
.btn-primary {
  color: #fff;
  background: var(--grad-acc);
  box-shadow: inset 0 1px 1.5px rgba(255,255,255,.45);
}
/* Стеклянная кнопка внутри hero-карты (на градиенте) */
.btn-hero {
  position: relative; z-index: 1;
  margin-top: 16px;
  color: #fff;
  background: rgba(255, 255, 255, .2);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
  backdrop-filter: blur(10px) saturate(1.4);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.42), inset 0 -1px 1px rgba(255,255,255,.12);
}
.btn-soft { color: var(--text); background: color-mix(in srgb, var(--acc-1) 9%, transparent); }
.btn-ghost { color: var(--text-2); background: var(--surface-2); }
.btn-danger { color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }
.btn-block { width: 100%; }
.btn-sm { padding: 10px 14px; font-size: 13.5px; border-radius: 13px; }

/* Экран «не удалось загрузить» — восстановимая ошибка вместо белого экрана, когда
   ядро (getCore) не пришло. Показывается из boot() → _showBootError. */
.boot-error {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; padding: 64px 24px 0;
}
.boot-error-emoji { font-size: 40px; line-height: 1; margin-bottom: 6px; }
.boot-error-title { font-weight: 700; font-size: 17px; color: var(--text); }
.boot-error-sub   { font-size: 14px; color: var(--text-2); max-width: 260px; }
.boot-error .btn  { margin-top: 18px; min-width: 160px; }

/* Ховер для мыши (ПК/десктоп Telegram) — ОДИНАКОВЫЙ ВЕЗДЕ по построению:
   поверх элемента лежит вуаль ::after, на ховере она плавно набирает одну и ту
   же прозрачность с одной и той же скоростью. Цвет вуали — currentColor, т.е.
   ЦВЕТ ТЕКСТА САМОГО элемента: текст по определению контрастен своему фону,
   поэтому вуаль одинаково заметна на любом фоне (белая на синих кнопках,
   тёмная на серых плашках, светлая в тёмной теме). Прежняя вуаль var(--text)
   была тёмной и на тёмно-синих кнопках почти исчезала — «в каждом меню ховер
   разной силы». border-radius: inherit повторяет скругление хозяина. */
@media (hover: hover) and (pointer: fine) {
  /* Панели «жидкого стекла» (вкладки таббара, кнопки периода) вуали НЕ получают:
     у них своя интерактивность (капля/портал), подсветка поверх стекла — чужеродна.
     Строки-меню (оферта/политика, роутинг) вуаль СНОВА получают — скруглённой
     таблеткой (правило в конце блока), без острых углов и без клипа. */
  :is(.btn, .icon-btn, .sheet-close, .row.selectable, .invite-link,
      .profile-chip, .about-chip, .theme-btn, .action-tile,
      .task-card.task-open, .key-link, .set-row, .route-row,
      .support-card, .nav-row) {
    position: relative;
  }
  /* Списки — через :where() (нулевая специфичность), чтобы точечные правила ниже
     по файлу (глушилка вуали у .key-link .icon-btn, скруглённые таблетки строк)
     всегда выигрывали. */
  :where(.btn, .icon-btn, .sheet-close, .row.selectable, .invite-link,
      .profile-chip, .about-chip, .theme-btn, .action-tile,
      .task-card.task-open, .key-link, .set-row, .route-row,
      .support-card, .nav-row)::after {
    content: '';
    position: absolute; inset: 0;
    border-radius: inherit;
    background: currentColor;
    opacity: 0;
    transition: opacity .18s ease;
    pointer-events: none;
  }
  :where(.btn, .icon-btn, .sheet-close, .row.selectable, .invite-link,
      .profile-chip, .about-chip, .theme-btn, .action-tile,
      .task-card.task-open, .key-link, .set-row, .route-row,
      .support-card, .nav-row):hover::after {
    opacity: .08;
  }
  /* Плоские строки-меню (оферта/политика в профиле, пункты роутинга, строка
     «Тип роутинга» на главной): вуаль — таблетка ВО ВСЮ ШИРИНУ строки (как в
     iOS-списках), с вертикальным отступом, чтобы не липнуть к разделителям.
     Горизонтальный inset НУЛЕВОЙ (строка и так внутри отступов карточки, до её
     overflow-клипа скруглённого угла вуаль не достаёт — потому раньше и «клипало»
     при отрицательном выносе). У .route-row верх опущен ниже её разделителя. */
  /* Таблетка обнимает КОНТЕНТ строки с равными полями сверху и снизу. Ключевое:
     у строк РАЗНЫЕ вертикальные отступы, поэтому единый inset давал кривой размер.
     .set-row: padding 11px сверху И снизу → симметрично, inset 4px.
     .route-row: padding-top 14px, а СНИЗУ ОТСТУПА НЕТ (иконка 38px упирается в
     нижний край) — прежний inset снизу 4px ОБРЕЗАЛ низ контента, отсюда «не по
     размеру». Теперь верх 9px (5px над иконкой) и низ −5px (5px под иконкой, уходит
     в нижний паддинг карточки, до её скруглённого угла не достаёт). */
  /* Ширину таблетки увеличили: она выходит на 6px за строку с каждой стороны
     (строка стоит в 14px от края карточки → таблетка в 8px). БОЛЬШЕ НЕЛЬЗЯ: у
     .card радиус 28px и overflow:hidden, и у ПЕРВОГО ряда угол карточки начинает
     срезать таблетку в острую кромку (это и был баг «острые углы»). Предел по
     геометрии — около −7px; берём −6px с запасом. */
  .set-row::after { inset: 4px -6px; border-radius: 14px; }
  .route-opt::after { inset: 4px 0; border-radius: 14px; }
  .route-row::after { inset: 9px -6px -5px; border-radius: 14px; }
  /* Ряд с ключом на главной ховерится ЦЕЛИКОМ (как ряд с ID в профиле) — вуаль
     вложенной кнопки-копии глушим, иначе при наведении она темнела бы дважды. */
  .key-link .icon-btn::after { display: none; }
}

.icon-btn {
  width: 42px; height: 42px; flex: none;
  border-radius: 14px;
  display: grid; place-items: center;
  color: var(--text);
  background: color-mix(in srgb, var(--acc-1) 9%, transparent);
  transition: transform .16s ease;
}
.icon-btn:active { transform: scale(.92); }
.icon-btn .icon { width: 19px; height: 19px; stroke-width: 2; }

/* Ключ доступа */
.key-link {
  display: flex; align-items: center; gap: 10px;
  margin-top: 14px;
  padding: 12px 12px 12px 15px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  box-shadow: inset 0 1px 2px rgba(20,28,66,.05);
  cursor: pointer;   /* копирует ключ тапом по ВСЕЙ строке (как ряд с ID в профиле) */
}
.key-link code {
  flex: 1; min-width: 0;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.action-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 13px; }

/* Строка выбора типа роутинга внутри карточки ключа */
.route-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--line);
  transition: opacity .15s ease;
}
.route-row:active { opacity: .6; }
.route-ico {
  width: 38px; height: 38px; flex: none;
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--text); background: var(--grad-acc-soft);
}
.route-ico .icon { width: 20px; height: 20px; }
.route-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.route-text > span { font-size: 11.5px; color: var(--text-3); font-weight: 600; }
.route-text b { font-size: 14.5px; letter-spacing: -.1px; }
.route-row .chev { color: var(--text-3); width: 19px; height: 19px; }

/* Прокси для Telegram */
.proxy-intro { font-size: 13px; line-height: 1.5; color: var(--text-2); font-weight: 500; margin-bottom: 14px; }
.proxy-row { gap: 11px; }
.proxy-flag { font-size: 25px; flex: none; line-height: 1; width: 30px; text-align: center; }
.proxy-row .row-main b { font-size: 14.5px; }
.proxy-go { flex: none; padding: 9px 16px; }

/* Опции роутинга в шторке (с описанием) */
.route-opt { align-items: flex-start; }
.route-opt .row-main b { font-size: 15px; }
.route-opt .row-main span { display: block; margin-top: 3px; font-size: 12.5px; line-height: 1.45; color: var(--text-2); font-weight: 500; white-space: normal; }
.route-opt .radio-dot { margin-top: 3px; }
.action-tile {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 13px 6px 11px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-size: 12px; font-weight: 650; color: var(--text-2);
  transition: transform .16s ease;
}
.action-tile:active { transform: scale(.95); }
.action-tile .icon { width: 21px; height: 21px; color: var(--text); }

/* Карточка поддержки (главная) — обычная белая, иконка с мягкой заливкой */
.support-card {
  display: flex; align-items: center; gap: 14px;
  width: 100%; text-align: left;
  padding: 15px 16px;
  transition: transform .16s ease;
}
.support-card:active { transform: scale(.98); }
.support-ico {
  width: 44px; height: 44px; flex: none;
  border-radius: 14px;
  display: grid; place-items: center;
  color: var(--text); background: var(--grad-acc-soft);
}
.support-ico .icon { width: 23px; height: 23px; }
.support-text { flex: 1; min-width: 0; }
.support-text b { display: block; font-size: 15px; letter-spacing: -.15px; }
.support-text span { display: block; margin-top: 2px; font-size: 12.5px; color: var(--text-2); font-weight: 500; }
.support-card .chev { color: var(--text-3); width: 19px; height: 19px; }

/* ── «Чего-то не хватает?» — карта-приглашение написать в поддержку ────────
   Полностью нейтральная: подложка --surface, как у всех карточек главной.
   Внимание держат крупный заголовок и контрастная кнопка, а не цвет фона.
   position+overflow клипуют водяной знак по радиусу. */
.wish-card {
  display: flex; flex-direction: column; align-items: flex-start;
  width: 100%; text-align: left;
  border-radius: var(--r-xl);
  padding: 20px;
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  position: relative;
  overflow: hidden;
  transition: transform .16s ease;
}
.wish-card:active { transform: scale(.98); }
.wish-title {
  position: relative; z-index: 1;
  color: var(--text);
  font-size: 21px; font-weight: 800; letter-spacing: -.5px; line-height: 1.15;
}
.wish-sub {
  position: relative; z-index: 1;
  margin-top: 8px; max-width: 32ch;
  color: var(--text-2);
  font-size: 13.5px; font-weight: 500; line-height: 1.45;
}
.wish-cta {
  position: relative; z-index: 1;
  margin-top: 16px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 11px 16px;
  border-radius: 100px;
  /* Контрастная нейтральная кнопка (приём .chip-popular): тёмная на светлой
     теме, светлая на тёмной. */
  color: var(--surface);
  background: var(--text);
  font-size: 14px; font-weight: 700; letter-spacing: -.1px;
  box-shadow: 0 4px 12px rgba(20, 28, 66, .18);
}
.wish-cta .icon { width: 17px; height: 17px; stroke-width: 2.1; }
.wish-glow {
  position: absolute; right: -18px; bottom: -30px; z-index: 0;
  width: 132px; height: 132px;
  color: color-mix(in srgb, var(--text) 7%, transparent);
}
.wish-glow .icon { width: 100%; height: 100%; stroke-width: 1.1; }

/* ── Рефералка: лаконичная карточка приглашения ── */
.invite { display: flex; flex-direction: column; gap: 12px; }
.invite-title { font-size: 13px; font-weight: 650; color: var(--text-2); letter-spacing: -.1px; }
.invite-link {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left;
  padding: 13px 13px 13px 16px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  transition: transform .16s ease;
}
.invite-link:active { transform: scale(.985); }
/* Блок «Ваш ID» — без отклика на нажатие: при копировании он сжимался и тут же
   разжимался обратно, из-за чего вся карточка дёргалась. Отклик даём тактильный
   (hap) и тостом, а не движением. Так же гасим transition, чтобы на WebView с
   «липким» :active не осталось подвисшего кадра анимации. */
.invite-link.uid-link { transition: none; }
.invite-link.uid-link:active { transform: none; }
.invite-link code {
  flex: 1; min-width: 0;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12.5px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.invite-copy { flex: none; color: var(--text); display: grid; place-items: center; }
.invite-copy .icon { width: 19px; height: 19px; stroke-width: 2; }

/* ── Минималистичная навигационная строка (Управление промокодами) ── */
.nav-row {
  display: flex; align-items: center; gap: 13px;
  width: 100%; text-align: left;
  padding: 15px 16px;
  transition: transform .16s ease;
}
.nav-row:active { transform: scale(.98); }
.nav-ico {
  width: 38px; height: 38px; flex: none;
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--text);
  background: color-mix(in srgb, var(--acc-1) 9%, transparent);
}
.nav-ico .icon { width: 19px; height: 19px; }
.nav-label { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 650; letter-spacing: -.1px; }
.nav-row .chev { color: var(--text-3); width: 19px; height: 19px; }

/* ── Подстраница (Промокоды) — поверх вкладки, таббар остаётся ── */
.empty-card {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 5px; padding: 30px 22px;
}
.empty-ico {
  width: 52px; height: 52px; margin-bottom: 8px;
  border-radius: 16px;
  display: grid; place-items: center;
  color: var(--text);
  background: var(--grad-acc-soft);
}
.empty-ico .icon { width: 25px; height: 25px; }
.empty-card b { font-size: 15px; letter-spacing: -.15px; }
.empty-card span { font-size: 13px; color: var(--text-2); font-weight: 500; line-height: 1.5; }

/* Действия в hero-блоке промокодов */
.hero-actions { position: relative; z-index: 1; margin-top: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.hero-actions .btn { margin-top: 0; width: 100%; white-space: nowrap; }
.hero-default { margin-top: 10px; width: 100%; }

/* Карточка статистики промокодов: заголовок + пилюля периода + метрики */
.stats-card { padding: 16px 18px 18px; }
.stats-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.stats-title { font-size: 15px; font-weight: 750; letter-spacing: -.2px; }
.period-pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 7px 10px 7px 13px;
  border-radius: 100px;
  font-size: 13px; font-weight: 650; color: var(--text);
  background: var(--surface-2);
  transition: transform .15s ease;
}
.period-pill:active { transform: scale(.95); }
.period-pill .chev { width: 15px; height: 15px; color: var(--text-3); transform: rotate(90deg); stroke-width: 2.4; }

.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 16px; }
.metric { display: flex; flex-direction: column; gap: 4px; align-items: center; text-align: center; position: relative; }
.metric + .metric::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  height: 30px; width: 1px; background: var(--line);
}
.metric b { font-size: 19px; font-weight: 800; letter-spacing: -.6px; white-space: nowrap; }
.metric span { font-size: 11.5px; color: var(--text-3); font-weight: 600; }

/* Метка «по-умолчанию» у промокода */
.tag-default {
  display: inline-block; vertical-align: middle;
  margin-left: 6px; padding: 2px 7px;
  border-radius: 7px;
  font-size: 10px; font-weight: 700; letter-spacing: .2px;
  color: var(--text);
  background: color-mix(in srgb, var(--acc-1) 11%, transparent);
}

/* Премиальный список выбора (период статистики) */
.opt-list { display: flex; flex-direction: column; gap: 8px; }
.opt {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left;
  padding: 15px 16px;
  border-radius: var(--r-sm);
  font-size: 15px; font-weight: 600; letter-spacing: -.1px;
  color: var(--text-2);
  background: var(--surface-2);
  transition: background .15s ease, transform .15s ease, color .15s ease;
}
.opt:active { transform: scale(.985); }
.opt .opt-check { width: 20px; height: 20px; flex: none; color: var(--text); opacity: 0; stroke-width: 2.4; }
.opt.sel { color: var(--text); background: color-mix(in srgb, var(--acc-1) 9%, transparent); font-weight: 700; }
.opt.sel .opt-check { opacity: 1; }
.opt-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.opt-main b { font-size: 15px; font-weight: 700; letter-spacing: -.1px; }
.opt-main span { font-size: 12px; color: var(--text-3); font-weight: 600; }

/* Метка «по-умолчанию» у промокода */
.tag-default {
  display: inline-block; vertical-align: middle;
  margin-left: 6px; padding: 2px 7px;
  border-radius: 7px;
  font-size: 10px; font-weight: 700; letter-spacing: .2px;
  color: var(--text);
  background: color-mix(in srgb, var(--acc-1) 11%, transparent);
}

/* Списки-строки */
.row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 4px;
}
.row + .row { border-top: 1px solid var(--line); }
.row-ico {
  width: 40px; height: 40px; flex: none;
  border-radius: 13px;
  display: grid; place-items: center;
  color: var(--text);
  background: var(--grad-acc-soft);
}
.row-ico .icon { width: 20px; height: 20px; }
.row-main { flex: 1; min-width: 0; }
.row-main b { display: block; font-size: 14px; letter-spacing: -.1px; }
.row-main span { font-size: 12px; color: var(--text-3); font-weight: 500; }
.row-side { font-size: 13px; font-weight: 700; color: var(--text-2); display: flex; align-items: center; gap: 6px; }
.row-side .icon { width: 16px; height: 16px; color: var(--text-3); }
.row.selectable { border-radius: var(--r-sm); padding: 13px 10px; transition: background .15s ease, transform .15s ease; }
/* Нажатие — ТОЛЬКО сжатие, никакой заливки. Это была ПОСЛЕДНЯЯ фоновая подсветка
   по нажатию во всём файле: на WebView с липким :active серый филл замерзал на
   тапнутой строке и выглядел «застрявшим бликом», который не переезжает. */
.row.selectable:active { transform: scale(.985); }
/* Фолбэк первой строкой: WebView без color-mix отбрасывал вторую и выбранная
   строка оставалась ВООБЩЕ без подсветки — казалось, что «подсветка не переехала». */
.row.selected { background: rgba(9, 144, 222, .09); }
.row.selected { background: color-mix(in srgb, var(--acc-1) 7%, transparent); }

/* ── Роутинг: подсветка выбора = ОДНА нейтральная пилюля, которая ПЕРЕЕЗЖАЕТ ──
   Контейнер НАМЕРЕННО плоский, без .card: у карточки в тёмной теме в shadow-soft
   зашит inset-ободок (1px белого по периметру) — та самая «внешняя обводка»,
   которая выглядела застрявшей рамкой выбора вокруг обеих опций и переживала
   любые перерисовки строк. Теперь и фон, и обводку несёт ТОЛЬКО подвижная
   пилюля; строки не красятся никогда, разделителей нет. */
.route-list { position: relative; padding: 2px 0; }
.route-list .row { position: relative; z-index: 1; }
.route-list .row + .row { border-top: 0; }
.route-list .row.selected,
.route-list .row.selectable:active { background: transparent; }
.route-list .radio-dot { width: 24px; height: 24px; }
.route-pill {
  position: absolute; left: 0; top: 0;
  z-index: 0; opacity: 0;
  border-radius: var(--r-sm);
  background: var(--surface-2);                 /* нейтральный, как в списках iOS */
  box-shadow: inset 0 0 0 1px var(--line);      /* обводка — НА подвижном элементе */
  pointer-events: none;
  will-change: transform;
  transition: transform .34s cubic-bezier(.22, .9, .28, 1),
              height .25s ease, width .25s ease, left .25s ease, opacity .2s ease;
}
.row.selected .row-ico { color: #fff; background: var(--acc-1); }

.radio-dot {
  width: 22px; height: 22px; flex: none;
  border-radius: 50%;
  border: 2px solid var(--text-3);
  display: grid; place-items: center;
  transition: border-color .18s ease, background-color .18s ease;
}
.row.selected .radio-dot { border-color: var(--text); background: var(--acc-1); }
.radio-dot::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #fff; opacity: 0; transform: scale(.4); transition: opacity .18s ease, transform .18s ease; }
.row.selected .radio-dot::after { opacity: 1; transform: none; }

/* ── Лидерборд ── */
/* Аватар участника (инициал или фото) */
.ava {
  position: relative;
  width: 40px; height: 40px; flex: none;
  border-radius: 50%;
  display: grid; place-items: center;
  color: #fff; font-size: 15px; font-weight: 700; letter-spacing: -.2px;
  background-size: cover; background-position: center;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.05);
  overflow: hidden;
}
/* Фото перекрывает букву-заглушку целиком; при onerror img удаляется — буква видна */
.ava img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* Место в топе — маленький бейдж на углу аватара (аватар занял «место») */
.lb-ava { position: relative; flex: none; line-height: 0; }
.lb-badge {
  position: absolute; right: -4px; bottom: -4px;
  min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: 10px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums;
  color: var(--text-2);
  background: var(--surface);
  border: 2px solid var(--surface);
  box-shadow: 0 2px 6px rgba(20,28,66,.16);
}
.lb-badge.me-badge { color: #fff; background: var(--acc-1); border-color: var(--text); }
.lb-row .row-main b { font-size: 14.5px; }
/* Скролл-производительность на слабых Android: строка — изолированная область
   отрисовки, браузер не перерасчитывает/не перерисовывает соседей при скролле */
.lb-row { contain: layout paint; }

/* Переключатель периода (неделя / месяц / год / всё время) */
/* ── Переключатель периода = МЛАДШИЙ БРАТ ТАББАРА ─────────────────────────
   Тот же рецепт один в один: .liquid-glass с настоящим SVG-преломлением через
   LiquidGlass.attach (Chromium) или чистым блюром (.lg-static, WebKit), та же
   капля .drop-glass со спекулярным сводом, та же сетка равных колонок. Никаких
   собственных заливок и отключённых слоёв — прошлые самодельные варианты и
   выглядели «не как таббар». */
/* Обёртка переключателя: как .tabbar-wrap — держит бегунок СНАРУЖИ панели
   (у .lbseg.liquid-glass overflow:hidden резал лифт бегунка за границы) */
.lbseg-wrap { position: relative; margin-top: 6px; }
.lbseg {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  align-items: stretch;
  height: 54px;
  padding: 7px;
  border-radius: 100px;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  cursor: grab;
}
/* Свечение изнутри — БЕЗ ИСКЛЮЧЕНИЙ. У контрола над плоским фоном гасим все три
   источника света базового стекла:
   1) прибавку яркости в backdrop-фильтре контейнера (brightness до 1.1 —
      равномерно «подсвечивала» весь контрол изнутри);
   2) нарисованные фаску и кольцо (::before с brightness 1.12–1.22 и белыми
      inset-тенями) — на ПК они ЖИЛИ до первого resize: attach() пропускает
      инициализацию, пока вкладка спрятана (размер 0), и lg-класс не ставился;
   3) спекуляр SVG-фильтра выключается в JS (spec: 0). */
.lbseg.liquid-glass {
  -webkit-backdrop-filter: blur(2.5px) saturate(1.4);
  backdrop-filter: blur(2.5px) saturate(1.4);
}
.lbseg.liquid-glass::before,
.lbseg.liquid-glass::after { display: none; }
.lbseg-drop {
  position: absolute; left: 0; top: 0;
  z-index: 4; pointer-events: none;   /* НАД кнопками (как капля таббара): стекло лежит поверх зажжённого пункта */
  will-change: transform;
  transition: transform .55s cubic-bezier(.3, 1.35, .35, 1), width .3s ease, height .3s ease;
}
/* Бегунок теперь СОСЕД панели (внутри .lbseg-wrap) — селекторы через «+» */
.lbseg.drag + .lbseg-drop { transition: none; }
.lbseg.drag + .lbseg-drop .drop-glass {
  /* Захват: ядро пружинисто набухает ПО ОБЕИМ осям и держит размер весь драг —
     как у капли таббара. Раньше стоял scale(1.12, 1) (только ширина), из-за чего
     бегунок при удержании вообще не подрастал по высоте. Рост идёт симметрично от
     центра и целиком помещается в 7px паддинга панели, а сама капля — сосед панели
     (.lbseg-wrap), её ничто не клипует. */
  transform: scale(1.12);
  box-shadow: var(--drop-shadow), 0 8px 20px rgba(20, 28, 66, .18);
  animation: drop-grab-seg .3s;
  transition: box-shadow .36s ease;
}
.lbseg-drop.drop-release .drop-glass { animation: drop-release-seg .44s; }
/* Мгновенный отклик на нажатие и продолжение с его формы — как у капли таббара
   (см. .tab-drop.press / drop-grab-tab), но под свой scale 1.12. */
.lbseg-drop.press .drop-glass {
  transform: scale(1.05, .988);
  transition: transform .13s cubic-bezier(.3, 0, .3, 1);
}
@keyframes drop-grab-seg {
  0%   { transform: scale(1.05, .988);  animation-timing-function: cubic-bezier(.2, 1.4, .36, 1); }
  100% { transform: scale(1.12); }
}
@keyframes drop-release-seg {
  /* Старт = форма и хвост ядра в момент отпускания — см. drop-release-tab */
  0%   { transform: translateX(var(--rel-t, 0%)) scale(var(--rel-x, 1.12), var(--rel-y, 1.12));
                                        animation-timing-function: cubic-bezier(.22, .72, .3, 1); }
  62%  { transform: translateX(0%) scale(.99, 1.008);
                                        animation-timing-function: cubic-bezier(.33, 0, .3, 1); }
  100% { transform: translateX(0%) scale(1); }
}
/* Пока плашку тянут — прижатый пункт не сжимается своим :active, иначе кнопка,
   с которой начали жест, оставалась «втянутой» весь драг («размер сам меняется») */
.lbseg.drag button:active { transform: none; }
.lbseg button {
  position: relative; z-index: 3;
  border-radius: 100px;
  font-size: 12.5px; font-weight: 650; letter-spacing: -.1px;
  color: var(--text-2);
  white-space: nowrap;
  transition: color .22s ease, transform .15s ease;
}
.lbseg button:active { transform: scale(.94); }
/* Вариант A (как у таббара): копии-линзы под бегунком больше нет. «Зажигается»
   сам выбранный пункт — .on в покое, .lit при перетаскивании (ведёт JS по
   ближайшему). Стекло без blur — подпись под ним читается, второго рисунка нет,
   значит нет и двоения. */
.lbseg button.on,
.lbseg button.lit { color: var(--text); }
/* При драге прежний .on, если он уже НЕ под каплей, гасим — иначе горели бы два */
.lbseg.drag button.on:not(.lit) { color: var(--text-2); }

/* Подиум топ-3 */
.lb-podium {
  display: grid;
  grid-template-columns: 1fr 1.14fr 1fr;
  gap: 10px;
  align-items: end;
}
.pod {
  position: relative;
  text-align: center;
  background: var(--surface);
  border-radius: 22px;
  padding: 22px 6px 14px;
  box-shadow: var(--shadow-soft);
}
.pod-ava-wrap { position: relative; width: 52px; margin: 0 auto 8px; line-height: 0; }
.pod-ava { width: 52px; height: 52px; font-size: 19px; margin: 0 auto; }
.pod-medal {
  position: absolute;
  right: -5px; bottom: -5px;
  width: 24px; height: 24px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 800;
  border: 2.5px solid var(--surface);
}
.pod.p1 { padding-top: 28px; padding-bottom: 18px; background: linear-gradient(180deg, rgba(255, 199, 84, .2), transparent 62%), var(--surface); }
.pod.p1 .pod-ava-wrap { width: 62px; margin-bottom: 10px; }
.pod.p1 .pod-ava { width: 62px; height: 62px; font-size: 23px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.05), 0 0 0 3px rgba(244,178,33,.55); }
.pod.p1 .pod-medal { width: 27px; height: 27px; font-size: 13px; color: #7A5200; background: var(--gold-grad); box-shadow: 0 4px 10px rgba(244, 178, 33, .45), inset 0 1px 2px rgba(255,255,255,.65); }
.pod.p2 { background: linear-gradient(180deg, rgba(148, 163, 190, .14), transparent 60%), var(--surface); }
.pod.p2 .pod-ava { box-shadow: inset 0 0 0 1px rgba(0,0,0,.05), 0 0 0 3px rgba(148,163,190,.5); }
.pod.p2 .pod-medal { color: #4E586B; background: var(--silver-grad); box-shadow: 0 3px 8px rgba(120, 136, 165, .35), inset 0 1px 2px rgba(255,255,255,.7); }
.pod.p3 { background: linear-gradient(180deg, rgba(212, 138, 66, .13), transparent 60%), var(--surface); }
.pod.p3 .pod-ava { box-shadow: inset 0 0 0 1px rgba(0,0,0,.05), 0 0 0 3px rgba(212,138,66,.5); }
.pod.p3 .pod-medal { color: #7B4315; background: var(--bronze-grad); box-shadow: 0 3px 8px rgba(196, 128, 62, .35), inset 0 1px 2px rgba(255,255,255,.55); }
.pod-name {
  font-size: 13px; font-weight: 750; letter-spacing: -.1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding: 0 4px;
}
.pod.p1 .pod-name { font-size: 14px; }
.pod-count {
  display: inline-flex; align-items: center; gap: 4px;
  margin-top: 4px;
  font-size: 12px; font-weight: 700;
  color: var(--text-2);
}
.pod-count .icon { width: 13px; height: 13px; stroke-width: 2.2; }
.pod-prize { margin-top: 8px; }
.chip-gold {
  color: #7A5200;
  background: var(--gold-grad);
  box-shadow: 0 4px 10px rgba(244, 178, 33, .3), inset 0 1px 1px rgba(255,255,255,.55);
}

/* Задания */
.task-card { position: relative; display: flex; flex-direction: column; gap: 12px; }
/* Невыполненное задание — вся карточка и есть кнопка: стрелка справа по центру
   (как у строк-настроек), под неё освобождено место справа */
.task-card.task-open { cursor: pointer; padding-right: 46px; }
.task-card .task-chev {
  position: absolute; right: 15px; top: 50%;
  transform: translateY(-50%);
  color: var(--text-3); width: 20px; height: 20px;
}
.task-head { display: flex; align-items: flex-start; gap: 12px; }
.task-num {
  width: 42px; height: 42px; flex: none;
  border-radius: 14px;
  display: grid; place-items: center;
  font-weight: 800; font-size: 16px;
  color: var(--text);
  background: var(--grad-acc-soft);
}
.task-num.done { color: #fff; background: var(--ok); }
.task-num.done .icon { width: 20px; height: 20px; stroke-width: 2.4; }
.task-title { font-size: 14.5px; font-weight: 700; letter-spacing: -.1px; line-height: 1.35; }
.task-reward { display: inline-flex; margin-top: 5px; }
.task-progress { display: flex; align-items: center; gap: 12px; }
.task-bar { flex: 1; height: 9px; border-radius: 100px; background: var(--surface-2); overflow: hidden; box-shadow: inset 0 1px 2px rgba(20,28,66,.06); }
.task-fill { height: 100%; border-radius: 100px; background: var(--acc-1); transition: width .8s cubic-bezier(.22,.9,.3,1); }
.task-fill.done { background: var(--ok); }
.task-count { font-size: 12.5px; font-weight: 700; color: var(--text-2); white-space: nowrap; }

/* ── «Плати по миру» — партнёрская страница (монохромный бренд партнёра) ── */
.pm-hero {
  position: relative; overflow: hidden;
  border-radius: var(--r-lg);
  padding: 22px 20px 24px;
  color: #fff;
  background: linear-gradient(160deg, #262A33 0%, #12141A 60%, #0A0C11 100%);
  box-shadow: var(--shadow-soft);
}
.pm-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px; border-radius: 100px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .2px;
  background: rgba(255, 255, 255, .12);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.pm-badge .icon { width: 14px; height: 14px; stroke-width: 2; }
.pm-h { margin-top: 14px; font-size: 27px; font-weight: 800; letter-spacing: -.6px; }
.pm-sub { margin-top: 8px; max-width: 32ch; font-size: 13.5px; line-height: 1.5; font-weight: 500; opacity: .82; }

/* Декоративная карта */
.pm-card {
  position: relative; overflow: hidden;
  margin-top: 20px; height: 156px;
  border-radius: 18px;
  background: linear-gradient(135deg, #3C424E 0%, #191C23 100%);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .45), inset 0 1px 1px rgba(255, 255, 255, .12);
}
.pm-card::after {
  content: ''; position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 100% 0, rgba(255, 255, 255, .16), transparent 55%);
}
.pm-card > span { position: absolute; z-index: 1; }
.pm-card-brand { top: 16px; left: 18px; font-size: 12px; font-weight: 800; letter-spacing: 1.6px; }
.pm-card-chip {
  top: 50px; left: 18px; width: 38px; height: 28px; border-radius: 7px;
  background: linear-gradient(135deg, #EAD08A, #B8923F);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .25);
}
.pm-card-num { bottom: 40px; left: 18px; font-size: 16.5px; font-weight: 600; letter-spacing: 2px; font-variant-numeric: tabular-nums; }
.pm-card-sys { bottom: 16px; right: 18px; font-size: 15px; font-weight: 800; font-style: italic; letter-spacing: .5px; }

.pm-cta {
  margin-top: 20px; width: 100%;
  color: #14161C; background: #fff;
  box-shadow: 0 8px 22px rgba(0, 0, 0, .28);
}
.pm-cta:active { transform: scale(.97); }

.pm-stats { display: grid; grid-template-columns: repeat(3, 1fr); padding: 16px 8px; }
.pm-stats > div { position: relative; text-align: center; }
.pm-stats > div + div::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 1px; height: 26px; background: var(--line);
}
.pm-stats b { display: block; font-size: 20px; font-weight: 800; letter-spacing: -.5px; }
.pm-stats span { font-size: 11.5px; color: var(--text-3); font-weight: 600; }

.pm-note { font-size: 11.5px; color: var(--text-3); line-height: 1.5; font-weight: 500; text-align: center; padding: 0 10px; }

/* ── Таббар (liquid glass) ── */
/* Позиционирование живёт на ОБЁРТКЕ (overflow: visible!): сама панель —
   .liquid-glass с overflow:hidden (клип для Android-блюра), и пока капля была
   её ребёнком, «поднятие» физически не могло выйти за границы панели. Капля
   теперь сосед панели внутри обёртки — геометрия та же (обёртка == панель),
   а клип панели её больше не касается. */
.tabbar-wrap {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--safe-b) + 12px);
  z-index: 60;
  width: min(calc(100% - 28px), 420px);
  height: var(--tabbar-h);
  /* Число вкладок: панель раскладывает их равными колонками (grid-auto-columns).
     Дефолтную ширину капли считаем от этого же числа, а не от жёсткой «3» —
     JS (boot) выставляет актуальное значение по видимым вкладкам. */
  --tabs: 3;
}
.tabbar {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 36px;
  display: grid;
  /* Колонки создаются по числу вкладок (одна равная колонка на вкладку), а не
     фиксировано под 4 — так 3 вкладки растягиваются на всю панель без пустого
     места, а при возврате «Плати по миру» станет снова 4. .tab-drop вне потока
     (position:absolute), поэтому в раскладке колонок не участвует. */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  align-items: stretch;
  padding: 8px;
}
.tab {
  position: relative;
  z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  border-radius: 26px;
  color: var(--text-2);
  font-weight: 650;
  transition: color .22s ease, transform .15s ease;
}
.tab span { font-size: 10px; letter-spacing: 0; white-space: nowrap; }
.tab:active { transform: scale(.94); }
/* Пока плашку тянут — прижатая вкладка не сжимается своим :active, иначе
   вкладка, с которой начали жест, оставалась «втянутой» весь драг */
.tabbar.dragging .tab:active { transform: none; }
.tab .icon { width: 22px; height: 22px; transition: transform .25s cubic-bezier(.34,1.56,.64,1); }
/* Вариант A: копии-линзы под каплей больше НЕТ (она двоила/кривила текст на
   стыке). «Зажигается» сама настоящая вкладка под каплей: в покое — .active, в
   перетаскивании — .lit (её ведёт JS по ближайшей). Стекло капли просто едет
   поверх зажжённой вкладки и слегка её преломляет — второго рисунка текста нет,
   значит нет и двоения. Иконки без transform: смена состояния — только цвет и
   толщина обводки, без «подпрыгивания». */
.tab.active,
.tab.lit { color: var(--text); }
.tab.active .icon,
.tab.lit .icon { stroke-width: 2.1; }
/* Во время драга семантически активную вкладку, если она сейчас НЕ под каплей,
   гасим до неактивной — иначе горели бы две (прежняя активная + та, что под
   каплей). Ровно одна .lit ведётся из JS. */
.tabbar.dragging .tab.active:not(.lit) { color: var(--text-2); }
.tabbar.dragging .tab.active:not(.lit) .icon { stroke-width: 1.8; }

/* Капля-индикатор активной вкладки: стеклянная плашка, которая ЕДЕТ (translateX)
   к активной вкладке. Вариант A: копии-линзы внутри больше нет — «зажжённость»
   даёт сама вкладка под каплей (.active/.lit), поэтому нет второго рисунка текста
   и двоения на стыке. Позицию и размер задаёт moveDrop() в app.js. */
.tab-drop {
  position: absolute;
  top: 8px; left: 0;
  height: calc(100% - 16px);
  width: calc((100% - 16px) / var(--tabs, 3));
  /* НАД вкладками (у .tab z-index:3): стекло лежит поверх зажжённой вкладки и
     слегка её преломляет. Под вкладками капля читалась бы как «сломанная» —
     серый текст кнопки рисовался бы поверх стекла. */
  z-index: 4;
  pointer-events: none;
  /* Скрыта, пока JS (moveDrop) не поставит её под активную вкладку. Иначе за
     время асинхронного старта (await темы/состояния) браузер успевает
     отрисовать каплю на CSS-дефолте (left:0) — эффект «появилась слева». */
  opacity: 0;
  will-change: transform, opacity;
  transition:
    transform .55s cubic-bezier(.3, 1.35, .35, 1),
    width .3s ease,
    height .3s ease,
    opacity .25s ease;
}
.tab-drop.placed { opacity: 1; }
/* Ядро ОБЕИХ капель (таббар и период) — полупрозрачная стеклянная плашка:
   градиентная вуаль + фирменные блики + мягкая тень. КЛЮЧЕВОЙ ИНВАРИАНТ: капля
   обязана читаться БЕЗ backdrop-filter. В WebKit вложенный backdrop-filter
   (капля стоит НАД стеклом панели, у которого свой backdrop-filter) не
   применяется вовсе — вся «стеклянность»/яркость от него там пропадает, и в
   тёмной теме индикатор становился почти невидимым. Поэтому объём держим на
   заливке, кромке и тени, а не на фильтре. blur НЕ ставим — иначе подпись под
   стеклом расплывается (а у периода ещё и двоится с копией в линзе). У ТАББАРА
   копии нет (вариант A): под каплей зажигается сама вкладка. У ПЕРИОДА копия-
   линза пока остаётся. SVG-«варп по кромке» (LiquidGlass.attach на .drop-glass)
   — лишь бонус в Chromium, где он поддерживается. */
.drop-glass {
  position: absolute;
  inset: 0;
  display: block;
  border-radius: 100px;
  background: linear-gradient(180deg, rgba(255, 255, 255, .30), rgba(255, 255, 255, .06) 52%, rgba(255, 255, 255, .15));
  box-shadow: var(--drop-shadow);
  -webkit-backdrop-filter: saturate(1.8) brightness(1.06);
  backdrop-filter: saturate(1.8) brightness(1.06);
  will-change: transform;
  /* Возврат на ОТПУСКАНИИ и посадка после желе — заметная, но честная пружина.
     Крайности обе плохи: 1.6 проскакивало сильно НИЖЕ единицы (капля сжималась в
     «щепотку» — нереалистично), 1.25 почти не читалось («анимации нет»). 1.42 —
     ощутимый отскок без пережима. */
  transition: transform .46s cubic-bezier(.28, 1.42, .38, 1);
}
/* У ПОДВИЖНОЙ капли кромки-линзы НЕТ (по просьбе): варп по границам остаётся
   только у ФОНОВОЙ панели (.liquid-glass::before). Капля — чистая стеклянная
   плашка: заливка + блик + тень, без искажения контура. */
/* Верхний спекулярный свод — объём линзы (в тёмной теме гасим) */
.drop-glass::after {
  content: ''; position: absolute;
  left: 9%; right: 9%; top: 2px; height: 46%;
  border-radius: 100px;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(180deg, rgba(255, 255, 255, .9), rgba(255, 255, 255, 0) 92%);
  opacity: .5;
}
:root[data-theme="dark"] .drop-glass {
  /* Чуть прозрачнее (.14 → .10) и без светлого контура — капля мягче. Выбор
     всё равно виден по зажжённой вкладке под ней (вариант A). */
  background: rgba(255, 255, 255, .10);
}
/* В тёмной теме гасим только спекулярный блик (::after). Кромка-линза (::before)
   ОСТАЁТСЯ — это и есть варп по границам, он нужен в обеих темах. */
:root[data-theme="dark"] .drop-glass::after { opacity: 0; }
/* Захват: зажали каплю → ядро подрастает и становится «интерактивным», каплю
   можно тянуть между вкладками. Внешний слой в это время без перехода — едет
   точно за пальцем; ядро масштабируется отдельно (пружинистая посадка).
   Лифт заметно тянется по ВЕРТИКАЛИ и выходит за границы панели (панель ничего
   не клипует) — как у Apple; полёт по клику при этом жёсткий, без сплющивания. */
/* Фаза полёта: ядро вытягивается по ходу движения и сплющивается, как жидкость;
   позиция при этом не искажается — тянется только ядро, внешний слой едет ровно.
   Обратно ядро пружинит базовым переходом .drop-glass (overshoot). Это и есть
   «упругая» капля; вернули после того, как копия-линза ушла (вариант A) — теперь
   деформироваться нечему рассинхронизироваться с текстом. */
.tab-drop.drop-stretch .drop-glass,
.lbseg-drop.drop-stretch .drop-glass {
  transform: scale(1.22, .84);
  transition: transform .2s cubic-bezier(.4, 0, .6, 1);
}
.tab-drop.drag { transition: none; cursor: grabbing; }
.tab-drop.drag .drop-glass {
  /* Захват: ядро набухает и держит этот размер весь драг; скоростную деформацию
     ведёт _jelly (JS-пружина) ПОСЛЕ того, как отыграет анимация захвата —
     инлайновый transform с transition:none, включённый раньше, рвал бы её на
     полувзлёте. Конечный кадр анимации РАВЕН transform ниже, поэтому анимация
     идёт без fill-mode (иначе она перебивала бы инлайновое желе), и по её
     окончании ядро остаётся ровно там же — стыка не видно. */
  transform: scale(1.17);
  box-shadow: var(--drop-shadow), 0 12px 30px rgba(20, 28, 66, .22);
  animation: drop-grab-tab .3s;
  transition: box-shadow .36s ease;
}
.tab-drop.drop-release .drop-glass { animation: drop-release-tab .44s; }
/* НАЖАТИЕ: отклик СРАЗУ по pointerdown, ещё до того как жест признан удержанием.
   Перетаскивание включается только через 240мс (holdTimer), и раньше весь набух
   ждал этого момента — палец уже держит, а капля ещё мертва, и пружина
   приезжала заметно позже нажатия. Теперь капля отвечает мгновенно: чуть
   собирается (сжатие по высоте ЕЛЕ заметное, 1.5%) и слегка растёт. */
.tab-drop.press .drop-glass {
  transform: scale(1.06, .985);
  transition: transform .13s cubic-bezier(.3, 0, .3, 1);
}
/* Захват (240мс): продолжаем РОВНО с формы нажатия — 0%-кадр равен .press, иначе
   капля скакнула бы обратно к 1 и набухла заново. Один пружинистый рост, без
   отдельной фазы «вдоха»: вдох уже отыгран мгновенным .press. */
@keyframes drop-grab-tab {
  0%   { transform: scale(1.06, .985);  animation-timing-function: cubic-bezier(.2, 1.4, .36, 1); }
  100% { transform: scale(1.17); }
}
/* Отпускание: спокойное оседание с ОДНИМ мягким недолётом, а не серия качаний.
   Четыре кадра «туда-сюда» за .54s читались как дрожь, да и подскок вверх на
   30% выглядел странно для только что положенной капли. Теперь капля ровно
   опадает, чуть проседает по ширине с отдачей вверх — и садится на место. */
/* Список transform-функций во ВСЕХ кадрах одинаков (translateX + scale): при
   разной длине списков CSS уходит в матричную интерполяцию, и покачивание
   плывёт. Хвост (--rel-t) с формой вместе гасится к нулю. */
@keyframes drop-release-tab {
  /* Старт = форма ядра в момент отпускания (её ставит _dropRelease); без броска
     это ровно scale захвата, после броска — растянутая желейная форма с хвостом. */
  0%   { transform: translateX(var(--rel-t, 0%)) scale(var(--rel-x, 1.17), var(--rel-y, 1.17));
                                        animation-timing-function: cubic-bezier(.22, .72, .3, 1); }
  62%  { transform: translateX(0%) scale(.985, 1.012);
                                        animation-timing-function: cubic-bezier(.33, 0, .3, 1); }
  100% { transform: translateX(0%) scale(1); }
}
/* На ПК показываем, что каплю можно зажать и потянуть */
.tabbar { cursor: grab; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
@media (prefers-reduced-motion: reduce) {
  .tab-drop, .drop-glass, .tab-drop.drag .drop-glass,
  .tab-drop.drop-stretch .drop-glass, .lbseg-drop.drop-stretch .drop-glass,
  .lbseg-drop, .lbseg.drag + .lbseg-drop .drop-glass,
  .tab-drop.drop-release .drop-glass, .lbseg-drop.drop-release .drop-glass,
  .tab-drop.press .drop-glass, .lbseg-drop.press .drop-glass {
    transition: none; transform: none; animation: none;
  }
}

/* ── Шторки (bottom sheets) ── */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 80;
  /* Цвет затемнения — по теме: старый rgba(12,15,30,…) — тёмно-СИНИЙ, и в
     тёмной теме он подсинивал весь фон главной под шторкой. */
  background: var(--backdrop);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  opacity: 0;
  will-change: opacity; /* слой готов заранее — фейд без рывка первого кадра */
  /* На той же пружине, что и сама шторка: затемнение, отстающее по кривой от
     того, что оно затемняет, читается как рассинхрон. */
  transition: opacity var(--dur-sheet) var(--ease-spring);
}
.sheet-backdrop.show { opacity: 1; }
/* Пока открыта шторка — фон под ней не скроллится. */
body.sheet-open { overflow: hidden; }
/* ⚠️ Тот же замок нужен и админке. `.adm-stack` — position:fixed поверх главной,
   но САМА ГЛАВНАЯ под ней остаётся прокручиваемой: вход в панель снимает
   sheet-open (см. openAdmin → closeSheet), и документ снова свободен.

   На macOS это видно и ощущается как баг: колесо/тачпад над панелью, если
   `.adm-scroll` прокрутку не забрал (контент влез целиком или уже упёрся в
   конец), уходит документу. Ползунок окна едет, главная листается ЗА непрозрачной
   панелью, а панель — fixed, поэтому стоит намертво. Читается как «страница не
   двигается, а внизу что-то невидимое».
   `overscroll-behavior-y: contain` у `.adm-scroll` тут не спасает: он про цепочку
   ОТ скроллера, а когда содержимое влезает, скроллера для колеса просто нет.
   На iOS не заметно — оверлейные полосы и тач-путь другие.

   Ширину это не дёргает: `scrollbar-gutter: stable` у body держит место под
   полосу всегда — ровно ради таких переключений. */
body.adm-open { overflow: hidden; }

/* Шторка «как в iOS»: во всю ширину, вплотную к низу и бокам — по краям просвета
   нет, скруглены только верхние углы, зазор виден лишь сверху. Высотой управляет
   JS: две ступени — обычная (по контенту) и раскрытая на весь экран. --x это
   прогресс раскрытия 0..1: за ним тянутся отступы и кегль внутри шторки, поэтому
   «разворачивание» привязано прямо к пальцу (см. _makeSheetDraggable). */
.sheet {
  --x: 0;
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 90;
  width: 100%;
  max-height: calc(var(--tg-viewport-stable-height, 100vh) - var(--sheet-top) - var(--safe-t));
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: 32px 32px 0 0;
  /* Клип по скруглённому прямоугольнику: на Qt/Chromium Telegram Desktop без
     этого прокручиваемые дети .sheet-body вырисовываются за границей шторки
     («выплывают снизу экрана»). Собственная тень шторки при overflow сохраняется. */
  overflow: hidden;
  box-shadow: var(--sheet-shadow);
  transform: translateY(100%);
  will-change: transform, height; /* высота и выезд — на компоузере */
  transition: transform var(--dur-sheet) var(--ease-spring),
              height var(--dur-sheet) var(--ease-spring);
}
.sheet.open { transform: translateY(0); }
/* Верхняя «ручка»: полоска тонкая, но зона захвата — во всю ширину и 26px по
   высоте, чтобы за неё легко было ухватиться пальцем. Вверх — раскрыть шторку на
   весь экран, вниз — свернуть и закрыть. touch-action:none — это жест, а не скролл.
   Шапка, ручка и тело растут вместе с раскрытием: их размеры ведёт --x, поэтому
   контент разворачивается ровно по ходу пальца, а не рывком после отпускания. */
.sheet-grab {
  /* Ручка растворяется при раскрытии — её зона СЖИМАЕТСЯ (26 → 12px), а не
     растёт: освободившееся место забирают заголовок и крестик, поднимаясь
     вверх. Всё на --x, т.е. едет прямо за пальцем во время раскрытия. */
  align-self: stretch; height: calc(26px - 14px * var(--x)); flex: none;
  display: flex; align-items: center; justify-content: center;
  touch-action: none; cursor: grab;
}
.sheet-grab::before {
  content: '';
  width: calc(40px + 8px * var(--x)); height: 4.5px;
  border-radius: 100px; background: var(--line);
  /* как в iOS: на весь экран ручка растворяется (тянуть можно по-прежнему) */
  opacity: calc(1 - var(--x));
}
.sheet-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* Верхний отступ при раскрытии не растёт, а слегка поджимается: шапка
     занимает место исчезнувшей ручки — контенту достаётся больше экрана */
  padding: calc(10px - 4px * var(--x)) calc(18px + 6px * var(--x)) calc(4px + 8px * var(--x));
  flex: none; touch-action: none;
}
.sheet-head h3 { font-size: calc(19px + 12px * var(--x)); font-weight: 800; letter-spacing: -.4px; }
/* Кнопка закрытия — серый круг как в iOS; крестик компактный и плотный.
   При раскрытии слегка подрастает вместе с заголовком. */
.sheet-close {
  color: var(--text-2); background: var(--surface-2);
  width: calc(32px + 4px * var(--x)); height: calc(32px + 4px * var(--x));
  border-radius: 50%;
  display: grid; place-items: center; flex: none;
  transition: transform .15s ease, opacity .15s ease;
}
.sheet-close:active { transform: scale(.9); opacity: .7; }
.sheet-close .icon { width: 16px; height: 16px; stroke-width: 2.6; }
.sheet-close .icon { width: 15px; height: 15px; stroke-width: 2.3; display: block; }
.sheet-body {
  padding: calc(12px + 8px * var(--x)) calc(18px + 6px * var(--x)) calc(18px + var(--safe-b));
  overflow-y: auto; overscroll-behavior: contain;
  flex: 1 1 auto; min-height: 0;   /* тело скроллится внутри шторки фиксированной высоты */
  font-size: calc(15px + 1px * var(--x));
  transition: opacity .15s ease;   /* кроссфейд контента при замене шага (replaceSheet) */
}
/* Подвал с кнопкой-продолжением — СВОЯ кнопка вместо нативного tg.MainButton
   (тот рисуется хромом Telegram: прямоугольный, на чужой полосе-подложке под
   нашей шторкой). Здесь — наши цвета, наши скругления, свой safe-area. */
.sheet-foot {
  flex: none;
  padding: 10px 18px calc(14px + var(--safe-b));
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.sheet-foot .btn { border-radius: 18px; padding: 15px 18px; font-size: 15px; }
/* Когда подвал есть, нижний отступ тела не нужен (safe-area уже в подвале) */
.sheet:has(.sheet-foot) .sheet-body { padding-bottom: 10px; }
/* Гейт «подписка на канал»: две кнопки столбиком. Главная (на канал) — крупная,
   с нейтральным «плавающим» свечением (палитра CTA нейтральна в обеих темах),
   чтобы бросалась в глаза без листания; вторая — проверка членства. */
.sheet-foot.gate-foot { display: flex; flex-direction: column; gap: 9px; }
.gate-foot .gate-cta {
  font-size: 15.5px; font-weight: 800; letter-spacing: -.2px;
  box-shadow: 0 12px 28px rgba(0,0,0,.22);
}
.gate-foot .gate-cta:active { box-shadow: 0 6px 16px rgba(0,0,0,.18); }
.sheet-note { font-size: 12.5px; color: var(--text-3); line-height: 1.5; font-weight: 500; }

/* ── Продление подписки: список тарифов как отдельные карточки-опции ── */
.plan-list { display: flex; flex-direction: column; gap: 9px; }
.plan-list .row.selectable {
  padding: 14px 15px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1.5px solid transparent;
  transition: border-color .18s ease, background .18s ease, transform .15s ease;
}
.plan-list .row.selectable:active { transform: scale(.985); }
.plan-list .row.selected {
  background: color-mix(in srgb, var(--acc-1) 8%, var(--surface));
  border-color: var(--text);
}

/* ── Скидка win-back: премиальный ценник «было → стало» ───────────────────
   Акцент ЗОЛОТОЙ, а не синий: синие фоны в этом дизайне намеренно погашены
   (см. «ПОЛНАЯ НЕЙТРАЛИЗАЦИЯ СИНИХ ФОНОВ» ниже), синим остаются только hero-
   карты. Золото — язык призовых бейджей (.chip-gold): премиально и уместно
   для ограниченного предложения.

   Строка со скидкой подсвечена тёплым ореолом из правого верхнего угла (приём
   .pay-card: position+overflow клипуют его по радиусу). Ореол не спорит с
   состоянием .selected — та несёт свою рамку. */
.plan-list .row.offer { position: relative; overflow: hidden; }
.plan-list .row.offer::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(130% 105% at 100% 0%,
              rgba(244, 178, 33, .14) 0%, transparent 62%);
}
.plan-list .row.offer > * { position: relative; z-index: 1; }

/* Бейдж «−34%» — золотой градиент с мягким свечением.
   ВАЖНО: селектор включает .row-main. Бейдж — это <span> внутри .row-main, а
   правило `.row-main span` (0,1,1) специфичнее одиночного `.chip-off` (0,1,0)
   и перекрашивало текст в серый --text-3 весом 500 — из-за этого бейдж был
   блёклым и почти не читался. Через .row-main .chip-off (0,2,0) мы выигрываем. */
.row-main .chip-off {
  color: #7A5200;
  font-weight: 800;
  letter-spacing: .3px;
  background: var(--gold-grad);
  box-shadow: 0 4px 10px rgba(244, 178, 33, .30), inset 0 1px 1px rgba(255, 255, 255, .55);
}

/* Ценник: старая цена сверху (зачёркнута, приглушена), новая — крупная снизу.
   Колонка с выравниванием вправо, поэтому переопределяем flex из .row-side. */
.row-side.price-off {
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  line-height: 1.15;
}
.row-side.price-off .was {
  font-size: 12px; font-weight: 600; color: var(--text-3);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--text-3) 65%, transparent);
}
.row-side.price-off .now {
  font-size: 18px; font-weight: 800; letter-spacing: -.35px; color: var(--text);
}

/* ── Оплата: премиальная карточка суммы + пункты доверия ── */
.pay-card {
  position: relative; overflow: hidden;
  border-radius: var(--r-lg);
  padding: 22px 20px 20px;
  color: #fff; text-align: center;
  background: var(--grad-acc);
  box-shadow: var(--shadow-soft), inset 0 1px 1.5px rgba(255, 255, 255, .4);
}
.pay-card::before {
  content: ''; position: absolute;
  width: 260px; height: 260px; top: -150px; right: -80px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, rgba(255, 255, 255, .34), rgba(255, 255, 255, .04) 62%, transparent 70%);
}
.pay-card-label { position: relative; z-index: 1; font-size: 11.5px; font-weight: 600; letter-spacing: .8px; text-transform: uppercase; opacity: .85; }
.pay-card-sum { position: relative; z-index: 1; font-size: 42px; font-weight: 800; letter-spacing: -1.5px; line-height: 1.04; margin-top: 6px; }
.pay-card-plan { position: relative; z-index: 1; font-size: 13.5px; font-weight: 500; opacity: .9; margin-top: 7px; }
.pay-card-glow { position: absolute; right: -14px; bottom: -24px; z-index: 0; opacity: .16; color: #fff; }
.pay-card-glow .icon { width: 120px; height: 120px; stroke-width: 1.1; }

.trust-list { display: flex; flex-direction: column; margin-top: 16px; }
.trust { display: flex; align-items: center; gap: 12px; padding: 11px 2px; }
.trust + .trust { border-top: 1px solid var(--line); }
.t-ico {
  width: 38px; height: 38px; flex: none;
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--text); background: var(--grad-acc-soft);
}
.t-ico .icon { width: 19px; height: 19px; }
.trust > div { min-width: 0; }
.trust b { display: block; font-size: 14px; letter-spacing: -.1px; }
.trust > div > span { display: block; margin-top: 1px; font-size: 12px; color: var(--text-3); font-weight: 500; }

/* Выбор тарифа в шторке */
.plan-pick {
  display: flex; align-items: center; gap: 13px;
  width: 100%;
  padding: 16px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  text-align: left;
  margin-bottom: 11px;
  transition: transform .16s ease, box-shadow .2s ease;
}
.plan-pick:active { transform: scale(.97); }
.plan-pick .plan-ico { width: 46px; height: 46px; border-radius: 16px; }
.plan-pick b { font-size: 15px; display: block; letter-spacing: -.15px; }
.plan-pick span { font-size: 12px; color: var(--text-2); font-weight: 500; line-height: 1.4; display: block; margin-top: 2px; }
.plan-pick .icon.chev { color: var(--text-3); width: 18px; height: 18px; }

/* Инпуты */
.field { margin: 12px 0; }
.field label { display: block; font-size: 12.5px; font-weight: 700; color: var(--text-2); margin: 0 4px 7px; }
.field input {
  width: 100%;
  padding: 14px 16px;
  border-radius: var(--r-sm);
  border: 1.5px solid transparent;
  background: var(--surface-2);
  font-size: 15px; font-weight: 600;
  outline: none;
  transition: border-color .2s ease, box-shadow .2s ease;
}
.field input:focus { border-color: var(--text); box-shadow: 0 0 0 4px color-mix(in srgb, var(--text) 10%, transparent); }
.field input::placeholder { color: var(--text-3); font-weight: 500; }

/* Итог оплаты */
.total-line {
  display: flex; justify-content: space-between; align-items: center;
  padding: 15px 16px;
  border-radius: var(--r-sm);
  background: var(--grad-acc-soft);
  font-weight: 700; font-size: 14px;
  margin-top: 10px;
}
.total-line b { font-size: 19px; font-weight: 800; letter-spacing: -.4px; color: var(--text); }

/* QR. Код генерируется в высоком разрешении (512px, см. openQRSheet), а на экран
   выводится размером по --x: при раскрытии шторки он вырастает почти во всю ширину,
   оставаясь резким (это уменьшение исходника, а не растягивание). Потолок в vw и
   max-width страхуют от вылезания за края в полноэкранном режиме. */
.qr-wrap {
  display: grid; place-items: center;
  margin: calc(12px + 4px * var(--x)) auto;
  /* при раскрытии рамку УЖИМАЕМ — освободившееся место отдаём самому коду */
  padding: calc(18px - 4px * var(--x));
  width: fit-content; max-width: 100%;
  border-radius: calc(var(--r-md) + 6px * var(--x));
  background: #fff;
  box-shadow: var(--shadow-soft);
}
.qr-wrap img, .qr-wrap canvas {
  border-radius: 8px; display: block;
  /* Растёт почти во всю ширину. Потолок в vw считает отступы тела шторки и рамки,
     поэтому в полноэкранном режиме код не вылезает за края. */
  width:  min(calc(210px + 260px * var(--x)), calc(100vw - 84px)) !important;
  height: min(calc(210px + 260px * var(--x)), calc(100vw - 84px)) !important;
}

/* Шаги инструкции */
.step-row { display: flex; gap: 13px; padding: 12px 0; }
.step-row + .step-row { border-top: 1px solid var(--line); }
.step-n {
  width: 30px; height: 30px; flex: none;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 13.5px; font-weight: 800;
  color: var(--text);
  background: var(--grad-acc-soft);
}
.step-row p { font-size: 13.5px; line-height: 1.5; color: var(--text-2); font-weight: 500; padding-top: 4px; }
.step-row p b { color: var(--text); }

/* Список устройств: прокручивается, если их много (15+) */
.dev-list {
  max-height: 46vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0 -4px;
  padding: 0 4px;
}

/* Меню профиля: шапка + список настроек (стиль как в настройках Telegram) */
/* Шапка профиля в «Настройках». При раскрытии шторки аватар вырастает и уезжает в
   центр, а имя с ником спускаются под него: «строка» непрерывно превращается в
   «портрет». Ведёт всё --x, поэтому перестроение идёт ровно за пальцем. Дети
   позиционированы абсолютно намеренно — flex-direction переходом не анимируется,
   а через left/top/transform раскладку можно менять плавно, без скачка. */
.prof-head {
  position: relative;
  height: calc(54px + 189px * var(--x));       /* 54 → 243: аватар + имя под ним */
  margin: 2px 2px calc(16px + 10px * var(--x));
}
.prof-ava {
  position: absolute; top: 0;
  left: calc(50% * var(--x));                  /* слева → по центру */
  transform: translateX(calc(-50% * var(--x)));
  width:  calc(54px + 116px * var(--x));       /* 54 → 170: «действительно большой» */
  height: calc(54px + 116px * var(--x));
  font-size: calc(21px + 34px * var(--x));     /* инициал под фото */
}
.prof-id {
  position: absolute;
  width: max-content;
  max-width: calc(100% - 76px + 68px * var(--x));
  text-align: left;                            /* имя и @username от ОДНОГО левого края */
  left: calc(68px + (50% - 68px) * var(--x));  /* справа от аватара → по центру */
  top:  calc(27px + 161px * var(--x));         /* по центру строки → под аватаром */
  transform: translateX(calc(-50% * var(--x)))
             translateY(calc(-50% + 50% * var(--x)));
}
.prof-id b {
  display: block; font-size: calc(18px + 9px * var(--x)); font-weight: 750; letter-spacing: -.3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.prof-id span {
  display: block; margin-top: calc(1px + 5px * var(--x));
  font-size: calc(13px + 4px * var(--x)); color: var(--text-3); font-weight: 500;
  /* @username по левому краю, ровно под именем (1:1, только меньше кеглем) */
}

/* Статистика в «Профиле» — РОВНО карточки «О нас» (.feat), только компактнее и
   в две колонки: белая поверхность, мягкая тень, иконка на сером скруглённом
   квадрате слева, справа значение и подпись. Без бликов и «сфер». */
.stat-grid {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  margin-bottom: 14px;
}
.stat-cell {
  display: flex; align-items: center; gap: 11px;
  padding: calc(12px + 2px * var(--x)) 13px;
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  min-width: 0;
}
.stat-ico {
  width: calc(38px + 2px * var(--x)); height: calc(38px + 2px * var(--x)); flex: none;
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--text);
  background: var(--surface-2);
}
.stat-ico .icon { width: calc(20px + 1px * var(--x)); height: calc(20px + 1px * var(--x)); }
.stat-body { min-width: 0; }
.stat-body b {
  display: block;
  font-size: calc(15.5px + 2px * var(--x)); font-weight: 800; letter-spacing: -.3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat-body span {
  display: block; margin-top: 1px;
  font-size: calc(11px + 1px * var(--x)); color: var(--text-2); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Оферта и политика — служебные строки, они НЕ должны раздуваться при раскрытии.
   Растут едва заметно, только чтобы не выглядеть мелкими рядом с крупной шапкой. */
.set-list { padding: calc(6px + 2px * var(--x)) calc(14px + 2px * var(--x)); }
.set-row {
  display: flex; align-items: center; gap: calc(13px + 1px * var(--x));
  width: 100%; text-align: left;
  padding: calc(11px + 2px * var(--x)) 2px;
  /* Тот же отклик, что у `.nav-row`. Раньше строка шторки НЕ поджималась вовсе,
     а просто гасла до 55 % — единственное место в приложении, где нажатие
     выглядело иначе, отчего шторка ощущалась чужой. */
  transition: transform .16s ease;
}
.set-row + .set-row { border-top: 1px solid var(--line); }
.set-row:active { transform: scale(.98); }
.set-ico {
  width: calc(38px + 3px * var(--x)); height: calc(38px + 3px * var(--x)); flex: none;
  border-radius: calc(11px + 1px * var(--x));
  display: grid; place-items: center;
  color: #fff;
}
.set-ico .icon { width: calc(21px + 1px * var(--x)); height: calc(21px + 1px * var(--x)); stroke-width: 2; }
.set-label { flex: 1; min-width: 0; font-size: calc(15px + 1px * var(--x)); font-weight: 600; letter-spacing: -.1px; }
.set-row .chev { color: var(--text-3); width: 19px; height: 19px; flex: none; }

/* Успех */
.success-box { text-align: center; padding: 12px 0 6px; }
.success-ico {
  width: 74px; height: 74px;
  margin: 6px auto 16px;
  border-radius: 50%;
  display: grid; place-items: center;
  color: #fff;
  background: linear-gradient(135deg, #2EBD85, #1FA873);
  box-shadow: 0 14px 32px rgba(46,189,133,.38), inset 0 1px 2px rgba(255,255,255,.5);
  animation: pop-in .45s cubic-bezier(.34,1.56,.64,1);
}
.success-ico .icon { width: 34px; height: 34px; stroke-width: 2.4; }
@keyframes pop-in { from { transform: scale(.4); opacity: 0; } to { transform: none; opacity: 1; } }
.success-box h4 { font-size: 18px; font-weight: 800; letter-spacing: -.3px; margin-bottom: 7px; }
.success-box p { font-size: 13.5px; color: var(--text-2); line-height: 1.55; font-weight: 500; }

/* Тост */
/* Своё подтверждение вместо нативного диалога Telegram */
.confirm-actions { display: flex; gap: 10px; margin-top: 18px; }
.confirm-actions .btn { flex: 1; min-width: 0; }
.btn-danger { color: #fff; background: var(--bad); }

.toast {
  position: fixed;
  left: 50%; bottom: calc(var(--tabbar-h) + var(--safe-b) + 30px);
  transform: translate(-50%, 16px);
  z-index: 120;
  /* На десктопе плашка вылезала за край окна: ограничиваем ширину вьюпортом и
     разрешаем перенос — теперь она всегда целиком на экране. */
  max-width: min(calc(100vw - 32px), 420px);
  text-align: center;
  padding: 12px 20px;
  border-radius: 100px;
  /* Цвета ПОВЕРХНОСТИ, не инверсия. Раньше плашка была инвертированной
     (фон = --text): в тёмной теме это БЕЛАЯ плашка — читалась как «телеграмовский
     попап, который игнорирует тему». Теперь тёмная тема → тёмная плашка. */
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  font-size: 13.5px; font-weight: 700;
  box-shadow: var(--shadow-float);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  transition: opacity .28s ease, transform .28s cubic-bezier(.22,.9,.3,1);
  white-space: nowrap;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* Кнопка-дублёр MainButton (только предпросмотр в браузере) */
/* Дублёр Telegram MainButton для браузера. MainButton живёт только в шторках,
   поэтому кнопка закреплена у нижнего края поверх шторки (z-index выше .sheet). */
.fallback-main {
  position: fixed;
  left: 50%; transform: translateX(-50%);
  bottom: calc(var(--safe-b) + 14px);
  z-index: 100;
  width: min(calc(100% - 32px), 432px);
  padding: 16px;
  border-radius: 19px;
  color: #fff;
  font-size: 15px; font-weight: 800; letter-spacing: -.1px;
  background: var(--grad-acc);
  box-shadow: var(--shadow-float), inset 0 1px 1.5px rgba(255,255,255,.5);
  transition: transform .16s ease;
}
.fallback-main:active { transform: translateX(-50%) scale(.97); }

/* Промо-баннер (создание промокода / пробный период) */
.banner {
  border-radius: var(--r-lg);
  padding: 17px;
  display: flex; align-items: center; gap: 13px;
  background: var(--grad-acc-soft);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.4), var(--shadow-soft);
  overflow: hidden;
  position: relative;
  text-align: left;
  width: 100%;
}
.banner-ico {
  width: 46px; height: 46px; flex: none;
  border-radius: 16px;
  display: grid; place-items: center;
  color: #fff;
  background: var(--acc-1);
}
.banner-ico .icon { width: 22px; height: 22px; }
/* Статичный баннер (просто блок, не кнопка) */
.banner-static { cursor: default; }
.banner-text { flex: 1; min-width: 0; }
.banner-text b { font-size: 14px; display: block; letter-spacing: -.1px; }
.banner-text span { font-size: 12px; color: var(--text-2); font-weight: 500; display: block; margin-top: 2px; line-height: 1.4; }
.banner .icon.chev { color: var(--text-3); width: 18px; height: 18px; margin-left: auto; }

/* Хелперы */
.mt-8 { margin-top: 8px; } .mt-12 { margin-top: 12px; } .mt-16 { margin-top: 16px; }
.muted { color: var(--text-3); font-size: 12px; font-weight: 500; }
.hidden { display: none !important; }
.stat-strip { display: flex; gap: 10px; }
.stat-box {
  flex: 1;
  background: var(--surface);
  border-radius: var(--r-md);
  padding: 14px;
  box-shadow: var(--shadow-soft);
  text-align: center;
}
.stat-box b { font-size: 21px; font-weight: 800; letter-spacing: -.5px; display: block; }
.stat-box b em { font-style: normal; color: var(--text); }
.stat-box span { font-size: 11.5px; color: var(--text-3); font-weight: 600; }

/* ── Нейтральные фоны у иконок-плашек (обе темы) ───────────────────────────
   Синие/акцентные плашки под иконками ломают картину — делаем их нейтрально-
   серыми в ОБЕИХ темах через тема-зависимый --surface-2 (светло-серый в
   светлой теме, тёмно-серый в тёмной). Глиф — нейтральный --text. */
.support-ico,
.nav-ico,
.empty-ico,
.row-ico,
.t-ico,
.step-n,
.banner-ico,
.task-num {
  background: var(--surface-2);
}
/* Плашки с белым глифом на сплошном цвете — на нейтральном фоне белый глиф
   пропал бы, поэтому красим его нейтральным --text. */
.banner-ico { color: var(--text); }
/* .set-ico красится инлайн-цветом в JS — перебиваем через !important. */
.set-ico { background: var(--surface-2) !important; color: var(--text) !important; }
/* Выбранная строка: был сплошной синий — теперь нейтральный. */
.row.selected .row-ico { color: var(--text); background: var(--surface-2); }
/* «Выполнено» у задания (.task-num.done) остаётся зелёным — это статус, не синий фон. */

/* ── Таблица лидеров: топ-3 построчно, со статусом ────────────────────────
   Медальный бейдж-место (золото/серебро/бронза) на аватаре + аккуратный
   приз «+1 месяц» под именем (мягкий золотистый лейбл, а не громкий пилл). */
.lb-badge.rank-1 { color: #7A5200; background: var(--gold-grad); }
.lb-badge.rank-2 { color: #4E586B; background: var(--silver-grad); }
.lb-badge.rank-3 { color: #7B4315; background: var(--bronze-grad); }
/* Приз «+1 месяц» у топ-3 — премиальный «золотой» бейдж: тёплый
   шампань-градиент, тонкая золотая кромка и внутренний блик (эффект
   металла), глубокий золотой текст. Не кричит, но читается как награда. */
.lb-prize {
  display: inline-flex; align-items: center; gap: 5px;
  margin-top: 5px; padding: 3px 10px 3px 8px;
  border-radius: 100px;
  font-size: 11px; font-weight: 700; letter-spacing: .1px;
  color: #7C5A16;
  background: linear-gradient(180deg, #FFF3D6 0%, #F6E1AE 100%);
  border: 1px solid rgba(199, 154, 58, .45);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.75), 0 1px 3px rgba(176,128,28,.14);
}
.lb-prize .icon { width: 12.5px; height: 12.5px; stroke-width: 2.1; color: #C79A3A; }
:root[data-theme="dark"] .lb-prize {
  color: #F2D486;
  background: linear-gradient(180deg, rgba(120,92,38,.55), rgba(84,64,26,.42));
  border-color: rgba(214,168,74,.4);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 1px 3px rgba(0,0,0,.2);
}
:root[data-theme="dark"] .lb-prize .icon { color: #E9C877; }

/* ── Hero «подписка истекла» ──────────────────────────────────────────────
   Приглушённый графит с тёплым янтарным подтоном (спящий доступ), статусный
   чип «Истекла» и акцент на продлении. Премиально, без тревожного красного. */
.hero-expired { background: linear-gradient(140deg, #34303A 0%, #211E27 60%, #1A171F 100%); }
.hero-expired .hero-shield { color: rgba(240, 190, 120, .12); }
.chip-expired {
  color: #F4C983;
  background: rgba(244, 178, 33, .16);
  box-shadow: inset 0 0 0 1px rgba(244, 178, 33, .28);
}
.chip-expired .icon { color: #F4C983; }
/* Отделяем «моё место» от списка тонкой линией сверху */
.lb-row.lb-me { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 12px; }

/* ═══ ПОЛНАЯ НЕЙТРАЛИЗАЦИЯ СИНИХ ФОНОВ (обе темы) ═══════════════════════════
   Синим остаются ТОЛЬКО крупные фирменные hero-карты (.hero-card, .pay-card)
   и разноцветные аватары (идентичность пользователя). Всё остальное — кнопки,
   мягкие подложки, выделения, индикаторы, прогресс — становится нейтральным.
   Акцентный синий у ГЛИФОВ/ТЕКСТА оставляем (это не «фон»). */

/* 1) Мягкий тон-подложка (--grad-acc-soft) → нейтральный серый в обеих темах.
   Одним токеном гасим все голубоватые подложки иконок, тоталов, баннеров. */
:root,
:root[data-theme="light"] { --grad-acc-soft: var(--surface-2); }
:root[data-theme="dark"]   { --grad-acc-soft: var(--surface-2); }

/* 2) Точечные голубоватые заливки (color-mix по --acc-1) → серый --surface-2. */
.chip-acc,
.btn-soft,
.icon-btn,
.tag-default { background: var(--surface-2); }

/* 3) Главные кнопки-CTA: синий градиент → нейтральный «тёмный/светлый»
   (в светлой теме — графит с белым текстом, в тёмной — светлая плашка с
   тёмным текстом; обе премиальны и не зависят от синего). */
.btn-primary { background: var(--text); color: var(--surface); box-shadow: none; }
.fallback-main { background: var(--text); color: var(--surface); box-shadow: var(--shadow-float); }

/* 4) Активные/выбранные индикаторы: сплошной синий → нейтральный контрастный. */
.lb-badge.me-badge { background: var(--text); color: var(--surface); }
.lb-badge.me-badge { border-color: var(--text); }
/* Радио выбранной строки: контрастная точка красится --surface, чтобы быть
   видимой в обеих темах (белая на тёмной плашке / тёмная на светлой). */
.row.selected .radio-dot { border-color: var(--text); background: var(--text); }
.row.selected .radio-dot::after { background: var(--surface); }
/* Полоса прогресса задания */
.task-fill { background: var(--text-2); }

/* 5) Выделения строк/опций: голубой тон → нейтральный, «выбор» остаётся ясным. */
.row.selected { background: color-mix(in srgb, var(--text) 6%, transparent); }
.opt.sel { background: var(--surface); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--text) 26%, transparent); }
/* Выбор тарифа: белая карточка с нейтральной рамкой вместо синей. */
.plan-list .row.selected { background: var(--surface); border-color: var(--text); }

/* 6) Крупные «успех»-круги (оплата/триал/задание) — инлайн-синий с !important. */
.success-ico {
  background: var(--text) !important;
  color: var(--surface) !important;
  box-shadow: 0 14px 32px rgba(0,0,0,.18) !important;
}

/* ═══ Шторка «О нас» — лендинг ═════════════════════════════════════════════
   Раздел собран из уже существующего языка приложения, а не из собственных
   форм. Два уровня вместо ровной стены одинаковых карточек:
   1) hero — ТА ЖЕ .hero-card, что и на главной (раньше был свой центрированный
      hero со своим скруглением и своим бликом — раздел выглядел чужим);
   2) .about-feats — ОДНА сгруппированная карта со строками-разделителями
      (раньше 12 отдельных плавающих карточек сливались в кашу).
   ═════════════════════════════════════════════════════════════════════════ */
.about { display: flex; flex-direction: column; gap: 14px; }

/* Hero — .hero-card + правки под лендинг. Своих фона/скругления/блика нет:
   всё наследуется от hero-семейства, поэтому карта совпадает с главной. */
.about-claim {
  position: relative; z-index: 1;
  margin-top: 18px;
  font-size: 27px; font-weight: 800; letter-spacing: -.8px; line-height: 1.16;
}
/* Факты в подвале на узких экранах переносим, а не сжимаем. */
.about-hero .hero-foot { flex-wrap: wrap; row-gap: 9px; }

/* Заголовок секции — негромкий, как .invite-title: работать должны карточки,
   а не подписи над ними. */
.about-title {
  font-size: 13px; font-weight: 700; letter-spacing: -.1px;
  color: var(--text-2);
  margin: 10px 6px -2px;
}

/* ── Особенности: одна карта, строки через разделитель ───────────────────── */
.about-feats {
  background: var(--surface);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-soft);
  padding: 2px 17px;
  overflow: hidden;
}
.feat { display: flex; align-items: flex-start; gap: 14px; padding: 15px 0; }
.feat + .feat { border-top: 1px solid var(--line); }
.feat-ico {
  width: 38px; height: 38px; flex: none;
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--text);
  background: var(--surface-2);
}
.feat-ico .icon { width: 20px; height: 20px; }
.feat-body { min-width: 0; }
.feat-body b { display: block; font-size: 14.5px; letter-spacing: -.15px; }
.feat-body span { display: block; margin-top: 4px; font-size: 12.5px; line-height: 1.55; color: var(--text-2); font-weight: 500; }

.about-cta { margin-top: 6px; }
.about-foot { text-align: center; font-size: 12px; color: var(--text-3); font-weight: 600; line-height: 1.5; margin: 6px 6px 0; }

/* ═══ Меню администратора (read-only) ════════════════════════════════════════
   Палитра — строго НЕЙТРАЛЬНАЯ (никакого цвета): те же поверхности, что и в
   остальном мини-аппе (--surface / --surface-2 / --text*). Иконки монохромные.
   --x нужен формулам .lbseg/.set-* (они рассчитаны на шторки, где --x = степень
   раскрытия); на экране его нет — фиксируем 0. */
#screen-admin { --x: 0; }

/* Нейтральный чип под иконку — общий для плиток, строк и заголовков.
   Фон — ТОТ ЖЕ --surface-2, что у всех иконочных плашек мини-аппа
   (.row-ico/.set-ico/.feat-ico/.nav-ico …). Свой полупрозрачный
   color-mix(--text N%) здесь давал ДРУГОЙ оттенок серого: он подмешивает тон
   текста (в светлой теме синевато-графитовый) и просвечивает подложку, поэтому
   админка не совпадала с пользовательскими экранами. */
#screen-admin .admin-tile-ico,
#screen-admin .admin-row-ico,
#screen-admin .admin-h-ico,
#screen-admin .node-sum-ico,
#screen-admin .node-metric-ico {
  color: var(--text);
  background: var(--surface-2);
  box-shadow: none;
}

/* Шапка экрана — жирный заголовок, БЕЗ кнопки «назад» (её даёт Telegram) */
.admin-head { display: flex; flex-direction: column; gap: 3px; padding: 2px; }
.admin-title { font-size: 24px; font-weight: 820; letter-spacing: -.5px; line-height: 1.1; }
.admin-sub { font-size: 13px; color: var(--text-2); font-weight: 500; }

/* Плитки хаба */
.admin-tiles { display: flex; flex-direction: column; gap: 10px; }
.admin-tile {
  display: flex; align-items: center; gap: 13px; width: 100%; text-align: left;
  padding: 15px 16px; border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--shadow-soft);
  /* Отклик на нажатие — РОВНО как у `.nav-row` в главном меню: тот же масштаб,
     та же длительность, тот же набор свойств. Здесь стояло `scale(.985)` плюс
     `opacity:.9` — и то, и другое чуть иначе, чем везде, отчего панель на ощупь
     отличалась от остального приложения. Гашение убрано намеренно: в главном
     меню строки не бледнеют при нажатии, они только поджимаются. */
  transition: transform .16s ease;
}
.admin-tile:active { transform: scale(.98); }
.admin-tile-ico {
  width: 44px; height: 44px; flex: none; border-radius: 13px;
  display: grid; place-items: center;
}
.admin-tile-ico .icon { width: 23px; height: 23px; stroke-width: 2; }
.admin-tile-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.admin-tile-txt b { font-size: 15.5px; font-weight: 700; letter-spacing: -.15px; }
.admin-tile-txt small { font-size: 12px; color: var(--text-2); font-weight: 500; }
.admin-tile .chev { color: var(--text-3); width: 19px; height: 19px; flex: none; }

.admin-body { display: flex; flex-direction: column; gap: 12px; }
/* Здесь ЖИЛ старый скелетон-спиннер: `.admin-skel { align-items: center;
   justify-content: center; padding: 30px 0 }` плюс крутящаяся иконка. Спиннер
   давно заменён силуэтом (см. блок ниже по файлу), а центрирование осталось — и
   поджимало КАЖДЫЙ силуэт в админке по ширине содержимого вместо полной строки.
   Отсюда и были «маленькие и нелогичные» заглушки. Правило удалено; крутилка
   `adm-spin` осталась, её используют «Проверить целостность» и «потянуть». */
@keyframes adm-spin { to { transform: rotate(360deg); } }
.admin-count { font-size: 12.5px; color: var(--text-2); font-weight: 600; margin: -2px 4px; }
.admin-count b { color: var(--text); }

/* Индикатор авто-обновления (нейтральный) */
.admin-live { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start;
  font-size: 12.5px; font-weight: 600; color: var(--text-2); margin: -2px 4px; }
.admin-live-dot { width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-3); animation: adm-pulse 1.8s ease-in-out infinite; }
@keyframes adm-pulse { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .admin-live-dot { animation: none; } }

/* Строки-навигация (промокоды, кампании, розыгрыши) */
.admin-list { display: flex; flex-direction: column; gap: 8px; }
.admin-row {
  display: flex; align-items: center; gap: 13px; width: 100%; text-align: left;
  padding: 13px 15px; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: var(--shadow-soft);
  transition: transform .16s ease;              /* см. .admin-tile — эталон .nav-row */
}
.admin-row:active { transform: scale(.98); }
.admin-row-ico {
  width: 40px; height: 40px; flex: none; border-radius: 12px;
  display: grid; place-items: center;
}
.admin-row-ico .icon { width: 21px; height: 21px; stroke-width: 2; }
.admin-row-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.admin-row-txt b { font-size: 15px; font-weight: 700; letter-spacing: -.1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.admin-row-txt small { font-size: 12px; color: var(--text-2); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.admin-row .chev { color: var(--text-3); width: 18px; height: 18px; flex: none; }
.admin-badge {
  flex: none; font-size: 11px; font-weight: 700; padding: 4px 9px; border-radius: 999px;
  letter-spacing: .01em; color: var(--text-2); background: var(--surface-2);
}
/* «Живой» бейдж — та же плашка --surface-2, отличается контрастным текстом и
   тонкой обводкой (--line), а не собственным полупрозрачным оттенком. */
.admin-badge.is-live {
  color: var(--text); background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* «Отчёт» бота: подзаголовок с иконкой + пары «label: value» */
.admin-kvs { display: flex; flex-direction: column; gap: 0; padding: 6px 18px; }
.admin-kv { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 9px 0; }
.admin-kv + .admin-kv { border-top: 1px solid var(--line); }
.admin-kv span { font-size: 13.5px; color: var(--text-2); font-weight: 500; min-width: 0; }
.admin-kv b { font-size: 14.5px; font-weight: 750; letter-spacing: -.1px; text-align: right;
  overflow-wrap: anywhere; }
.admin-h {
  display: flex; align-items: center; gap: 9px;
  font-size: 15px; font-weight: 800; letter-spacing: -.2px; color: var(--text);
  margin: 8px 4px 0;
}
.admin-h-ico { width: 28px; height: 28px; flex: none; border-radius: 9px; display: grid; place-items: center; }
.admin-h-ico .icon { width: 16px; height: 16px; stroke-width: 2.1; }
.admin-h-count { margin-left: auto; font-size: 12px; font-weight: 700; color: var(--text-3);
  background: var(--surface-2); border-radius: 999px; padding: 2px 9px; }
/* Кнопка ⓘ у заголовка раздела: раскрывает пояснение под карточкой. Плашка та же
   --surface-2, что у всех иконок-подложек в приложении. */
.admin-h-info {
  margin-left: auto; width: 26px; height: 26px; flex: none;
  display: grid; place-items: center; border-radius: 50%;
  color: var(--text-3); background: var(--surface-2);
  transition: transform .14s ease, color .14s ease;
}
.admin-h-count + .admin-h-info { margin-left: 8px; }
.admin-h-info .icon { width: 16px; height: 16px; stroke-width: 2; }
.admin-h-info:active { transform: scale(.92); }
.admin-h-info[aria-expanded="true"] { color: var(--text); }
.admin-line { padding: 7px 18px; font-size: 13.5px; color: var(--text-2); font-weight: 500; line-height: 1.5; }
.admin-kvs .admin-line { padding: 7px 0; }

/* Список админов — карточки с реальной аватаркой (нейтральный фон под инициал) */
.admin-adm-card { display: flex; flex-direction: column; gap: 0; padding: 6px 16px; }
.admin-adm-row { display: flex; align-items: center; gap: 12px; padding: 9px 0; }
.admin-adm-row + .admin-adm-row { border-top: 1px solid var(--line); }
.admin-adm-ava {
  position: relative; width: 38px; height: 38px; flex: none; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  color: var(--text); font-size: 15px; font-weight: 700;
  background: var(--surface-2);
}
.admin-adm-ava img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.admin-adm-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.admin-adm-txt b { font-size: 14.5px; font-weight: 700; }
.admin-adm-txt small { font-size: 12px; color: var(--text-3); font-weight: 500;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace; }

/* Выбор периода: поле-селект, список под ним и строка «дата — дата».
   Заливки, скругления и кегль — те же, что у полей формы ниже (.admin-date
   input), поэтому обе строки читаются как один блок, а не как два разных
   элемента. Строки списка — общие .opt, как во всех выборах приложения. */
.adm-period { display: flex; flex-direction: column; gap: 9px; }
.adm-select-wrap { position: relative; z-index: 5; }
.adm-select {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 12px 14px; border-radius: 12px;
  background: var(--surface-2); color: var(--text);
  font-size: 14px; font-weight: 650; text-align: left;
  transition: transform .14s ease, box-shadow .14s ease;
}
.adm-select:active { transform: scale(.99); }
.adm-select-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Пара шевронов вверх/вниз — как у нативного селекта. Своей иконки не заводим:
   это тот же i-chevron, повёрнутый в две стороны. */
.adm-select-arr { display: flex; flex-direction: column; gap: 1px; flex: none; color: var(--text-3); }
.adm-select-arr .icon { width: 13px; height: 13px; stroke-width: 2.6; }
.adm-select-arr .icon:first-child { transform: rotate(-90deg); }
.adm-select-arr .icon:last-child { transform: rotate(90deg); }
.adm-select.is-open { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--text) 26%, transparent); }

/* Список поверх содержимого, а не раздвигая его: иначе при открытии страница
   прыгает, а закрытие возвращает её обратно. */
.adm-drop {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px);
  padding: 8px; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: var(--shadow-float);
  gap: 4px;
}
/* .opt-list задаёт display:flex, а он сильнее UA-правила для [hidden] — без
   этой строки список нарисовался бы раскрытым. */
.adm-drop[hidden] { display: none; }
.adm-drop .opt { padding: 12px 14px; font-size: 14px; }
.adm-drop .opt .opt-check { width: 18px; height: 18px; }

/* Детализация графиков — второй такой же селект под периодом.
   Отрицательный отступ выравнивает расстояние: между строками внутри .adm-period
   9px, а между блоками страницы (.adm-content) 14px — без поправки детализация
   висела бы дальше от периода, чем даты от селекта. */
.adm-gran { z-index: 4; margin-top: -5px; }  /* z ниже периода: их списки не пересекаются */
/* Недоступный пункт списка: «По дням» на окне длиннее месяца. Гасим и делаем
   непрожимаемым — «нельзя» должно быть видно, а не выясняться нажатием. */
.opt.is-off { opacity: .38; pointer-events: none; }
.adm-dates { display: flex; align-items: center; gap: 10px; }
.adm-dates .adm-date { flex: 1 1 0; min-width: 0; }
.adm-dash { flex: none; font-style: normal; font-weight: 650; color: var(--text-3); }
/* z-index:0 → СВОЙ контекст наложения: капля сегмента-тумблера (.lbseg-drop,
   z-index:4) и её тёмная тень остаются ВНУТРИ обёртки и не перекрашивают
   соседние статичные кнопки (в тёмной теме тень капли — rgba(0,0,0,.2)).
   Клипа нет — «лифт» капли над сегментом сохраняется. */
.adm-seg-wrap { position: relative; z-index: 0; }
.adm-custom { position: relative; z-index: 1; }
.adm-lbseg { height: 46px; padding: 6px; border-radius: 100px; }
.adm-lbseg button { font-size: 12.5px; font-weight: 650; }
.adm-toggle button { font-size: 13.5px; }
.adm-seg-wrap.no-drop > .lbseg-drop { opacity: 0; }

/* Поля дат периода. Раньше это была отдельная форма «свой период» в теле экрана
   (.admin-range-form с кнопками «Отмена/Показать»); теперь даты видны всегда в
   самом баре, и от формы осталась только сама плашка поля.

   Дату РИСУЕМ САМИ (ДД.ММ.ГГГГ): нативный input[type=date] печатает её в формате
   системы — на английском телефоне это 07/01/2026, и ни атрибутом, ни стилем это
   не меняется. Поэтому input лежит поверх плашки полностью прозрачным: тап по
   полю открывает штатный календарь, а видно нашу подпись. */
/* Calendar icon pinned to the leading edge, date centred in the plate itself.
   The icon is taken out of the flow (absolute, same 12px as the plate padding)
   precisely so it does not shift the centre: an in-flow icon would push the date
   right by its width plus the gap, and the date would only be centred in the
   space left over. Vertical centring stays on align-items — icon and text sit on
   different baselines. The transparent input is absolute too, so it affects
   neither. */
.adm-date { position: relative; display: flex; align-items: center; justify-content: center;
  width: 100%; padding: 11px 12px;
  border-radius: 12px; background: var(--surface-2);
  color: var(--text); font-size: 14px; font-weight: 600;
  /* Плашка даты нажимается (под ней прозрачный нативный input), а отклика не
     имела — единственная кликабельная вещь в панели без него. Тот же .nav-row. */
  transition: transform .16s ease; }
/* Scales the whole plate, icon included — the plate is one button. The icon's own
   translateY is a separate transform on a separate element, so the two do not
   fight; and the containing block for the absolute icon is this element either
   way (it is already position:relative), so pressing does not move the icon. */
.adm-date:active { transform: scale(.98); }
.adm-date-ico { position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; stroke-width: 2; color: var(--text-3); }
.adm-date input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; border: none; background: none; padding: 0;
  font-family: inherit; font-size: inherit;
  -webkit-appearance: none; appearance: none;
}
.adm-date.is-empty .adm-date-txt { color: var(--text-3); }

/* Нагрузка нод — сводка и карточки (нейтральные, без обрезки «…») */
.node-summary-head { display: flex; align-items: center; gap: 10px; margin-bottom: 15px;
  font-size: 16px; font-weight: 800; letter-spacing: -.2px; }
.node-sum-ico { width: 30px; height: 30px; flex: none; border-radius: 9px; display: grid; place-items: center; }
.node-sum-ico .icon { width: 18px; height: 18px; stroke-width: 2; }
/* Две колонки — метрике хватает ширины, значение не приходится резать */
.node-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 12px; }
.node-metric { display: flex; align-items: center; gap: 10px; min-width: 0; }
.node-metric-ico { width: 32px; height: 32px; flex: none; border-radius: 9px; display: grid; place-items: center; }
.node-metric-ico .icon { width: 17px; height: 17px; stroke-width: 2; }
.node-metric-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.node-metric-txt b { font-size: 15px; font-weight: 750; letter-spacing: -.2px; }
.node-metric-txt small { font-size: 10.5px; font-weight: 600; color: var(--text-3);
  text-transform: uppercase; letter-spacing: .04em; }
.node-card .node-name { font-size: 15px; font-weight: 750; letter-spacing: -.15px; margin-bottom: 14px; }
.node-summary .node-name { opacity: 1; }
.node-err-txt { display: flex; align-items: center; gap: 7px; font-size: 13.5px; font-weight: 650; color: var(--text-2); }
.node-err-txt .icon { width: 16px; height: 16px; stroke-width: 2.4; }

/* ── Пользователи: поиск, фильтры, список, карточка ──────────────────────────
   Тот же нейтральный набор: поверхность-карточка, монохромные иконки, ссылки в
   карточке переносятся по любому символу (длинный UUID не расширяет экран). */
#screen-admin .admin-search-ico,
#screen-admin .admin-urow-ava,
#screen-admin .admin-gate-ico,
#screen-admin .admin-warn-ico,
#screen-admin .admin-fail-ico,
#screen-admin .admin-run-ico {
  color: var(--text);
  background: var(--surface-2);
}

.admin-search { display: flex; align-items: center; gap: 10px; padding: 8px 8px 8px 12px; }
.admin-search-ico { width: 32px; height: 32px; flex: none; border-radius: 10px; display: grid; place-items: center; }
.admin-search-ico .icon { width: 17px; height: 17px; stroke-width: 2; }
.admin-search input {
  flex: 1; min-width: 0; border: none; background: none; padding: 8px 0;
  font-family: inherit; font-size: 14.5px; font-weight: 600; color: var(--text);
}
.admin-search input::placeholder { color: var(--text-3); font-weight: 500; }
.admin-search input::-webkit-search-cancel-button { display: none; }
/* Никакой системной рамки при вводе — поле уже оформлено карточкой */
.admin-search input:focus, .admin-search input:focus-visible { outline: none; box-shadow: none; }
.admin-search-go {
  flex: none; padding: 10px 15px; border-radius: 12px;
  font-size: 13px; font-weight: 700; color: var(--surface); background: var(--text);
  transition: transform .14s ease, opacity .14s ease;
}
.admin-search-go:active { transform: scale(.96); opacity: .9; }
.admin-note { font-size: 12px; line-height: 1.45; color: var(--text-3); font-weight: 500; margin: -4px 6px 0; }

.admin-urow { gap: 12px; }
.admin-urow-ava {
  position: relative; overflow: hidden;
  width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center;
  font-size: 15px; font-weight: 700;
}
/* Фото из Telegram ложится поверх инициала; не загрузилось — <img> удаляет себя */
.admin-urow-ava img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.admin-badge.is-ban { color: var(--surface); background: var(--text); }
.admin-more { width: 100%; margin: 2px 0 0; }

/* Пороги антифрода — компактный ряд чипов (три равные доли) */
.adm-chips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.adm-chip {
  padding: 10px 6px; border-radius: 100px; text-align: center;
  font-size: 12px; font-weight: 650; color: var(--text-2); background: var(--surface-2);
  transition: transform .14s ease;
}
.adm-chip:active { transform: scale(.97); }
.adm-chip.on { color: var(--surface); background: var(--text); }

/* Длинная ссылка подписки в карточке пользователя */
.admin-line { overflow-wrap: anywhere; }

/* ── Обязательная подписка: два гейта одной высоты ───────────────────────── */
.admin-gates { display: flex; flex-direction: column; gap: 0; padding: 6px 16px; }
.admin-gate { display: flex; align-items: center; gap: 12px; padding: 13px 0; }
.admin-gate + .admin-gate { border-top: 1px solid var(--line); }
.admin-gate-ico { width: 36px; height: 36px; flex: none; border-radius: 11px; display: grid; place-items: center; }
.admin-gate-ico .icon { width: 18px; height: 18px; stroke-width: 2.2; }
.admin-gate-txt { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 700; letter-spacing: -.1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ── Switch, matched to iOS 26 ───────────────────────────────────────────────
   NOT the pre-26 switch. Two independently confirmed facts drive every number
   here:
     1) iOS 26 stretched the control HORIZONTALLY ONLY, by roughly 20%;
     2) its HEIGHT did NOT change — still the same 31pt.
   Hence the track went 51×31 → 61×31. The knob stopped being a circle: it kept
   its height (27 = 31 − 2×2) but widened along with the track, 27 → 37, which
   leaves the travel exactly as it was: 61 − 2×2 − 37 = 20.

   ⚠️ The knob WIDTH (37) is DERIVED from those two facts, not measured — Apple
   publishes no control dimensions. Verify against a screenshot of a real switch.

   ⚠️ An earlier revision used 64×25 with a 35×17 knob. Those came from a
   third-party "flat pill" web recreation whose author had slimmed the HEIGHT to
   taste, and it read as both too small and too flat. Someone's iOS-style CSS is
   not a spec.

   All transitions run on `transform`/`background`/`opacity` with a cubic bezier:
   `linear()` is forbidden here, it drops the animation off the compositor and
   back to 60 Hz (see CLAUDE.md). */
.admin-sw {
  position: relative; flex: none;
  width: 61px; height: 31px; border-radius: 999px; padding: 0;
  background: var(--surface-2);
  transition: background var(--dur-switch) var(--ease-spring), opacity .15s ease;
  touch-action: pan-y;            /* vertical scrolling stays native, horizontal is ours */
  -webkit-tap-highlight-color: transparent;
}
/* On state uses the ink fill shared by every active state in the panel.
   Apple puts green here; the owner dropped it as an unnecessary colour. */
.admin-sw.on { background: var(--text); }

/* The track is 31px but HIG wants a 44px target, so an invisible layer grows
   the hit area without touching the look. No transform: it would turn the
   element into a containing block (see the scrolling rule in CLAUDE.md). */
.admin-sw::after { content: ''; position: absolute; left: 0; right: 0; top: -7px; bottom: -7px; }

/* The knob. `left` is never animated — transform only, or it drops off the
   compositor to 60 Hz. Inset is a uniform 2px: knob height is 31 − 2×2 = 27. */
.admin-sw > i {
  position: absolute; top: 2px; left: 2px;
  display: block; width: 37px; height: 27px; border-radius: 999px;
  background: var(--surface);
  box-shadow: 0 2px 5px rgba(24, 34, 84, .22), 0 0 1px rgba(24, 34, 84, .3);
  transition: transform var(--dur-switch) var(--ease-spring),
              width var(--dur-switch) var(--ease-spring);
}
/* 61 − 2 − 2 − 37 = 20. Keep in step with ADM_SW_TRAVEL in app.js. */
.admin-sw.on > i { transform: translateX(20px); }

/* Pressed — the pill STRETCHES wider. Width, not scale: scaling would smear
   the rounded caps, whereas the pill itself should lengthen.
   In the on position it grows LEFTWARD, so the offset shrinks by the same
   amount (43 − 37 = 6) or the right cap would poke outside the track. */
.admin-sw.pressing > i { width: 43px; }
.admin-sw.on.pressing > i { transform: translateX(14px); }

/* While the finger drives the knob the settle is silenced — it must track the
   finger exactly. Width still eases, only the position is hand-driven. */
.admin-sw.dragging > i { transition: width var(--dur-switch) var(--ease-spring); }

.admin-sw:disabled { opacity: .55; }
/* In dark theme the off track and a white knob nearly match — use grey. */
:root[data-theme="dark"] .admin-sw:not(.on) > i { background: var(--text-2); }
@media (prefers-reduced-motion: reduce) {
  .admin-sw, .admin-sw > i { transition: none; }
}

/* ── Проверка целостности: предупреждение / ход / ошибка (одна вертикаль) ── */
.admin-warn, .admin-run, .admin-fail {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 22px 18px; text-align: center;
}
.admin-warn-ico, .admin-fail-ico, .admin-run-ico {
  width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center; margin-bottom: 2px;
}
.admin-warn-ico .icon, .admin-fail-ico .icon, .admin-run-ico .icon { width: 23px; height: 23px; stroke-width: 2; }
.admin-run-ico .icon { animation: adm-spin 1s linear infinite; }
@media (prefers-reduced-motion: reduce) { .admin-run-ico .icon { animation: none; } }
.admin-warn b, .admin-run b, .admin-fail b { font-size: 16px; font-weight: 800; letter-spacing: -.2px; }
/* :not([class]) — только текстовые span'ы, не иконочный чип над ними */
.admin-warn > span:not([class]), .admin-run > span:not([class]), .admin-fail > span:not([class]) {
  font-size: 13px; line-height: 1.5; color: var(--text-2); font-weight: 500; max-width: 30em;
}
.admin-run small { font-size: 11.5px; color: var(--text-3); font-weight: 600; }
.admin-run-btn { width: 100%; margin: 0; }

/* ── Копируемое моно-значение (там, где в боте <code>) ───────────────────── */
.mono-copy {
  display: inline; text-align: inherit; padding: 0; border-radius: 6px;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: .94em; font-weight: 650; color: inherit; overflow-wrap: anywhere;
  text-decoration: underline; text-decoration-style: dotted;
  text-underline-offset: 3px; text-decoration-color: var(--text-3);
  transition: opacity .14s ease;
}
.mono-copy:active { opacity: .55; }

/* ── Список действий под данными экрана ─────────────────────────────────── */
.admin-acts { margin-top: 4px; }
/* Подзаголовок внутри карточки (название сервера над ссылкой прокси) */
.admin-sub-title { padding: 9px 0 3px; font-size: 14.5px; font-weight: 750; letter-spacing: -.1px; }
.admin-row.is-danger .admin-row-txt b { color: var(--bad); }
.admin-row.is-danger .admin-row-ico { color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }

/* ── Универсальная форма (замена FSM-промптов бота) ─────────────────────── */
.admin-form { display: flex; flex-direction: column; gap: 14px; }
.admin-field { display: flex; flex-direction: column; gap: 6px; }
.admin-field > span {
  font-size: 12px; font-weight: 650; color: var(--text-3);
  text-transform: uppercase; letter-spacing: .04em;
}
.admin-field input, .admin-field select {
  width: 100%; padding: 12px 14px; border-radius: 12px; border: none;
  background: var(--surface-2); color: var(--text);
  font-family: inherit; font-size: 15px; font-weight: 600;
  -webkit-appearance: none; appearance: none;
}
.admin-field select {
  background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%),
                    linear-gradient(135deg, var(--text-3) 50%, transparent 50%);
  background-position: calc(100% - 19px) 21px, calc(100% - 14px) 21px;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
  padding-right: 38px;
}
.admin-field input::placeholder { color: var(--text-3); font-weight: 500; }
.admin-field > small { font-size: 11.5px; line-height: 1.4; color: var(--text-3); font-weight: 500; }
.admin-form-go { width: 100%; margin: 2px 0 0; }

/* ── Динамика: спарклайны, дельты, воронка ───────────────────────────────────
   Палитра — та же нейтральная, что и во всей админке: НИ ОДНОГО собственного
   цвета. Рост/падение показывает стрелка и знак, а не зелёный/красный — иначе
   в экран, из которого цвет убирали намеренно, он вернулся бы через графики.
   Линия и заливка наследуют --text через currentColor, поэтому обе темы
   работают без отдельных правил. */
.metric-card { display: flex; flex-direction: column; gap: 2px; padding: 15px 17px 12px; }
.metric-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.metric-title { font-size: 13px; font-weight: 650; color: var(--text-2); letter-spacing: -.05px; }
.metric-value { font-size: 26px; font-weight: 800; letter-spacing: -.7px; line-height: 1.15; }
.metric-card > small { font-size: 11.5px; font-weight: 600; color: var(--text-3); margin-top: 2px; }

/* Высота фиксированная, ширина резиновая: preserveAspectRatio="none" в разметке
   тянет viewBox по горизонтали, поэтому линия не зависит от числа корзин. */
.spark { display: block; width: 100%; height: 56px; margin: 8px 0 2px; color: var(--text); overflow: visible; }
.spark-line { fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  /* Линия растягивается вместе с viewBox — без этого при широкой карточке
     горизонтальные участки становятся заметно тоньше вертикальных. */
  vector-effect: non-scaling-stroke; }
.spark-area { fill: currentColor; fill-opacity: .10; stroke: none; }

/* Ведение пальцем: курсор — HTML-оверлей, а не элемент внутри SVG. viewBox
   растягивается по ширине карточки (preserveAspectRatio="none"), и нарисованный
   внутри кружок расплющило бы тем же преобразованием, которое делает линию
   независимой от плотности экрана. `touch-action: pan-y` оставляет вертикальную
   прокрутку браузеру: поперёк ведём мы, вдоль — он. */
.spark-wrap { position: relative; touch-action: pan-y; }
.spark-cur {
  position: absolute; top: 8px; bottom: 2px; width: 1px; margin-left: -.5px;
  background: var(--text-3); opacity: 0; pointer-events: none;
  transition: opacity .14s ease;
}
.spark-cur > b {
  position: absolute; left: 50%; width: 9px; height: 9px; border-radius: 50%;
  background: var(--text); transform: translate(-50%, -50%);
  box-shadow: 0 0 0 3px var(--surface);
}
.metric-card.is-scrub .spark-cur { opacity: 1; }

/* Края окна под графиком. Промежуточных подписей нет намеренно: на 30-90
   корзинах они сливаются, а дату точки показывает сам жест. */
.spark-axis {
  display: flex; justify-content: space-between; gap: 10px; margin: 1px 0 3px;
  font-size: 11px; font-weight: 600; color: var(--text-3);
}

/* Дельта к прошлому периоду */
.delta {
  display: inline-flex; align-items: center; gap: 3px; flex: none;
  font-size: 11.5px; font-weight: 700; letter-spacing: .01em;
  padding: 3px 8px 3px 6px; border-radius: 999px;
  color: var(--text-2); background: var(--surface-2);
}
.delta .icon { width: 13px; height: 13px; stroke-width: 2.6; }
.delta.is-up { color: var(--text); }
.delta.is-flat { padding: 3px 9px; }

/* ── Спидометр удержания ─────────────────────────────────────────────────────
   ЕДИНСТВЕННОЕ место в админке, где есть цвет: так решил владелец. Зелёный и
   красный — ОБЩИЕ токены --ok/--bad (те же, что у чипов и опасных кнопок), своей
   палитры блок не заводит. Числа под дугой — общий компонент .stats-grid/.metric,
   тот же, что в статистике промокодов. */
.gauge-card { display: flex; flex-direction: column; align-items: center; padding: 16px 18px 16px; }
.gauge-wrap { position: relative; width: 100%; max-width: 264px; }
.gauge { display: block; width: 100%; height: auto; }
/* Концы РАЗНЫЕ: на стыке цветов плоский срез (цвета сходятся одной линией), по
   краям полукруга — скругление. `stroke-linecap` красит оба конца пути сразу и
   так не умеет, поэтому дуги плоские, а кончики — отдельные кружки .gauge-cap
   радиусом в полтолщины обводки. Подложка остаётся страховкой на случай
   округлений. */
.gauge-track, .gauge-keep, .gauge-lost { fill: none; stroke-width: 15; }
.gauge-track { stroke: var(--surface-2); }
.gauge-keep { stroke: var(--ok); }
.gauge-lost { stroke: var(--bad); }
.gauge-cap { stroke: none; }
.gauge-cap.is-keep { fill: var(--ok); }
.gauge-cap.is-lost { fill: var(--bad); }
/* Проценты стоят В ЧАШЕ дуги: удержание слева, отток справа — «левая часть
   зелёная, правая красная» читается без легенды. Ряд отцентрован, а не прибит к
   краям: у краёв чаша сужается и текст налезал бы на саму дугу. */
.gauge-read {
  position: absolute; left: 0; right: 0; bottom: 2px;
  display: flex; justify-content: center; gap: 26px;
}
.gauge-side { display: flex; flex-direction: column; align-items: center; gap: 2px; line-height: 1; }
.gauge-side b { font-size: 21px; font-weight: 800; letter-spacing: -.6px; }
.gauge-side small { font-size: 10.5px; font-weight: 650; color: var(--text-3); }
.gauge-side.is-keep { color: var(--ok); }
.gauge-side.is-lost { color: var(--bad); }
.gauge-card .stats-grid { width: 100%; margin-top: 12px; }

/* Воронка: ширина полосы = сквозная доля от вершины. В строке два процента —
   <i> шаговый (конверсия из предыдущей ступени, тот же кегль, что у числа) и
   <em> сквозной (доля от вершины, мелкий). */
.funnel { display: flex; flex-direction: column; gap: 13px; padding: 15px 17px; }
.funnel-step { display: flex; flex-direction: column; gap: 6px; }
.funnel-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 13px; font-weight: 600; color: var(--text-2); }
.funnel-top b { font-size: 14.5px; font-weight: 750; color: var(--text); letter-spacing: -.1px; }
.funnel-top b i { font-style: normal; font-weight: 700; color: var(--text-2); margin-left: 9px; }
.funnel-top b em { font-style: normal; font-size: 11.5px; font-weight: 650;
  color: var(--text-3); margin-left: 7px; }
.funnel-bar { height: 8px; border-radius: 100px; background: var(--surface-2); overflow: hidden; }
.funnel-bar > i { display: block; height: 100%; border-radius: 100px; background: var(--text-2); }

/* История нагрузки под метриками ноды */
.node-hist { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.node-hist-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 11.5px; font-weight: 600; color: var(--text-3);
  text-transform: uppercase; letter-spacing: .04em; }
.node-hist-head b { font-size: 12px; font-weight: 700; color: var(--text-2); text-transform: none; letter-spacing: 0; }
.node-hist .spark { height: 44px; margin: 6px 0 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   МЕНЮ АДМИНИСТРАТОРА — стек живых карточек
   ═══════════════════════════════════════════════════════════════════════════
   ⚠️ В ЭТОМ БЛОКЕ ТОЛЬКО ГЕОМЕТРИЯ И ДВИЖЕНИЕ. Ни одного правила, меняющего
   ВНЕШНИЙ ВИД компонентов. Карточки, кнопки, строки, поиск, поля, скругления,
   тени, размеры иконок, типографика, цвета — ОБЩИЕ с остальным мини-аппом.
   Админка обязана выглядеть как то же самое приложение, а не как отдельное.

   Однажды здесь лежал набор `#screen-admin .admin-row {…}` и т.п., который
   переоформил панель «под Settings.app»: плоские группы, свои серые, свои
   размеры иконок, свои кнопки. Панель перестала быть частью приложения.
   НЕ ДОБАВЛЯТЬ СЮДА ПРАВИЛА ВНЕШНЕГО ВИДА. Нужно поменять вид строки — правьте
   .admin-row там, где она объявлена, и осознанно для всего приложения.

   Почему движение ведёт JS, а не CSS-переход: переход не умеет стартовать СО
   СКОРОСТЬЮ ПАЛЬЦА, а без неё смахнутая и медленно уведённая страница летят
   одинаково. Пружины — в js/motion.js. */

/* ── Контейнер стека ──────────────────────────────────────────────────────── */
.adm-stack {
  position: fixed; inset: 0; z-index: 70;
  overflow: hidden;
  background: var(--bg);
  /* Ведёт себя РОВНО как #view: тот же origin, та же кривая, тот же масштаб и
     то же размытие под шторкой. Благодаря этому вход в панель выглядит как
     обычное закрытие шторки — панель стоит уменьшенной и размытой за уходящей
     вниз шторкой и «оживает» вместе с её уходом, а не «прыгает» на экран. */
  transform-origin: 50% 0;
  /* Размытие СВОИМ фильтром, а не от backdrop-filter шторочного фона.
     Фон (#backdrop) лежит выше по z-index и теоретически должен размывать панель
     сам, но панель — отдельный композиторский слой (её двигает JS покадрово), а
     сэмплирование таких слоёв в backdrop-filter ненадёжно от движка к движку.
     Свой filter даёт гарантированный результат.
     blur(0px), а не none: между `none` и длиной браузер не интерполирует, и
     размытие бы щёлкало вместо плавного ухода. */
  /* ⚠️ `none`, а НЕ `blur(0px)`. Нулевой блюр — это всё равно ФИЛЬТР: браузер
     заводит под него отдельную поверхность и держит её постоянно, а поддерево с
     фильтром теряет право ехать на компоузере. Из-за этого переходы карточек
     считались на основном потоке и панель ощущалась медленнее экрана даже после
     перевода анимаций на CSS.
     На интерполяцию это не влияет: по спеке фильтров `none` при переходе
     заменяется единичной функцией другого списка, то есть `none ↔ blur(6px)`
     анимируется ровно как `blur(0) ↔ blur(6px)`. */
  filter: none;
  opacity: 1;
  /* Прозрачность гасим БЫСТРЕЕ, чем идёт движение (0.6 от общей длительности):
     затянутый кроссфейд оставляет обе картинки полупрозрачными надолго и мутит
     кадр. К моменту, когда панель ещё только разжимается, она уже непрозрачна. */
  transition: transform var(--dur-sheet) var(--ease-spring),
              filter var(--dur-sheet) var(--ease-spring),
              opacity calc(var(--dur-sheet) * .6) var(--ease-spring);
}
/* Состояние «панель только что встала на место главной»: тот же масштаб, то же
   размытие (6px — как у .sheet-backdrop), и ПОЛНАЯ прозрачность. Снятие
   sheet-open вместе с уходом шторки проявляет её поверх главной — это и есть
   кроссфейд «главная → админка». */
body.sheet-open .adm-stack {
  transform: scale(.955) translateY(4px);
  filter: blur(6px);
  opacity: 0;
}
.adm-stack[hidden] { display: none; }

/* ── Карточка = одна страница стека ───────────────────────────────────────── */
/* ⚠️ У КАРТОЧКИ В ПОКОЕ НЕ ДОЛЖНО БЫТЬ НИ `transform`, НИ `will-change`.
   Здесь стояли `transform: translate3d(0,0,0)` и `will-change: transform` — как
   «подсказка компоузеру». Обе делают карточку ТРАНСФОРМИРОВАННЫМ ПРЕДКОМ
   постоянно (по спеке `will-change: transform` тоже заводит containing block и
   stacking context, ровно как настоящий transform), а трансформированный предок
   на iOS ломает нативную прокрутку вложенного скроллера — это и была «после
   „назад“ перестаёт скроллиться». Снятия ИНЛАЙНОВОГО transform не хватало:
   правило из таблицы стилей продолжало действовать.
   Теперь и то, и другое навешивается только на время движения (admTransition)
   и снимается по приезде. НЕ ВОЗВРАЩАТЬ СЮДА. */
.adm-card {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  background: var(--bg);
}
/* Нижние карточки не кликаются: во время жеста палец не должен попадать по
   строкам страницы, которая видна из-под текущей. */
.adm-card:not(.is-top) { pointer-events: none; }

/* Затемнение нижней страницы — как в UINavigationController: уезжающий экран
   не просто отъезжает, он уходит в тень. Лежит ПОВЕРХ содержимого. */
.adm-scrim {
  position: absolute; inset: 0; z-index: 5;
  background: #000; opacity: 0; pointer-events: none;
}

/* Полосы `.adm-navbar` со «схлопывающимся» заголовком здесь БОЛЬШЕ НЕТ (решение
   владельца): при прокрутке сверху ничего не выезжает. Заголовок на экране один
   — крупный, в начале страницы, и уходит вместе с содержимым. Разметку и
   слушатель прокрутки убрали в `admBuildCard`. */

/* Отступы РОВНО как у #view — панель не должна отличаться полями от остальных
   экранов (сверху там же, где на главной начинается ряд профиля). Снизу дока
   нет, он убран, поэтому только безопасная зона. */
.adm-scroll {
  flex: 1; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  /* `-webkit-overflow-scrolling: touch` УБРАН НАМЕРЕННО. С iOS 13 ускоренная
     прокрутка включена для всех overflow-элементов, свойство не нужно — а вот
     вреда от него хватает: именно оно фигурирует в баге «прокрутка отваливается,
     если у предка есть transform», а предок-карточка у нас трансформируется на
     каждом переходе. Не возвращать. */
  overscroll-behavior-y: contain;
  padding: calc(14px + var(--safe-t)) 16px calc(var(--safe-b) + 32px);
  display: flex; flex-direction: column;
  /* Та же колонка, что у #app — иначе на широком экране панель расползается на
     всю ширину, а главная под ней остаётся в 480px, и «замена фона» не сходится. */
  width: 100%; max-width: 480px; margin: 0 auto;
}
/* Крупный заголовок живёт в потоке контента и уезжает вместе с ним */
.adm-large { flex: none; padding-bottom: 14px; }
.adm-large:empty { display: none; }
/* gap как у .screen — интервалы между блоками те же, что на обычных экранах */
.adm-content { display: flex; flex-direction: column; gap: 14px; min-width: 0; }

/* ── Ожидание данных: силуэт вместо спиннера ─────────────────────────────────
   Крутящаяся иконка с «Загрузка…» ничего не сообщала и ломала впечатление от
   перехода. Здесь — силуэт БУДУЩИХ СТРОК, геометрия 1-в-1 как у .admin-row
   (те же отступы, размер иконки, скругление, тень), а мерцание — общий .sk
   мини-аппа. Никакого своего оформления. */
/* Скелетон ПОЯВЛЯЕТСЯ НЕ СРАЗУ. Он ждёт 220мс и только потом проступает: если
   данные пришли раньше (а с кэшем и предзагрузкой это обычный случай), его
   успевают заменить, пока он ещё полностью прозрачен — то есть человек не
   видит его вообще. Это дешевле и честнее, чем рисовать под каждый экран свой
   «похожий» силуэт: силуэт всё равно врёт, а отсутствие ожидания — нет. */
.admin-skel { display: flex; flex-direction: column; gap: 8px;
  opacity: 0; animation: adm-skel-in .18s ease .22s forwards; }
@keyframes adm-skel-in { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .admin-skel { animation-duration: .01s; } }
.admin-skel-row {
  display: flex; align-items: center; gap: 13px;
  padding: 13px 15px; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: var(--shadow-soft);
}
.admin-skel-ico { width: 40px; height: 40px; border-radius: 12px; flex: none; }
.admin-skel-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.admin-skel-line { height: 11px; border-radius: 6px; }
.admin-skel-line.is-short { width: 45%; height: 9px; }

/* Скелетон «Статистики». Общий силуэт из строк списка врал: на этом экране
   приезжают не строки, а заголовок раздела, карточки с графиком и таблица
   «подпись — значение». Размеры взяты с РЕАЛЬНЫХ блоков (.admin-h, .metric-card,
   .admin-kvs), поэтому контент встаёт ровно на место силуэта, а не прыгает. */
.admin-skel.is-stats { gap: 12px; }        /* как .admin-body */
.sk-h { display: flex; align-items: center; gap: 9px; margin: 8px 4px 0; }
.sk-h-ico { width: 28px; height: 28px; border-radius: 9px; flex: none; }
.sk-h-txt { width: 104px; height: 13px; border-radius: 7px; }
.sk-metric { display: flex; flex-direction: column; padding: 15px 17px 12px; }
.sk-metric-title { width: 92px; height: 11px; border-radius: 6px; }
.sk-metric-value { width: 148px; height: 24px; border-radius: 8px; margin-top: 9px; }
.sk-metric-chart { height: 56px; border-radius: 12px; margin: 12px 0 8px; }
.sk-metric-cap { width: 156px; height: 9px; border-radius: 5px; }
.sk-kvs { display: flex; flex-direction: column; padding: 6px 18px; }
.sk-kv { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; }
.sk-kv + .sk-kv { border-top: 1px solid var(--line); }
.sk-kv > i:first-child { width: 42%; height: 11px; border-radius: 6px; }
.sk-kv > i:last-child { width: 62px; height: 11px; border-radius: 6px; }

/* ── Потянуть вниз, чтобы обновить ────────────────────────────────────────────
   Новый элемент, а не переоформление существующего: индикатора обновления в
   приложении не было. Оформлен теми же токенами (--text-3, .icon) и той же
   крутилкой adm-spin, что уже жила в стилях админки. --pull (0..1) ведёт JS. */
.adm-refresh {
  position: absolute; z-index: 3; left: 0; right: 0;
  /* 46px тут стояли, чтобы разойтись с полосой `.adm-navbar`. Полосу убрали
     (заголовок теперь только крупный, в потоке), и индикатор висел в пустоте
     посреди контента — подтягиваем его к верхней кромке. */
  top: calc(var(--safe-t) + 10px);
  display: grid; place-items: center;
  pointer-events: none;
  opacity: var(--pull, 0);
  transform: translateY(calc(-14px + 20px * var(--pull, 0)))
             scale(calc(.7 + .3 * var(--pull, 0)));
}
.adm-refresh .icon {
  width: 19px; height: 19px; color: var(--text-3); stroke-width: 2.2;
  /* До порога стрелка ПОВОРАЧИВАЕТСЯ за пальцем — так видно, сколько осталось
     тянуть; после отпускания включается обычное вращение. */
  transform: rotate(calc(var(--pull, 0) * 260deg));
}
.adm-refresh.is-spinning .icon { animation: adm-spin 1s linear infinite; transform: none; }
@media (prefers-reduced-motion: reduce) { .adm-refresh.is-spinning .icon { animation: none; } }

/* ── Плашка «Отменить» ───────────────────────────────────────────────────────
   Отдельный элемент, а не .toast с кнопкой внутри: обычный тост зовут из
   десятка мест и он ставит textContent — добавлять туда разметку значило бы
   переделывать общий компонент ради одного случая. Геометрия и цвета взяты у
   .toast один в один, чтобы это читалось как та же плашка. */
.toast-undo {
  position: fixed;
  left: 50%; bottom: calc(var(--tabbar-h) + var(--safe-b) + 30px);
  transform: translate(-50%, 16px);
  z-index: 121;
  max-width: min(calc(100vw - 32px), 420px);
  display: flex; align-items: center; gap: 14px;
  padding: 10px 10px 10px 20px;
  border-radius: 100px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line);
  font-size: 13.5px; font-weight: 700;
  box-shadow: var(--shadow-float);
  opacity: 0;
  transition: opacity .28s ease, transform var(--dur-sheet) var(--ease-spring);
  white-space: nowrap;
}
.toast-undo.on { opacity: 1; transform: translate(-50%, 0); }
.toast-undo button {
  flex: none; padding: 8px 16px; border-radius: 100px;
  font-size: 13px; font-weight: 700;
  color: var(--surface); background: var(--text);
  transition: transform .14s ease, opacity .14s ease;
}
.toast-undo button:active { transform: scale(.95); opacity: .9; }
/* Полоска утекающего времени — видно, сколько осталось на «передумать». */
.toast-undo i {
  position: absolute; left: 0; bottom: 0; height: 2px;
  background: var(--text-3); border-radius: 0 0 100px 100px;
  animation: undo-drain 5s linear forwards;
}
@keyframes undo-drain { from { width: 100%; } to { width: 0; } }
@media (prefers-reduced-motion: reduce) { .toast-undo i { animation: none; width: 0; } }

/* ── Долгое нажатие на строку: быстрые действия ──────────────────────────────
   Новый элемент (такого в приложении не было). Собран на существующих токенах:
   поверхность .card, тени, --line, .admin-row внутри. Строка под пальцем
   приподнимается — это единственный намёк, что «держат» именно её. */
.adm-peek-veil {
  position: fixed; inset: 0; z-index: 95;
  background: rgba(0, 0, 0, .18);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  opacity: 0; transition: opacity .18s ease;
}
.adm-peek-veil.on { opacity: 1; }
.adm-peek {
  position: fixed; z-index: 96;
  width: min(240px, calc(100vw - 32px));
  background: var(--surface);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-float);
  overflow: hidden;
  transform-origin: 50% 0;
  transform: scale(.86); opacity: 0;
  transition: transform var(--dur-sheet) var(--ease-spring), opacity .16s ease;
}
.adm-peek.on { transform: scale(1); opacity: 1; }
.adm-peek button {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  padding: 13px 15px; font-size: 15px; font-weight: 600; color: var(--text);
  transition: background-color .14s ease;
}
.adm-peek button + button { box-shadow: inset 0 1px 0 var(--line); }
.adm-peek button:active { background: var(--surface-2); }
.adm-peek button .icon { width: 19px; height: 19px; color: var(--text-2); flex: none; }
.adm-peek button.is-danger, .adm-peek button.is-danger .icon { color: var(--bad); }
.admin-row.is-peeked { transform: scale(1.02); z-index: 2; }

/* ── Медиа проявляется сверху вниз по очереди (см. Motion.orderedMedia) ────── */
img.media-pending { opacity: 0; }
img.media-in { animation: media-in .34s cubic-bezier(.2, .8, .3, 1) both; }
@keyframes media-in { from { opacity: 0; transform: scale(1.06); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  img.media-pending { opacity: 1; }
  img.media-in { animation: none; }
}

/* ── Док (таббар) складывается вниз, как док iPadOS ──────────────────────────
   --dock-hide (0..1) ведёт JS: 1 — убран, 0 — на месте. Значение непрерывное:
   во время жеста «назад» с корня док раскрывается ПРОПОРЦИОНАЛЬНО пальцу. */
.tabbar-wrap {
  transform:
    translateX(-50%)
    translateY(calc(var(--dock-hide, 0) * 155%))
    scale(calc(1 - var(--dock-hide, 0) * .1));
  transform-origin: 50% 130%;
  opacity: max(0, calc(1 - var(--dock-hide, 0) * 1.35));
}
.tabbar-wrap.is-tucked { pointer-events: none; }

/* Затемнение главной, когда её открывает уезжающая вправо админка */
.adm-view-scrim {
  position: fixed; inset: 0; z-index: 55;
  background: #000; opacity: 0; pointer-events: none;
}
.adm-view-scrim[hidden] { display: none; }
