/* J Sapron Club — панель администратора. Тот же дизайн-язык, что у клиентского
   приложения (стекло, мягкие тени, пружинистые появления), но со своим акцентом —
   глубокий сливовый вместо розового, чтобы рабочий инструмент сотрудника визуально
   отличался от того, что видит клиент, оставаясь частью одной "семьи". */

:root{
  /* Минимальная ширина холста приложения. Уже неё ничего не ужимаем — включается
     горизонтальная прокрутка (см. @media в разделе про минимальный размер). */
  --app-min-w:320px;
  /* ---- Шрифты ----
     Rubik — весь интерфейс. Гротеск со скруглёнными углами, и один из немногих
     бесплатных, кто закрывает разом латиницу, кириллицу И иврит — а приложение
     трёхъязычное (см. i18n.js: ru / en / he). Прежняя пара Fraunces + Libre Franklin
     иврита не знала вовсе: ивритские надписи уезжали в системный запасной шрифт и
     выглядели чужими рядом с остальным экраном.
     Manrope — ТОЛЬКО цифры: баланс, суммы, ячейки SMS-кода. Он намеренно не круглый:
     на фоне мягкого Rubik прямые жирные цифры читаются как вес, а не как пузыри.
     Оба шрифта приезжают одной таблицей Google Fonts (см. index.html), и адрес у
     клиентского приложения и у админки буквально один и тот же — второе из них берёт
     шрифты уже из кэша браузера, не скачивая заново. */
  --font-ui:"Rubik", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-num:"Manrope", var(--font-ui);
  --bg:#F7F1EE; --bg-2:#EFE0DA; --surface:#FFFFFF; --surface-glass:rgba(255,255,255,0.78);
  --ink:#241826; --ink-soft:#8B7A87; --border:#EBDCD3;
  --accent:#5B3A63; --accent-deep:#3E2745; --accent-soft:#E7DCEC;
  --gold:#C79A4B; --gold-soft:#F3E3C5;
  --rose:#B8456A; --success:#5C8A6E;
  --shadow-color:rgba(60,30,55,0.16);
}
@media (prefers-color-scheme: dark){
  :root{
    --bg:#170F1B; --bg-2:#231A28; --surface:#241A2C; --surface-glass:rgba(36,26,44,0.68);
    --ink:#F5EAF0; --ink-soft:#C2AEC0; --border:#3A2A40;
    --accent:#B892C4; --accent-deep:#8C68A0; --accent-soft:#3A2C42;
    --gold:#E3C27A; --gold-soft:#4A3B22; --rose:#E8829E; --success:#8FCB9F;
    --shadow-color:rgba(0,0,0,0.5);
  }
}
*{ box-sizing:border-box; }
/* 100dvh рядом со 100vh — не дубль: на мобильных 100vh считается от экрана с уже
   свёрнутой адресной строкой, поэтому при загрузке низ страницы уходит под неё.
   dvh пересчитывается сам, пока браузер прячет/показывает свою панель. */
/* min-width вместо прежнего overflow-x:hidden. hidden не спасал от тесноты, он просто
   ОБРЕЗАЛ то, что не влезло: на очень узком экране правый край таблиц и кнопок молча
   уходил за границу и достать его было нечем. Теперь ниже --app-min-w ничего не
   ужимается — появляется горизонтальная прокрутка, и всё остаётся доступным. */
/* touch-action: pan-x pan-y — разрешены только прокрутки, щипок и двойное касание
   масштаба не дают. Это второй из трёх запретов масштабирования (первый — метатега
   viewport в index.html, третий — no-zoom.js): именно он снимает увеличение по двойному
   касанию в Safari, где метатегу не верят. pan-x нужен наравне с pan-y: ниже --app-min-w
   появляется горизонтальная прокрутка, и manipulation её бы оставил, но заодно оставил
   бы и щипок. */
/* И на корне тоже: в этой панели страница прокручивается сама (не как в клиентском
   приложении, где body закреплён), поэтому жест может начаться и на области выше body. */
/* scrollbar-gutter: место под полосу прокрутки резервируется всегда. Вкладки разной
   высоты: на "Сводке" полоса не нужна, на "Сервисах" нужна, и без этого при каждом
   переключении содержимое дёргалось бы по ширине на толщину полосы. На телефоне
   правило ничего не меняет — там полоса накладывается поверх и места не занимает. */
html{ touch-action:pan-x pan-y; scrollbar-gutter:stable; }
body{ margin:0; min-height:100vh; min-height:100dvh; background:var(--bg); color:var(--ink);
  font-family:var(--font-ui); position:relative; min-width:var(--app-min-w);
  display:flex; flex-direction:column; -webkit-tap-highlight-color:transparent;
  touch-action:pan-x pan-y; }
.hidden{ display:none !important; }

/* ---- Спокойный фон: два размытых пятна, почти неподвижные — рабочий инструмент, не шоу ---- */
.ambient-bg{ position:fixed; inset:0; z-index:0; overflow:hidden; pointer-events:none; }
.blob{ position:absolute; border-radius:50%; filter:blur(80px); opacity:.35; animation:drift 40s ease-in-out infinite alternate; }
.blob-1{ width:360px; height:360px; left:-100px; top:-100px; background:var(--accent); }
.blob-2{ width:320px; height:320px; right:-110px; bottom:-80px; background:var(--gold); animation-delay:-14s; }
@keyframes drift{
  from{ transform:translate(0,0) scale(1); }
  to{ transform:translate(18px,-14px) scale(1.06); }
}
@media (prefers-reduced-motion: reduce){ .blob{ animation:none; } }
/* Тёмная тема: золотое пятно у нижнего края читалось не мягким свечением, а тёплой
   полосой по низу экрана (видно на экране входа под переключателем языка). Пятна
   размываем сильнее и приглушаем, золотое — заметнее прочих. */
@media (prefers-color-scheme: dark){
  .blob{ filter:blur(110px); opacity:.24; }
  .blob-2{ opacity:.16; }
}

.brand{ font-family:var(--font-ui); font-weight:600; font-size:20px; display:flex; align-items:center; gap:8px; }
.brand.small{ font-size:17px; }
.brand-logo{ width:3.3em; height:3.3em; border-radius:50%; object-fit:cover; flex:none; position:relative; z-index:1; }
/* Обёртка логотипа. Раньше держала вокруг него венок из приборов (logo2.png фоном
   в ::before), сейчас только центрирует круг и задаёт ему размер шрифтом: сам круг
   считается в em от неё. */
.logo-halo{ position:relative; display:inline-flex; align-items:center; justify-content:center; flex:none; }
.tag{
  font-family:var(--font-ui); font-size:11px; letter-spacing:0.04em; text-transform:uppercase;
  background:var(--accent-soft); color:var(--accent); padding:2px 8px; border-radius:999px; font-weight:600;
}
.brand-title{ display:inline-flex; align-items:center; gap:6px; }
.login-card .brand{
  text-align:center; justify-content:center; flex-direction:column; gap:10px;
  font-size:22px; margin:12px 0 6px;
}
.login-card .logo-halo{ font-size:52px; }

/* Телефон — базовый случай: экран занят приложением целиком, без карточки-рамки
   посреди страницы (её добавляет @media в конце файла, только для широкого экрана).
   Отступы считаются от безопасной области, чтобы содержимое не пряталось под вырезом
   камеры сверху и полосой-жестом снизу. Место под переключатель языка здесь больше
   не резервируется: он лежит отдельным подвалом в потоке и занимает своё. */
/* flex:1 0 auto, а не flex:1. Короткое flex:1 разворачивается в flex:1 1 0% — нулевой
   базис плюс разрешённое сжатие, то есть экран соглашается стать ниже собственного
   содержимого, и на низком окне панель наезжает на подвал с языками. Базис auto и
   flex-shrink:0 означают: расти до всей свободной высоты можно, ужиматься ниже
   содержимого — нельзя; вместо этого прокручивается страница. */
.view{ flex:1 0 auto; width:100%; max-width:none; margin:0;
  padding:calc(env(safe-area-inset-top) + 26px) 18px 26px;
  position:relative; z-index:1; }
/* Форма входа: экран целиком на телефоне, карточка по центру на большом экране. */
#view-login{ display:flex; align-items:stretch; padding:0; }

/* ---- Заставка (пока Firebase вспоминает вошедшего сотрудника) ---- */
/* Живёт ровно до первого ответа onAuthStateChanged, после чего app.js плавно гасит её и
   удаляет из разметки — обратно она не возвращается даже при выходе из аккаунта. */
/* Слой поверх приложения, а не ещё один экран в потоке: нужный экран под ней уже
   показан и просто проявляется, когда заставка гаснет, — иначе на время гашения оба
   экрана стояли бы друг под другом и интерфейс дёргался. */
/* Цвета здесь намеренно ЖЁСТКИЕ, а не var(--bg)/var(--ink), и намеренно фиолетовые, хотя
   сама панель светло-бежевая. Причина — системный экран запуска Android: по значку сначала
   показывается не наша страница, а экран, который телефон рисует сам, — значок из манифеста
   на фоне background_color. Подложка значка у админки тёмно-фиолетовая (#3E2745, чтобы
   на рабочем столе её было не спутать с клиентским приложением), поэтому и background_color
   выставлен такой же, и заставка тоже: тогда подложка сливается с фоном и переход от
   системного экрана к нашему не виден. К бежевой панели приложение уходит уже своим
   плавным гашением (.splash-out ниже), а не рывком. */
#view-splash{
  position:fixed; inset:0; z-index:5;
  /* Высота — 100svh, а не растяжка от inset:0. У position:fixed нижний край считается
     по БОЛЬШОМУ видимому окну — тому, каким экран становится, когда браузер прячет свою
     адресную строку. Пока строка на месте, заставка снизу уходит под неё, и середина
     нашего блока оказывается ниже середины того, что человек реально видит: логотип с
     подписью выглядят сползшими вниз, хотя в разметке они ровно по центру. 100svh — это
     высота окна в его самом маленьком состоянии (со строкой), поэтому центр заставки
     совпадает с центром видимой области всегда. В установленном приложении адресной
     строки нет, svh там равен всей высоте — ничего не меняется. Старые браузеры без svh
     просто пропустят строку и останутся на прежнем поведении. */
  height:100svh;
  display:flex; align-items:center; justify-content:center; padding:24px;
  background:#3E2745; color:#F5EAF0;
  /* Фон гаснет НЕ сразу, а после содержимого — задержка здесь и есть вторая доля такта
     (см. блок про уход в два такта ниже). */
  transition:opacity .24s ease .2s;
}

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

   Такт первый: содержимое заставки (логотип с названием, подпись, полоска) уезжает вверх
   и гаснет. Такт второй: когда содержимого уже нет, гаснет фон — под ним пусто, потому
   что следующий экран стоит на паузе (.screen-hold ниже). Каскад появления начинается
   только после того, как заставку убрали из разметки, — это делает hideSplash в app.js. */
@keyframes splashOut{
  from{ opacity:1; transform:translateY(0); }
  to{ opacity:0; transform:translateY(-10px); }
}
#view-splash.splash-out{ opacity:0; pointer-events:none; }
#view-splash.splash-out .splash-brand,
#view-splash.splash-out .splash-line,
#view-splash.splash-out .splash-bar{
  animation-name:splashOut;
  animation-duration:.18s;
  animation-timing-function:ease-in;
  animation-fill-mode:both;
}
#view-splash.splash-out .splash-line{ animation-delay:.05s; }
#view-splash.splash-out .splash-bar{ animation-delay:.09s; }
/* Полоска на уходе перестаёт бегать: блик, скользящий внутри уезжающей полоски, — второе
   движение поверх первого, и оба читаются хуже. */
#view-splash.splash-out .splash-bar span{ animation:none; }

/* Пауза для экрана под заставкой. Без неё форма входа и разделы панели проявляются своими
   анимациями вхолостую — под непрозрачной заставкой, — и к моменту, когда та начинает
   гаснуть, всё уже стоит готовым и просвечивает сквозь неё. Снимает паузу
   releaseScreenCascade в app.js, когда заставки на экране уже нет совсем. */
#view-login.screen-hold .card,
#view-panel.screen-hold .panel-header,
#view-panel.screen-hold .panel-section{ animation-play-state:paused; }

@media (prefers-reduced-motion: reduce){
  /* Без анимаций заставка просто гаснет, и ждать содержимое фону незачем. */
  #view-splash{ transition-delay:0s; }
  #view-splash.splash-out .splash-brand,
  #view-splash.splash-out .splash-line,
  #view-splash.splash-out .splash-bar{ animation:none; }
}
/* Сдвиг вниз — чтобы по центру экрана оказался ЛОГОТИП, а не карточка целиком. Под
   логотипом в карточке ещё название, подпись и полоска, поэтому центрирование карточки
   поднимает сам логотип выше середины (на 375×812 — на 44 px). Системный экран запуска
   Android ставит значок ровно по центру, и при переходе логотип заметно прыгал вверх.
   Число равно половине высоты всего, что лежит под логотипом; на layout не влияет —
   transform не двигает соседей и не меняет размеры карточки.

   НО этот сдвиг нужен только там, где системный экран запуска вообще есть, — в
   установленном приложении. В браузере ему предшествует не значок по центру, а вкладка
   с адресной строкой, и опущенный логотип просто читается как «съехал вниз». Поэтому по
   умолчанию сдвига нет (карточка стоит по центру, логотип — чуть выше середины, как
   глаз и ждёт), а 44 px возвращаются только в standalone/fullscreen, то есть когда
   приложение открыто с рабочего стола. */
.splash-card{ display:flex; flex-direction:column; align-items:center; gap:16px; transform:translateY(var(--splash-shift, 0px)); }
@media (display-mode: standalone), (display-mode: fullscreen){
  .splash-card{ --splash-shift:44px; }
}
/* Название, подпись и полоска проявляются НЕ сразу, а через четверть секунды после
   логотипа. На системном экране запуска есть только значок, без текста. Если наша заставка
   появляется целиком и разом, глаз читает это как «открылась вторая страница»; когда же
   первый кадр — один логотип, а остальное всплывает следом, переход читается как
   продолжение того же экрана. */
.splash-brand .brand-title, .splash-line, .splash-bar{ animation:splashFadeIn .45s ease-out .25s backwards; }
@keyframes splashFadeIn{
  from{ opacity:0; transform:translateY(6px); }
  to{ opacity:1; transform:translateY(0); }
}
@media (prefers-reduced-motion: reduce){
  .splash-brand .brand-title, .splash-line, .splash-bar{ animation:none; }
}
.splash-brand{ flex-direction:column; gap:10px; font-size:20px; text-align:center; }
.splash-brand .logo-halo{ font-size:46px; }
.splash-line{
  margin:0; min-height:1.3em; text-align:center;
  font-family:var(--font-ui); font-style:italic; font-size:14px; color:#C2AEC0;
  transition:opacity .22s ease;
}
/* Тонкий блик, скользящий по полоске: видно, что панель занята делом, а не зависла. */
/* Цвета, как и у всей заставки, жёсткие: полоска лежит на тёмно-фиолетовом фоне, а не на
   фоне панели, поэтому обычные --border и --accent на ней просто не видны. */
.splash-bar{ width:130px; height:3px; border-radius:2px; background:rgba(255,255,255,0.18); overflow:hidden; }
.splash-bar span{
  display:block; width:42%; height:100%; border-radius:2px;
  background:linear-gradient(90deg, #B892C4, #E3C27A);
  animation:splashSweep 1.3s ease-in-out infinite;
}
@keyframes splashSweep{
  from{ transform:translateX(-115%); }
  to{ transform:translateX(255%); }
}
@media (prefers-reduced-motion: reduce){
  .splash-bar span{ animation:none; width:100%; }
}

/* Матовая подложка остаётся и на телефоне — сквозь неё просвечивают цветные пятна
   фона; уходят только граница, скругления и тень: им неоткуда быть видимыми, когда
   вокруг карточки нет «страницы». */
.card{
  background:var(--surface-glass); backdrop-filter:blur(20px) saturate(150%); -webkit-backdrop-filter:blur(20px) saturate(150%);
  padding:26px; width:100%;
  animation:cardFade .4s ease-out both;
}
/* Форма входа на телефоне — это весь экран, содержимое по центру по вертикали. */
.login-card{
  flex:1; justify-content:center;
  padding:calc(env(safe-area-inset-top) + 30px) 20px 30px;
}
/* На весь экран пружинистый «вылет со скейлом» читается как рывок всего интерфейса
   и на время анимации вылезает за края, поэтому на телефоне — просто проявление. */
@keyframes cardFade{ from{ opacity:0; } to{ opacity:1; } }
@keyframes cardIn{ from{ opacity:0; transform:translateY(16px) scale(.98); } to{ opacity:1; transform:translateY(0) scale(1); } }
@media (prefers-reduced-motion: reduce){ .card{ animation:none; } }
.card.shake{ animation:shake .45s; }
@keyframes shake{
  10%,90%{ transform:translateX(-1px); } 20%,80%{ transform:translateX(3px); }
  30%,50%,70%{ transform:translateX(-6px); } 40%,60%{ transform:translateX(6px); }
}

.hint{ font-size:13px; color:var(--ink-soft); margin:8px 0 18px; }

/* ---- Экран второго шага входа (#view-mfa) ---- */
/* Отпечаток и предложение его включить. Оба блока — одна мысль и одна кнопка под ней:
   на этом экране человек не выбирает из списка, он подтверждает, что это он. */
.mfa-pass, .mfa-code{ display:flex; flex-direction:column; gap:10px; }
/* Переход на другой путь («ввести код», «войти отпечатком», «настроить код вместо этого»).
   Тише обычной второстепенной кнопки: этим пользуются, когда обычное не сработало, и
   выглядеть наравне с главным действием оно не должно. */
.mfa-alt{ font-size:13px; opacity:.75; }
/* Настройка показывается один раз, при первом входе, и на телефоне обязана уместиться над
   клавиатурой вместе с полем для кода: поэтому блок плотный, без собственной рамки и без
   заголовка — карточка входа у него уже есть. */
.mfa-setup{ display:flex; flex-direction:column; gap:10px; margin-bottom:6px; }
/* Ссылка ведёт по otpauth:// — телефон открывает по ней аутентификатор и добавляет панель
   одним касанием. Выглядит кнопкой, потому что это и есть главное действие экрана. */
.mfa-link{ text-align:center; text-decoration:none; }
.mfa-secret-label{ font-size:12px; color:var(--ink-soft); margin:0; }
/* Квадратик для камеры. Белое поле вокруг него нарисовано в самом SVG (тихая зона по
   стандарту), поэтому здесь только размер и рамка. Ширину держим небольшой: снимают его
   с расстояния в ладонь, и код на пол-экрана нужен не больше, чем на четверть. */
.mfa-qr-box{ display:flex; flex-direction:column; gap:8px; }
.mfa-qr{ align-self:center; width:min(184px, 62vw); }
/* Белый фон и рамка — на самом блоке, а не только внутри картинки: в тёмной теме SVG
   остаётся чёрно-белым (иначе камера его не прочтёт), и без своей подложки он висел бы
   белым пятном с рваным краем. */
.mfa-qr svg{
  display:block; width:100%; height:auto;
  background:#FFFFFF; border-radius:10px;
  border:1px solid var(--border);
}
/* Ключ набирают руками с экрана на экран, поэтому он моноширинный, крупнее подписи и
   разбит пробелами по четыре знака (разбивает app.js). break-all — чтобы длинная строка
   переносилась внутри карточки, а не растягивала её за край телефона. */
.mfa-secret{
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:15px; letter-spacing:.06em; word-break:break-all;
  background:var(--paper-soft, rgba(0,0,0,0.04)); border-radius:10px; padding:10px 12px;
}
/* Поле кода: шесть цифр посередине, крупно — их набирают, переписывая с другого экрана. */
#mfa-input{
  font-size:22px; letter-spacing:.35em; text-align:center;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* Выход с этого экрана — не повседневная кнопка, а запасной ход для того, кто вошёл не тем
   аккаунтом. Поэтому он тихий и стоит внизу, отдельно от «Подтвердить». */
.mfa-back{ margin-top:14px; align-self:center; font-size:13px; }
/* Под полем списания подсказки идут парой — потолок по чеку и сколько баллов доступно, —
   и читаться должны как один абзац. Поэтому собственных отступов у них нет: расстояние
   задаёт gap самой формы. Пустая подсказка исчезает совсем, иначе в flex-колонке она
   занимала бы ещё один такой gap ни за чем. */
#redeem-cap-hint, #redeem-available-hint{ margin:0; }
#redeem-cap-hint:empty, #redeem-available-hint:empty{ display:none; }
.form, .login-card{ display:flex; flex-direction:column; gap:14px; }
label{ display:flex; flex-direction:column; gap:6px; font-size:13px; color:var(--ink-soft); font-weight:500; }
input{
  border:1px solid var(--border); border-radius:12px; padding:11px 13px; font-size:15px;
  background:var(--bg); color:var(--ink); font-family:inherit; transition:border-color .15s ease, box-shadow .15s ease;
}
/* Подсказка в поле — образец, а не значение: в цвет ввода она читается как уже
   набранный номер. opacity:1 — иначе Firefox приглушит её ещё раз поверх цвета. */
input::placeholder{
  color:color-mix(in srgb, var(--ink-soft) 45%, transparent);
  opacity:1;
}
input:focus{
  outline:none; border-color:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent);
}

.btn-primary, .btn-secondary{
  position:relative; border:none; border-radius:13px; padding:12px; font-size:14.5px; font-weight:600;
  cursor:pointer; font-family:inherit; color:#fff; transition:transform .15s ease, box-shadow .15s ease, filter .15s ease;
}
.btn-primary{
  background:linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
  box-shadow:0 10px 22px -10px color-mix(in srgb, var(--accent-deep) 65%, transparent);
}
.btn-secondary{
  background:linear-gradient(135deg, var(--success) 0%, color-mix(in srgb, var(--success) 65%, #1c3324) 100%);
  box-shadow:0 10px 22px -10px color-mix(in srgb, var(--success) 55%, transparent);
}
.btn-primary:hover, .btn-secondary:hover{ transform:translateY(-1px); filter:brightness(1.06); }
.btn-primary:active, .btn-secondary:active{ transform:translateY(0) scale(.97); }
.btn-primary:disabled, .btn-secondary:disabled{ cursor:default; opacity:.85; }
/* ---- Ожидание: хоровод звёзд ----
   Стоит на КАЖДОЙ кнопке админки с состоянием отправки — и сливовых .btn-primary, и
   зелёных .btn-secondary. Раньше здесь крутилось белое колечко из границы; теперь по
   кругу идут восемь звёзд.

   Они не мигают поодиночке: по кольцу убывает и яркость, и РАЗМЕР — от крупной белой
   головы к почти невидимому хвосту, а последняя звезда пустая совсем (нулевой размер).
   Кольцо проворачивается на 45° восемь раз за оборот, и мимо каждой позиции проходит
   сначала голова, потом всё более тусклый хвост: звезда как будто вспыхивает во всю
   величину и тут же ужимается в ничто. Отсюда steps(8): плавное вращение размазало бы
   звёзды в пунктирный круг, а шаг ровно в одну позицию читается как «зажглась следующая».

   Как это сделано. Восемь звёзд — это восемь теней ОДНОГО элемента: лишней разметки под
   спиннер в формах нет, а восемь пустых span'ов в каждую кнопку — плохая цена за
   украшение. Отсюда все приёмы этого блока:
     • сам элемент — точка 3×3 в центре кнопки, её саму не видно нигде;
     • по окружности звёзды расставлены смещением тени (радиус 9 пикселей; пары ±6.4 —
       тот же радиус, разложенный по диагоналям);
     • размер каждой звезды задаёт РАСТЯЖЕНИЕ тени (четвёртое число): +1.7 у головы даёт
       точку 6.4 пикселя, -1.5 у последней — ровно ноль, то есть пустое место. Это
       единственный способ дать восьми теням разный размер: ширина у них общая;
     • размытие тоже убывает: горячая точка светит ореолом, погасшая — уже нет;
     • девятая тень — широкий мягкий ореол ПОД головой (в списке он последний, а тени
       рисуются сверху вниз, поэтому он оказывается позади). Без него голова просто
       белая точка; с ним она светится.

   Шампань --star одинаково читается и на сливе, и на зелени: обе кнопки тёмные с белым
   текстом, так что отдельный вариант для .btn-secondary не нужен.

   Тот же хоровод стоит в клиентском приложении (client-app/style.css). Общего файла
   стилей у двух приложений нет, поэтому блок продублирован намеренно — правку нужно
   вносить в оба. */
.btn-primary .btn-spinner, .btn-secondary .btn-spinner{
  --star:color-mix(in srgb, var(--gold) 42%, #fff);
  display:none; position:absolute; left:50%; top:50%;
  width:3px; height:3px; margin:-1.5px 0 0 -1.5px; border-radius:50%;
  box-shadow:
    0      -9px   5px    1.7px var(--star-head, rgba(255,255,255,.98)),
    6.4px  -6.4px 4px    1.1px color-mix(in srgb, var(--star) 80%, transparent),
    9px     0     3.5px   .6px color-mix(in srgb, var(--star) 60%, transparent),
    6.4px   6.4px 3px     .1px color-mix(in srgb, var(--star) 44%, transparent),
    0       9px   2.5px  -.4px color-mix(in srgb, var(--star) 30%, transparent),
   -6.4px   6.4px 2px    -.9px color-mix(in srgb, var(--star) 18%, transparent),
   -9px     0     1.5px -1.3px color-mix(in srgb, var(--star) 8%,  transparent),
   -6.4px  -6.4px 0     -1.5px transparent,
    0      -9px   7px     1px  color-mix(in srgb, var(--star) 30%, transparent);
  animation:btn-orbit .8s steps(8, end) infinite;
}
@keyframes btn-orbit{ to{ transform:rotate(360deg); } }
.btn-primary.is-loading .btn-label, .btn-secondary.is-loading .btn-label{ visibility:hidden; }
.btn-primary.is-loading .btn-spinner, .btn-secondary.is-loading .btn-spinner{ display:inline-block; }
/* spin остался ради значка обновления списка (.btn-refresh) — кнопки им больше не
   пользуются. */
@keyframes spin{ to{ transform:rotate(360deg); } }
/* Тёмная тема: акценты отражаются в светлые тона, и белая надпись на них перестаёт
   читаться (на сливе 2.5:1 при требуемых 4.5:1, на зелени и того меньше). Обычный
   приём для светлого акцента на тёмном фоне — тёмные чернила на самой кнопке. Тёмный
   конец сливового градиента (--accent-deep #8C68A0) — средний тон, плохой и для белого,
   и для тёмного текста, поэтому в темноте градиент ведём не до него, а до смеси с
   акцентом: кнопка остаётся объёмной, а тёмная надпись держит 4.5:1+ по всей заливке.
   Звёзды ожидания меняют полярность вместе с надписью. */
@media (prefers-color-scheme: dark){
  .btn-primary, .btn-secondary{ color:#1A0F22; }
  .btn-primary{
    background:linear-gradient(135deg, var(--accent) 0%,
      color-mix(in srgb, var(--accent-deep) 55%, var(--accent)) 100%);
  }
  .btn-primary .btn-spinner, .btn-secondary .btn-spinner{
    --star-head:rgba(26,15,34,.95);
    --star:#3E2745;
  }
}
/* При выключенных анимациях хоровод не вертится, а дышит — тот же приём, что у кружка
   ожидания установки (installPulse, объявлен в своём @media ниже). */
@media (prefers-reduced-motion: reduce){
  .btn-primary .btn-spinner, .btn-secondary .btn-spinner{ animation:installPulse 1.4s ease-in-out infinite; }
}

.btn-ghost{
  background:transparent; border:1px solid var(--border); color:var(--ink-soft);
  border-radius:10px; padding:7px 14px; font-size:13px; cursor:pointer; font-family:inherit;
  transition:border-color .15s ease, color .15s ease;
}
.btn-ghost:hover{ border-color:var(--accent); color:var(--accent); }

/* Видимый фокус для ходьбы по Tab: панелью работают с планшета и ноутбука, у инпутов
   кольцо есть (input:focus выше), а у кнопок, вкладок и ссылок не было ничего. :where
   держит специфичность нулевой, поэтому собственные кольца элементов (те же инпуты)
   остаются сильнее. Кольцо повторяет скругление элемента — это делает сам outline. */
:where(button, [href], select, summary):focus-visible{
  outline:2px solid var(--accent); outline-offset:2px;
}

/* Замок на настройках программы. Закрытое поле должно читаться как справка, а не как
   поле, которое почему-то не работает: приглушённая подложка и отсутствие рамки-фокуса
   говорят "это значение показано", а не "здесь что-то сломалось". */
/* ---- Вкладки уровней в «Правилах» ----
   Три одинаковых четвёрки чисел визита (по одной на уровень карты) — вкладками, а не
   одной лентой: на вертикальном экране двенадцать полей подряд неотличимы друг от
   друга. Активная вкладка красится камнем своего уровня — --tier-metal приходит с
   data-tier, тем же правилом, что красит чипы уровня в карточке клиента. */
.settings-tier-tabs{ display:flex; flex-wrap:wrap; gap:6px; margin:0 0 10px; }
.settings-tier-tab{
  font-family:var(--font-ui); font-size:11.5px; font-weight:600; letter-spacing:.05em;
  text-transform:uppercase; padding:7px 12px; border-radius:999px; cursor:pointer;
  border:1px solid var(--border); background:transparent; color:var(--ink-soft);
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.settings-tier-tab:hover:not(.is-active){ border-color:var(--tier-metal); color:var(--tier-metal); }
.settings-tier-tab.is-active{
  cursor:default;
  background:color-mix(in srgb, var(--tier-metal) 15%, transparent);
  border-color:color-mix(in srgb, var(--tier-metal) 55%, transparent);
  color:var(--tier-metal);
}
/* Четвёрка полей уровня — обычные поля формы, без собственной рамки: рамка вокруг
   рамок полей читалась бы как вторая форма внутри первой. display:contents отдаёт
   поля вертикальному ритму самой .form; скрытие вкладки делает общий .hidden. */
.settings-tier-fields{ display:contents; }

.settings-lock{ display:flex; align-items:center; gap:12px; margin:8px 0 16px; }
.settings-lock .hint{ margin:0; flex:1; }
.settings-lock .btn-ghost{ flex:none; }
#settings-section.is-locked input:disabled{
  background:color-mix(in srgb, var(--ink-soft) 7%, transparent);
  color:var(--ink-soft); border-style:dashed; cursor:default;
}
.settings-actions{ display:flex; align-items:center; gap:10px; }
.settings-actions .btn-ghost{ flex:none; padding:12px 18px; border-radius:13px; font-size:14.5px; }
.settings-actions .btn-primary{ flex:1; }

.form-error{
  color:var(--rose); font-size:13px; min-height:16px; margin:0;
  opacity:0; transform:translateY(-2px); transition:opacity .15s ease, transform .15s ease;
}
.form-error:not(:empty){ opacity:1; transform:translateY(0); }
.form-success{ color:var(--success); font-size:13px; margin:0; font-weight:600; }

.panel-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; opacity:0; animation:fadeInUp .3s ease-out both; }
.panel-section{ margin-bottom:22px; opacity:0; animation:fadeInUp .3s ease-out both; }
.panel-section:nth-of-type(1){ animation-delay:.06s; }
.panel-section:nth-of-type(2){ animation-delay:.12s; }
.panel-section:nth-of-type(3){ animation-delay:.18s; }
.panel-section:nth-of-type(4){ animation-delay:.24s; }
.panel-section:nth-of-type(5){ animation-delay:.30s; }
@keyframes fadeInUp{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:translateY(0); } }
@media (prefers-reduced-motion: reduce){ .panel-header, .panel-section{ animation:none; opacity:1; } }
.panel-section h2{ font-family:var(--font-ui); font-size:17px; margin:0 0 10px; }

.search-row{ display:flex; gap:8px; }
.search-row input{ flex:1; }

.client-card{
  position:relative; background:var(--surface); border:1px solid var(--border); border-radius:18px;
  padding:20px 22px; margin-bottom:18px; overflow:hidden;
  animation:clientIn .4s cubic-bezier(.2,.9,.25,1.1) both;
}
@keyframes clientIn{ from{ opacity:0; transform:translateY(10px) scale(.98); } to{ opacity:1; transform:translateY(0) scale(1); } }
@media (prefers-reduced-motion: reduce){ .client-card{ animation:none; } }
.client-card::before{
  content:""; position:absolute; inset:0; border-radius:inherit; padding:1px;
  background:linear-gradient(135deg, var(--gold), transparent 45%, var(--accent) 100%);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude; opacity:.55; pointer-events:none;
}
.client-card.shake{ animation:shake .45s; }
/* Тряска «неверный ввод» — самый резкий жест панели, и до сих пор она была единственной
   анимацией без учёта prefers-reduced-motion. Текст ошибки рядом остаётся — сигнал не
   теряется, уходит только рывок. */
@media (prefers-reduced-motion: reduce){
  .card.shake, .client-card.shake{ animation:none; }
}
.client-name{ font-family:var(--font-ui); font-size:19px; font-weight:600; }
.client-meta{ font-size:13px; color:var(--ink-soft); margin:4px 0 12px; }
.client-points{ font-family:var(--font-num); font-size:15px; font-weight:800; font-variant-numeric:tabular-nums; color:var(--accent); }

/* ---- База клиентов: список всех карточек ---- */
.section-head{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px; }
.section-head h2{ margin:0; }

.refresh-icon{ width:16px; height:16px; flex:none; }
.btn-refresh.is-refreshing{ cursor:default; opacity:.6; }
.btn-refresh.is-refreshing .refresh-icon{ animation:spin .8s linear infinite; }
@media (prefers-reduced-motion: reduce){ .btn-refresh.is-refreshing .refresh-icon{ animation:none; } }

.clients-count{ font-size:12px; color:var(--ink-soft); margin:0 0 10px; }
.clients-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px; }

.client-row{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  background:var(--surface); border:1px solid var(--border); border-radius:14px;
  padding:12px 14px; cursor:pointer;
  transition:border-color .15s ease, transform .12s ease, box-shadow .15s ease;
}
.client-row:hover{ border-color:var(--accent); transform:translateY(-1px); box-shadow:0 8px 18px -14px var(--shadow-color); }
.client-row:active{ transform:translateY(0) scale(.995); }
/* Выбранный клиент — тот, чья карточка сейчас открыта выше. */
.client-row.is-active{ border-color:var(--accent); background:var(--accent-soft); }
.client-row-info{ min-width:0; }
.client-row-name{ font-weight:600; font-size:14.5px; }
/* Телефон и email длиннее строки — обрезаем многоточием, а не ломаем раскладку. */
.client-row-meta{ font-size:12.5px; color:var(--ink-soft); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.client-row-points{ flex:none; font-family:var(--font-num); font-size:13px; font-weight:800; font-variant-numeric:tabular-nums; color:var(--accent); white-space:nowrap; }



/* ---- Вкладки панели ---- */
/* Панель живёт на телефоне одной вертикальной лентой, и пять разделов подряд читались
   как свалка: поиск клиента — то, ради чего её открывают чаще всего, — оказывался в
   середине прокрутки между сводкой и настройками. Сегментированный переключатель
   оставляет на экране один раздел за раз. */
.tabs{
  display:flex; gap:4px; margin-bottom:20px; padding:4px;
  background:var(--accent-soft); border-radius:14px;
  position:relative; /* система координат для .tab-thumb */
}
.tab{
  flex:1 1 0; min-width:0; border:none; background:transparent; color:var(--accent);
  position:relative; z-index:1; /* подпись поверх бегунка */
  font-family:inherit; font-size:13px; font-weight:600; padding:9px 6px; border-radius:11px;
  cursor:pointer; transition:background .15s ease, color .15s ease, box-shadow .15s ease;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.tab:hover{ background:color-mix(in srgb, var(--surface) 55%, transparent); }
.tab.is-active{
  background:var(--surface); color:var(--accent-deep);
  box-shadow:0 4px 10px -6px var(--shadow-color);
}
@media (prefers-color-scheme: dark){ .tab.is-active{ color:var(--ink); } }
/* Разделы внутри вкладки появляются каскадом — тем же, что и раньше; отсчёт задержек
   начинается заново в каждой вкладке, поэтому при переключении анимация не копится. */
.tab-pane{ display:block; }

/* ---- Листание вкладок пальцем ---- */
/* Панель держат одной рукой, и тянуться большим пальцем к узкой кнопке наверху ради
   соседнего раздела неудобно: разделы листаются влево-вправо, как страницы. Кнопки
   вкладок никуда не делись — жест только дублирует их, а переход у них общий, чтобы
   выглядел одинаково независимо от того, чем его начали. */
/* overflow-x:clip, а не hidden: hidden сделал бы обёртку прокручиваемой по вертикали и
   сломал бы прокрутку страницы, а clip просто обрезает по горизонтали и не трогает
   вертикаль. Отрицательные поля выносят границу обрезки на края экрана (за отступы
   .view), иначе раздел исчезал бы, не дойдя до края, — значения парные с padding .view. */
.tab-panes{ --gutter:18px; overflow-x:clip; margin-inline:calc(-1 * var(--gutter)); padding-inline:var(--gutter); }

/* Пока идёт переезд, разделы стоят рядом, как страницы одной ленты: уезжающий уходит за
   край, а следующий всё это время виден и приезжает ему на смену. Значит, на экране их
   двое, и в потоке они бы встали друг под друга во всю свою высоту — поэтому на время
   поездки оба вынимаются из потока, а высоту обёртки держит app.js (по самому высокому
   из двух, чтобы приезжающий не наезжал на подвал с языками). */
.tab-panes.is-sliding{ position:relative; }
.tab-panes.is-sliding > .tab-pane{
  position:absolute; top:0;
  /* Отступы именно var(--gutter), а не 0. Абсолютное позиционирование считается от
     ВНУТРЕННЕЙ границы обёртки вместе с её полями, а в потоке раздел стоит по их
     внутреннему краю: при left/right:0 он в момент начала жеста становился на 2×gutter
     шире и прыгал влево — текст в карточках успевал переверстаться. */
  left:var(--gutter); right:var(--gutter);
  /* Слой под собственный transform: обещание браузеру, что двигать будем именно этот
     элемент, — иначе он перерисовывал бы страницу целиком на каждом кадре жеста. */
  will-change:transform;
}
/* Переход вешает app.js прямо на элемент: длительность и кривая одни и те же и для
   жеста, и для нажатия на вкладку, а пока палец на экране, переход не нужен вовсе —
   раздел обязан идти за пальцем кадр в кадр. */

/* ---- Бегунок активной вкладки ---- */
/* Раньше подложка просто перекрашивалась под нажатой кнопкой — при листании пальцем это
   выглядело обрывом: раздел едет плавно, а отметка вкладки перескакивает. Теперь это
   отдельный слой, который переезжает между кнопками и во время жеста идёт за пальцем,
   показывая, куда попадёшь, если отпустить. */
.tab-thumb{
  position:absolute; top:4px; bottom:4px; left:0; width:0;
  border-radius:11px; background:var(--surface);
  box-shadow:0 4px 10px -6px var(--shadow-color);
  pointer-events:none; z-index:0;
  transition:transform .34s cubic-bezier(.22,.9,.28,1), width .34s cubic-bezier(.22,.9,.28,1);
}
/* Палец на экране — бегунок обязан успевать за ним, а не догонять со своим переходом. */
.tabs.is-tracking .tab-thumb{ transition:none; }
/* Подпись должна лежать поверх бегунка, а собственную подложку активной кнопки снимаем:
   теперь её рисует бегунок. Правило про has-thumb, а не безусловное, — на случай, если
   app.js не дошёл до создания бегунка: тогда работает прежняя подсветка кнопки. */
.tabs.has-thumb .tab.is-active,
.tabs.has-thumb .tab.is-active:hover{ background:transparent; box-shadow:none; }

/* При выключенных анимациях разделы просто подменяются: app.js в этом режиме не запускает
   поездку вовсе, а бегунок переставляется без перехода. */
@media (prefers-reduced-motion: reduce){
  .tab-thumb{ transition:none; }
}

/* Четвёртая вкладка ("Сервисы") сузила каждую кнопку примерно до 66px на самом узком
   экране, и "Программа" начала обрезаться многоточием. На узких экранах немного
   уменьшаем шрифт и внутренние отступы: целиком читаемая подпись здесь важнее её размера.
   С «Ресурсами» и «Командой» тот же расчёт сместился на два шага: кнопок стало шесть, а
   самые длинные подписи прежней длины, поэтому и уменьшать начинаем раньше. */
@media (max-width:480px){
  .tabs{ gap:2px; }
  .tab{ font-size:12px; padding:9px 3px; }
}

/* Ещё уже — ещё пункт вниз, и заодно почти без внутренних отступов: на 375px (самый
   ходовой размер телефона) шести подписям не хватало ровно пары точек, и по-английски
   "Summary" обрезалась многоточием. По обрезанной подписи не поймёшь, куда ведёт вкладка,
   а отступ по бокам кнопки не значит ничего: соседей разводит зазор ряда. */
@media (max-width:420px){
  .tab{ font-size:11px; padding:9px 1px; }
}
/* Полпункта сверх того — на 375px английская "Summary" помещалась в свою кнопку с
   запасом в треть точки, и его съедало округление: на живом экране подпись то влезала
   целиком, то обрывалась многоточием. Полпункта размера дают ей уже пару точек запаса —
   разницы в чтении никакой, а подпись перестаёт зависеть от округления. */
@media (max-width:400px){
  .tab{ font-size:10.5px; }
}
/* Ниже 360px подписи не помещаются ни при каком размере, при котором их ещё можно
   прочесть. Тогда ряд перестаёт делить ширину поровну и прокручивается вбок: активная
   вкладка всегда видна (app.js подводит её сам), а до соседей можно и дотянуться пальцем —
   тем более что разделы всё равно листаются жестом. */
@media (max-width:360px){
  .tabs{ overflow-x:auto; scrollbar-width:none; }
  .tabs::-webkit-scrollbar{ display:none; }
  .tab{ flex:0 0 auto; font-size:11px; padding:9px 9px; }
}

/* ---- Сервисы: список ссылок наружу ---- */
/* Ссылки сделаны карточками во всю ширину, а не строчками текста: панель открывают
   с телефона одним пальцем, и в момент, когда "всё сломалось", промахнуться мимо
   нужной строки — последнее, что нужно. Заодно под каждым названием помещается
   строка о том, что там лежит: "Firestore" сама по себе не говорит владелице салона
   ничего, а "база клиентов" говорит. */
.link-group-title{
  font-family:var(--font-ui); font-size:11px; font-weight:600;
  letter-spacing:0.08em; text-transform:uppercase; color:var(--ink-soft);
  margin:18px 0 8px;
}
.link-list{ list-style:none; margin:0; padding:0; display:grid; gap:8px; }

/* ---- Журнал входов в панель (вкладка «Сервисы») ---- */
/* Список двухуровневый: адрес → входы с него. Свёрнутая группа занимает одну строку,
   поэтому на вертикальном экране виден весь журнал целиком, а не три записи из тридцати. */
.login-list{ list-style:none; margin:10px 0 0; padding:0; display:flex; flex-direction:column; gap:8px; }
.login-group{ background:var(--surface); border:1px solid var(--border); border-radius:14px; }
/* Незнакомое устройство подсвечено рамкой, а не только значком: по списку глаз идёт
   сверху вниз и цепляется за цвет раньше, чем читает подписи. */
.login-group.is-new{ border-color:var(--accent); background:var(--accent-soft); }
.login-group-head{ display:flex; align-items:center; gap:8px; padding:8px 10px 8px 12px; }
/* Нажимается весь заголовок, а не стрелка: попасть пальцем в стрелку на телефоне трудно. */
.login-group-toggle{
  flex:1 1 auto; min-width:0; display:flex; align-items:center; gap:10px;
  background:transparent; border:0; padding:4px 0; margin:0;
  font-family:inherit; color:inherit; text-align:start; cursor:pointer;
}
.login-group-text{ display:flex; flex-wrap:wrap; align-items:center; gap:4px 8px; min-width:0; flex:1 1 auto; }
/* Адрес — моноширинным: цифры одинаковой ширины сравниваются глазом, а не чтением. */
.login-ip{
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:13px; font-weight:600; overflow-wrap:anywhere;
}
.login-ip.is-unknown{ font-family:inherit; color:var(--ink-soft); font-weight:500; }
.login-group-sub{ font-size:12px; color:var(--ink-soft); font-variant-numeric:tabular-nums; }
.login-caret{ flex:none; width:12px; height:12px; color:var(--ink-soft); transition:transform .15s ease; }
.login-group.is-open .login-caret{ transform:rotate(180deg); }
/* Вложенные входы: рамки у них нет — она уже есть у группы, и вторая делала бы список
   рябым. Разделяет их тонкая линия и общий отступ слева, под заголовком. */
.login-sublist{
  list-style:none; margin:0; padding:0 12px 8px; display:flex; flex-direction:column;
}
/* display:flex выше сильнее встроенного [hidden]{display:none}, поэтому свёрнутость
   группы прописана явно — без этого кнопка сворачивания ничего не сворачивала. */
.login-sublist[hidden]{ display:none; }
.login-row{
  display:flex; align-items:flex-start; justify-content:space-between; gap:10px;
  padding:8px 0; border-top:1px solid var(--border);
}
.login-row.is-new .login-when{ color:var(--accent); }
.login-info{ min-width:0; }
.login-when{ font-size:13px; font-weight:600; font-variant-numeric:tabular-nums; }
.login-who{ font-size:13px; color:var(--ink-soft); overflow:hidden; text-overflow:ellipsis; }
.login-badge{
  flex:none; font-size:11px; font-weight:700; letter-spacing:.02em;
  background:var(--accent); color:#fff; border-radius:999px; padding:3px 9px; white-space:nowrap;
}
/* Строка под списком: «скрыть просмотренные» слева, счётчик и возврат справа. Кнопки
   текстовые и мелкие намеренно — это действия про порядок в списке, а не про работу. */
.logins-actions{
  display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap;
  gap:6px 12px; margin-top:10px;
}
.logins-hidden-count{ font-size:12px; color:var(--ink-soft); margin-inline-start:auto; }
.btn-link{
  background:none; border:0; padding:0; margin:0; font-family:inherit; font-size:12.5px;
  color:var(--accent); cursor:pointer; text-decoration:underline; text-underline-offset:3px;
}
.btn-link:hover{ color:var(--accent-deep); }
.link-card{
  display:flex; align-items:flex-start; gap:10px; text-decoration:none; color:inherit;
  background:var(--surface); border:1px solid var(--border); border-radius:14px;
  padding:12px 14px; transition:border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.link-card:hover{
  border-color:var(--accent); transform:translateY(-1px);
  box-shadow:0 8px 18px -12px var(--shadow-color);
}
.link-card:active{ transform:translateY(0); }
.link-text{ display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 auto; }
.link-title{ font-size:14px; font-weight:600; color:var(--accent); overflow-wrap:anywhere; }
@media (prefers-color-scheme: dark){ .link-title{ color:var(--ink); } }
.link-desc{ font-size:12px; line-height:1.4; color:var(--ink-soft); }
/* Адрес клиентского приложения показан прямо в карточке: его чаще диктуют клиенту
   вслух или переписывают, чем открывают сами. */
.link-url{
  font-family:var(--font-ui); font-size:12px; letter-spacing:0.02em;
  color:var(--gold); margin-top:2px; overflow-wrap:anywhere;
}
.link-go{ flex:none; color:var(--ink-soft); font-size:14px; line-height:1.5; }
.project-line{ font-size:12px; color:var(--ink-soft); margin:0 0 4px; }
.project-line code{
  font-family:var(--font-ui); font-size:13px; letter-spacing:0.02em;
  color:var(--accent); background:var(--accent-soft); border-radius:6px; padding:1px 6px;
  unicode-bidi:plaintext;
}

/* ---- Кнопка-иконка (обновить, выгрузить) ---- */
/* Подпись у таких кнопок живёт в aria-label и title: рядом с заголовком раздела две
   текстовые кнопки весили больше самого заголовка. */
.icon-btn{
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  width:34px; height:34px; padding:0; border-radius:10px;
  border:1px solid var(--border); background:transparent; color:var(--ink-soft);
  cursor:pointer; transition:border-color .15s ease, color .15s ease;
}
.icon-btn:hover{ border-color:var(--accent); color:var(--accent); }
.icon-btn svg{ width:16px; height:16px; }
/* Кнопка фильтра горит, пока список показан не целиком или не в обычном порядке. */
.icon-btn.is-active{ border-color:var(--accent); color:var(--accent); background:var(--accent-soft); }

/* ---- Фильтр и сортировка списка клиентов ---- */
/* Два селектора в одну строку: панель раскрывается под заголовком и не должна
   отъедать у вертикального экрана больше одной строки. */
.clients-filter{ display:flex; gap:8px; margin:0 0 10px; }
.filter-field{ flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:4px; }
.filter-field span{ font-size:12px; color:var(--ink-soft); }
.filter-field select{
  width:100%; min-width:0;
  border:1px solid var(--border); border-radius:12px; padding:9px 11px; font-size:14px;
  background:var(--bg); color:var(--ink); font-family:inherit;
}
.filter-field select:focus{
  outline:none; border-color:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent);
}

/* ---- Операция с баллами: своя карточка на каждую ---- */
.op-card{
  background:var(--surface); border:1px solid var(--border); border-radius:16px;
  padding:16px; margin-bottom:12px;
}
.op-card h3{
  font-family:var(--font-ui); font-size:15px; font-weight:600; margin:0 0 12px;
}

/* ---- Общая тревожная строка (App Check, необработанный сбой) ---- */
/* Поверх всех экранов: беда, ради которой она существует, ломает панель целиком, и
   привязывать её к какой-то одной вкладке было бы враньём о масштабе. Розовая — тем же
   цветом, каким панель говорит обо всём тревожном. */
.app-alert{
  position:fixed; top:0; left:0; right:0; z-index:60;
  display:flex; align-items:flex-start; gap:10px;
  padding:calc(10px + env(safe-area-inset-top)) 14px 10px;
  background:color-mix(in srgb, var(--rose) 92%, #000 8%); color:#fff;
  font-size:13px; line-height:1.45;
  box-shadow:0 2px 12px rgba(0,0,0,.25);
}
.app-alert.hidden{ display:none; }
.app-alert span{ flex:1 1 auto; min-width:0; }
.app-alert-close{
  flex:0 0 auto; border:0; background:transparent; color:#fff;
  font-size:15px; line-height:1; padding:2px 4px; cursor:pointer; opacity:.85;
}
.app-alert-close:active{ opacity:.6; }

/* ---- Ресурсы: вердикт, столбики расхода, пределы ---- */
/* Вердикт — единственная строка, ради которой сюда заходят («хватает / пора докупать»),
   поэтому он крупнее подписей вокруг и покрашен по состоянию: спокойный — цветом текста,
   тревожный — розовым. Отдельной иконки нет намеренно: цвет плюс текст читаются и в
   темноте, и на солнце, а значок пришлось бы объяснять. */
.res-verdict{
  margin:2px 0 14px; padding:10px 12px; border-radius:12px;
  background:var(--accent-soft); color:var(--accent-deep);
  font-size:13px; line-height:1.45;
}
@media (prefers-color-scheme: dark){ .res-verdict{ color:var(--ink); } }
.res-verdict[data-state="idle"]{ display:none; }
.res-verdict[data-state="warn"]{
  background:color-mix(in srgb, var(--rose) 14%, transparent); color:var(--rose);
}

/* Столбики по дням. Высота считается в app.js от самого высокого дня, здесь только
   геометрия: ряд одинаковых колонок, которые сжимаются вместе с экраном. Отказы и
   упёршиеся в потолок рисуются вторым цветом поверх того же столбика — так видно не
   только «сколько ушло», но и «сколько не ушло». */
.res-chart{
  display:flex; align-items:flex-end; gap:2px; height:68px; margin:2px 0 4px;
  padding:6px 8px; background:var(--surface); border:1px solid var(--border); border-radius:14px;
}
.res-bar{
  flex:1 1 0; min-width:0; display:flex; flex-direction:column; justify-content:flex-end;
  height:100%; gap:1px;
}
/* Пустой день — тонкая черта у основания: иначе в ряду образуются провалы, по которым
   не отличить «в этот день не входил никто» от «этого дня в ряду нет вовсе». */
.res-bar i{ display:block; border-radius:2px; background:var(--accent); min-height:2px; }
.res-bar i.res-bar-empty{ background:var(--border); }
.res-bar i.res-bar-bad{ background:var(--rose); }
.res-chart-foot{
  display:flex; justify-content:space-between; gap:8px; margin:0 0 4px;
  font-family:var(--font-ui); font-size:11px; color:var(--ink-soft);
}

/* Пределы: слева — что ограничено и сколько раз в это упёрлись, справа — само число.
   Строкой, а не плиткой: читают их сверху вниз и подряд, а не выхватывают глазом одну. */
.res-limits{ list-style:none; margin:0 0 6px; padding:0; }
.res-limits li{
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  padding:8px 0; border-bottom:1px solid var(--border); font-size:13px;
}
.res-limits li:last-child{ border-bottom:none; }
.res-limit-text{ min-width:0; }
.res-limit-hits{ display:block; font-size:11px; color:var(--ink-soft); margin-top:2px; }
.res-limit-hits.is-hit{ color:var(--rose); }
.res-limit-value{
  font-family:var(--font-num); font-weight:800; font-variant-numeric:tabular-nums;
  color:var(--accent); white-space:nowrap;
}

/* ---- Кто тратит SMS ---- */
/* Строки — как в списке закрытых номеров: номер с именем, под ними расход, справа
   кнопка. unicode-bidi:plaintext — по той же причине: номер и имя могут быть на любом
   языке, а в ивритском интерфейсе строка течёт справа налево. */
.res-spenders{ list-style:none; margin:0 0 6px; padding:0; }
.res-spender-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:10px 0; border-top:1px solid var(--border);
}
.res-spender-row:first-child{ border-top:none; }
.res-spender-info{ min-width:0; }
.res-spender-phone{ font-size:13.5px; font-weight:600; unicode-bidi:plaintext; }
.res-spender-usage{ margin-top:2px; font-size:12px; color:var(--ink-soft); line-height:1.4; unicode-bidi:plaintext; }
/* Строка про персональный лимит — тем же цветом, что «упирались» в пределах выше:
   это не поломка, но и не фон — её ищут глазами, когда решают, кого ещё прижать. */
.res-spender-usage.is-limited{ color:var(--rose); }
/* Кнопка «Лимит» — крупнее обычной .btn-ghost, как «Открыть» в чёрном списке:
   по ней попадают пальцем на телефоне. */
.res-spender-btn{ flex:none; padding:10px 14px; border-radius:12px; }

/* Номер в окне лимита — заголовком над формой, менять его в этом окне нельзя. */
.smslim-phone{ margin:0 0 8px; font-size:14px; font-weight:700; unicode-bidi:plaintext; }

/* Шапка «Пределов»: заголовок и кнопка «Изменить» в одну строку — отдельной полосы
   на вертикальном экране кнопка не заслуживает. Отступы шапка забирает себе,
   у заголовка внутри них не остаётся. */
.res-limits-head{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin:18px 0 8px; }
.res-limits-head .link-group-title{ margin:0; }
.res-limits-head .btn-ghost{ flex:none; padding:6px 12px; border-radius:10px; }

/* ---- Сводка: плитки с цифрами ---- */
/* Две колонки на телефоне — не компромисс, а нужный минимум: шесть цифр в один столбец
   превращают сводку в длинную ленту, ради которой пришлось бы прокручивать до поиска
   клиента (то, зачем панель открывают чаще всего). auto-fit добавляет третью колонку
   сам, когда экран это позволяет. */
.stats-grid{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(140px, 1fr)); gap:8px;
}
.stat-tile{
  background:var(--surface); border:1px solid var(--border); border-radius:14px;
  padding:10px 12px; min-width:0;
}
.stat-value{
  font-family:var(--font-num); font-size:19px; font-weight:800; font-variant-numeric:tabular-nums; color:var(--accent);
  /* Длинное число не должно распирать колонку — пусть лучше переносится по месту. */
  overflow-wrap:anywhere;
}
.stat-label{ font-size:11px; color:var(--ink-soft); margin-top:1px; line-height:1.3; }
.stats-period{
  font-family:var(--font-ui); font-size:11px; letter-spacing:0.04em;
  text-transform:uppercase; color:var(--ink-soft); white-space:nowrap;
}

/* ---- Возврат к списку ---- */
/* Не иконка без подписи, как «обновить» и «выгрузить»: те стоят в заголовке раздела, где
   смысл понятен из соседства, а эта кнопка открывает экран и должна называть себя сама.
   Прижата влево и приглушена — она уводит с карточки, а не делает на ней работу. */
.back-btn{
  display:inline-flex; align-items:center; gap:6px; margin:0 0 10px;
  padding:6px 12px 6px 8px; border-radius:999px;
  border:1px solid var(--border); background:transparent; color:var(--ink-soft);
  font-family:inherit; font-size:13px; cursor:pointer;
  transition:border-color .15s ease, color .15s ease;
}
.back-btn:hover{ border-color:var(--accent); color:var(--accent); }
.back-btn svg{ width:16px; height:16px; flex:none; }

/* ---- Карточка клиента: уровень и анкетные факты ---- */
.client-card-top{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
.client-id-block{ min-width:0; }
.tier-badge{
  flex:none; font-family:var(--font-ui); font-size:11px; font-weight:600;
  letter-spacing:0.06em; text-transform:uppercase; padding:3px 9px; border-radius:999px;
  background:var(--gold-soft); color:var(--gold);
}
/* Значок и активный чип переключателя красятся металлом своего уровня — тем же, каким
   одета карта в клиентском приложении: сотрудник видит не слово, а цвет, который видит
   клиент. Металлы заданы одной переменной на уровень, полутона — прозрачностью от неё,
   поэтому в тёмной теме ничего отдельно перекрашивать не надо. Золото — токен темы
   --gold: у него в тёмной теме есть своя светлая пара, бронзе и серебру она не нужна —
   оба и так средней светлоты. */
[data-tier="bronze"]{ --tier-metal:#B0793F; }
[data-tier="silver"]{ --tier-metal:#8E9AAC; }
[data-tier="gold"]{ --tier-metal:var(--gold); }
.tier-badge[data-tier]{
  background:color-mix(in srgb, var(--tier-metal) 15%, transparent);
  color:var(--tier-metal);
}

/* ---- Переключатель уровня ----
   Подпись и три чипа одним компактным рядом под строкой баллов: операция на один тап,
   отдельной карточки-ленты ей на вертикальном экране не положено. Текущий уровень
   выключен и залит своим металлом — он и есть отметка «вы здесь». */
.tier-switch{ display:flex; align-items:center; flex-wrap:wrap; gap:6px 10px; margin-top:10px; }
.tier-switch-label{ font-size:12px; color:var(--ink-soft); }
/* На самом узком экране три чипа могут не поместиться в строку — пусть переносятся,
   а не ужимаются в нечитаемое. */
.tier-switch-btns{ display:flex; flex-wrap:wrap; gap:6px; }
.tier-switch-btn{
  font-family:var(--font-ui); font-size:11px; font-weight:600; letter-spacing:.05em;
  text-transform:uppercase; padding:4px 11px; border-radius:999px; cursor:pointer;
  border:1px solid var(--border); background:transparent; color:var(--ink-soft);
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.tier-switch-btn:hover:not(:disabled){
  border-color:var(--tier-metal); color:var(--tier-metal);
}
.tier-switch-btn.active{
  cursor:default;
  background:color-mix(in srgb, var(--tier-metal) 15%, transparent);
  border-color:color-mix(in srgb, var(--tier-metal) 55%, transparent);
  color:var(--tier-metal);
}
/* Ряд выключен на время вызова сервера (changeTier), но блёкнуть должны только
   неактивные чипы: текущий уровень выключен всегда, и серым он быть не обязан. */
.tier-switch-btn:disabled:not(.active){ opacity:.45; cursor:default; }
/* Ошибка занимает всю ширину ряда, чтобы не вклиниваться между чипами. */
.tier-switch .form-error{ flex-basis:100%; margin:0; }
/* Факты — сетка "подпись / значение", а не две колонки текста: так значения выстроены
   в один столбец и читаются сверху вниз, даже когда подписи разной длины. */
.client-facts{
  display:grid; grid-template-columns:auto 1fr; gap:2px 10px;
  margin:12px 0 0; padding:12px 0 0; border-top:1px solid var(--border);
  font-size:12.5px;
}
.client-facts:empty{ display:none; }
.client-facts dt{ color:var(--ink-soft); }
/* plaintext — в значениях теперь бывают имена клиентов, а имя может быть на любом
   языке: ивритское имя в русском интерфейсе (и наоборот) пусть течёт по своему письму. */
.client-facts dd{ margin:0; unicode-bidi:plaintext; }

/* ---- Список клиентов: правая колонка (баллы + дата) ---- */
.client-row-side{ flex:none; text-align:right; }
.client-row-since{ font-size:11px; color:var(--ink-soft); margin-top:2px; white-space:nowrap; }
.section-actions{ display:flex; align-items:center; gap:8px; flex:none; }

/* ---- Ручная корректировка: закрытый по умолчанию блок ---- */
.adjust-block{
  border:1px dashed var(--border); border-radius:16px; padding:0 16px; margin-bottom:12px;
}
.adjust-block summary{
  cursor:pointer; padding:12px 0; font-size:13.5px; font-weight:600; color:var(--ink-soft);
  list-style:none;
}
.adjust-block summary::-webkit-details-marker{ display:none; }
/* Своя стрелка вместо системного треугольника: он в Safari и Chrome выглядит по-разному
   и не поддаётся окраске под тему. */
.adjust-block summary::before{
  content:"+"; display:inline-block; width:1em; font-weight:700; color:var(--accent);
}
.adjust-block[open] summary::before{ content:"−"; }
.adjust-block[open] summary{ color:var(--accent); }
.adjust-block .form{ padding-bottom:16px; }
/* Корректировка — редкая и опасная операция, поэтому её кнопка приглушена по сравнению
   с обычными: та же форма, но без градиента и тени, чтобы взгляд за неё не цеплялся. */
.btn-adjust{ background:var(--ink-soft); box-shadow:none; }

/* ---- Удаление клиента: тот же закрытый блок, но в цвете предупреждения ---- */
/* Отличается от корректировки намеренно: рамка, заголовок и кнопка розовые, чтобы блок
   не прочитался как ещё одна безобидная настройка под тем же серым плюсиком. */
.danger-block{
  border:1px dashed color-mix(in srgb, var(--rose) 45%, transparent);
  border-radius:16px; padding:0 16px; margin-bottom:12px;
}
.danger-block summary{
  cursor:pointer; padding:12px 0; font-size:13.5px; font-weight:600; color:var(--rose);
  list-style:none;
}
.danger-block summary::-webkit-details-marker{ display:none; }
.danger-block summary::before{ content:"+"; display:inline-block; width:1em; font-weight:700; }
.danger-block[open] summary::before{ content:"−"; }
.danger-block .form{ padding-bottom:16px; }
.danger-block .hint{ margin:0; }
/* Галочка "понимаю" — единственная строка в панели, где подпись стоит справа от поля,
   поэтому направление у неё своё, а не общее для label. */
.check-row{ flex-direction:row; align-items:flex-start; gap:9px; line-height:1.4; }
.check-row input{ margin-top:2px; flex:none; width:16px; height:16px; accent-color:var(--rose); }
/* Пока галочка не стоит, кнопка выключена и приглушена: видно, что она есть, но что
   нажимать её ещё рано. */
.btn-delete{ background:var(--rose); box-shadow:none; }
.btn-delete:disabled{ background:var(--ink-soft); opacity:.45; }
/* Сообщение об удалении живёт в разделе со списком (карточка к этому моменту скрыта),
   поэтому ему нужен собственный отступ от счётчика клиентов. */
#delete-success:not(:empty){ margin:0 0 8px; }

/* ---- История операций клиента ---- */
.client-history{ margin-top:18px; }
.client-history h3{ font-family:var(--font-ui); font-size:15px; margin:0 0 8px; }
.mini-history{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }
.mini-history li{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:9px 12px;
}
.mini-history-left{ min-width:0; }
.mini-history-note{ font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mini-history-sub{ font-size:11.5px; color:var(--ink-soft); margin-top:1px; }
.mini-history-points{ flex:none; font-family:var(--font-num); font-size:14px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap; }
.mini-history-points.positive{ color:var(--success); }
.mini-history-points.negative{ color:var(--rose); }

/* ---- Переключатель языка (RU / EN / HE) — перенесён из demo-прототипа ---- */
/* Переключатель — настоящий подвал в общем потоке, а не полоса, зафиксированная
   поверх экрана. Пока он был position:fixed, он лежал НАД содержимым: под ним
   пропадали кнопки и строки, а прокрутка проносила текст прямо сквозь флаги, и
   никакой запас нижнего отступа этого до конца не убирал — отступ подгоняется под
   один размер экрана, а налезание вылезало на другом. В потоке подвал занимает
   собственную высоту: экран выше него просто становится ниже, и перекрывать друг
   друга им уже нечем.
   position:relative + z-index:1 — не про наложение, а про фон: и .ambient-bg, и
   .dash-bg лежат на z-index:0, а элемент без позиционирования рисуется ниже любого
   позиционированного даже с нулевым уровнем — без этих двух строк флаги оказались
   бы ЗА фоном и пропали. */
.lang-switch{
  flex:none; position:relative; z-index:1;
  display:flex; justify-content:center;
  width:100%; min-width:var(--app-min-w); margin:0 auto;
  padding:10px 12px calc(env(safe-area-inset-bottom) + 10px);
}
/* Обёртка нужна ради position:relative: всплывающий список позиционируется от самой
   кнопки, а не от всей полосы подвала — иначе он растянулся бы по её ширине. */
.lang-picker{ position:relative; }
/* Постоянно видна только эта кнопка: флаг текущего языка + код. Флаг в неё копирует
   i18n.js из активного пункта списка, поэтому разметка флага живёт в одном месте. */
.lang-current{
  display:flex; align-items:center; gap:7px;
  background:none; border:1.5px solid transparent; border-radius:999px;
  padding:7px 11px; cursor:pointer; color:var(--ink-soft);
  font-family:inherit; font-size:12px; font-weight:600; letter-spacing:.06em; line-height:1;
  opacity:.7; transition:opacity .15s ease, border-color .15s ease, background-color .15s ease;
}
.lang-current:hover{ opacity:1; }
.lang-current[aria-expanded="true"]{ opacity:1; border-color:var(--border); background:var(--surface); }
.lang-current-flag{ display:block; }
/* Галочка смотрит вверх — список открывается вверх, — и переворачивается, когда он открыт. */
.lang-caret{ width:11px; height:11px; transition:transform .18s ease; }
.lang-current[aria-expanded="true"] .lang-caret{ transform:rotate(180deg); }
.lang-menu{
  position:absolute; bottom:calc(100% + 8px); left:50%; transform:translateX(-50%);
  display:flex; flex-direction:column; gap:2px;
  min-width:170px; padding:6px;
  background:var(--surface); border:1px solid var(--border); border-radius:14px;
  box-shadow:0 20px 44px -20px var(--shadow-color), 0 2px 8px -4px var(--shadow-color);
  animation:langMenuIn .16s ease-out both;
}
/* display:flex выше сильнее встроенного [hidden]{display:none}, поэтому скрытие
   приходится задавать явно — без этой строки список висел бы открытым всегда. */
.lang-menu[hidden]{ display:none; }
@keyframes langMenuIn{
  from{ opacity:0; transform:translate(-50%, 6px); }
  to{ opacity:1; transform:translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce){ .lang-menu{ animation:none; } }
.lang-flag{
  display:flex; align-items:center; gap:10px; width:100%;
  background:none; border:0; border-radius:10px; padding:8px 10px;
  cursor:pointer; color:var(--ink); text-align:left;
  font-family:inherit; font-size:14px; line-height:1;
  transition:background-color .15s ease;
}
.lang-flag:hover{ background:var(--bg-2); }
.lang-flag.active{ background:var(--bg-2); font-weight:600; }
/* Флаги — инлайновые SVG, а не эмодзи: эмодзи-флаги не на всех телефонах есть в системном
   шрифте (особенно Android) и рисуются текстом ("RU"/"GB"/"IL") вместо картинки флага. */
.lang-flag-img{ display:block; flex:none; }
.lang-flag-img svg, .lang-current-flag svg{
  display:block; width:22px; height:16px; border-radius:3px; box-shadow:0 0 0 1px rgba(0,0,0,0.12);
}

/* Иврит: меняем только смысл (перевод) и выравнивание текста на правое — порядок
   элементов и раскладка не зеркалятся. Более специфичные правила (например
   .login-card .brand с его text-align:center) продолжают действовать поверх этого. */
[lang="he"] .view{ text-align:right; }

/* Порядок слов в смешанных строках ("קבלת קוד ב-SMS", "סכום הרכישה (₪)"). Без явного
   направления браузер считает такую строку левосторонней и ставит латиницу/цифры справа —
   а справа в иврите читают ПЕРВЫМ, и "SMS" выглядит переехавшим в начало фразы.
   unicode-bidi:plaintext определяет направление каждой строки по её же первой букве: иврит
   становится правосторонним и SMS уходит в конец, а номера телефонов, "+50" и "(₪)"
   внутри него остаются левосторонними. Берём именно unicode-bidi, а не dir="rtl"/direction:
   он меняет только порядок текста внутри строки и не зеркалит flex-раскладку (см. выше).
   Перечислены только блочные контейнеры: на строчном (span, a) plaintext изолирует
   кусок от соседей, и родитель снова стал бы левосторонним. */
[lang="he"] .view p,
[lang="he"] .view div,
[lang="he"] .view h2,
[lang="he"] .view li,
[lang="he"] .view label > span,
[lang="he"] .view button{ unicode-bidi:plaintext; }

/* Названия и пояснения в карточках-ссылках — тоже смешанные строки ("צריכה ומגבלות
   ב-Firebase"), но лежат они в span'ах. Общее правило выше их не ловит: там перечислены
   блочные теги. Эти два — элементы flex-контейнера, то есть ведут себя как блоки, и
   plaintext на них работает так же, ничего не изолируя от соседей. */
[lang="he"] .link-title,
[lang="he"] .link-desc,
[lang="he"] .link-group-title{ unicode-bidi:plaintext; }

/* Те же смешанные строки в span'ах-элементах flex-контейнеров: журнал входов
   ("28.08, 21:40 · כניסות: 3", "IP לא ידוע"), лимиты SMS ("{n} שנ׳", "נעצרו {n}…")
   и журнал команды ("{email} — תפקיד: …"). Каждый из них — flex-элемент или
   display:block, то есть сам себе абзац, и plaintext здесь ничего не изолирует
   от соседей по предложению. */
[lang="he"] .login-ip,
[lang="he"] .login-group-sub,
[lang="he"] .res-limit-text,
[lang="he"] .res-limit-hits,
[lang="he"] .res-limit-value,
[lang="he"] .team-log-what,
[lang="he"] .team-log-meta{ unicode-bidi:plaintext; }

/* Строки, начинающиеся с номера телефона («+9725… · שם»). plaintext по ивритскому
   имени сделал бы абзац правосторонним, и «+» отрывается от номера — уезжает на его
   правую сторону. Телефон задаёт строке направление: слева направо, «+» на месте;
   выравнивание при этом остаётся правым, как у соседних строк. */
[lang="he"] .view .res-spender-phone,
[lang="he"] .blocklist-row-phone,
[lang="he"] .smslim-phone{ direction:ltr; unicode-bidi:isolate; text-align:right; }

/* ---- Предложение поставить панель на домашний экран ----
   Карточка стоит последней во вкладке «Сервисы»: это настройка рабочего места, которую
   делают один раз, и в повседневных «Клиентах» она только мешала бы. Показывает и прячет
   её install.js; в уже установленной панели её не бывает. */
.install-card{
  display:flex; flex-wrap:wrap; align-items:center; gap:14px;
  padding:16px; border-radius:16px;
  background:var(--surface); border:1px solid var(--border);
  box-shadow:0 10px 24px -18px var(--shadow-color);
}
.install-icon{
  width:48px; height:48px; flex:none; border-radius:14px; object-fit:cover;
  box-shadow:0 6px 14px -8px var(--shadow-color);
}
/* min-width заставляет текст первым переноситься на свою строку на узком экране:
   без него длинное описание ужимает картинку в вертикальную полоску. */
.install-text{ flex:1 1 150px; min-width:150px; }
.install-card h2{ margin:0 0 4px; font-size:15px; }
.install-card .hint{ margin:0; font-size:12.5px; line-height:1.45; }
.install-btn{ flex:1 0 100%; margin-top:0; }
/* Пока телефон ставит панель, кнопка перестаёт быть приглашением и становится состоянием:
   кружок ожидания и надпись «Устанавливается…». Это единственный след установки на экране,
   когда окно ожидания свёрнуто; нажатие возвращает его обратно (install.js). */
.install-btn.is-busy, .install-btn.is-done{
  display:flex; align-items:center; justify-content:center; gap:9px;
}
.install-btn.is-busy::before{
  content:""; width:15px; height:15px; flex:none; border-radius:50%;
  /* currentColor, а не #fff: в тёмной теме надпись на кнопке тёмная (см. блок про
     тёмные чернила у .btn-primary), и кольцо должно темнеть вместе с ней. */
  border:2px solid color-mix(in srgb, currentColor 35%, transparent); border-top-color:currentColor;
  animation:installSpin .9s linear infinite;
}
.install-btn.is-done::before{ content:"✓"; font-size:15px; line-height:1; }
@media (prefers-reduced-motion: reduce){
  .install-btn.is-busy::before{ animation:installPulse 1.4s ease-in-out infinite; }
}

/* ---- Уведомления о новых клиентах ----
   Оформлена как та же карточка, что и предложение установить панель, и стоит сразу под
   ней: обе — разовые настройки этого телефона, а не программы лояльности. Заголовок и
   состояние в одну строку (.section-head), чтобы «включены/выключены» читалось сразу,
   без чтения кнопки. */
.notify-card{
  padding:16px; border-radius:16px;
  background:var(--surface); border:1px solid var(--border);
  box-shadow:0 10px 24px -18px var(--shadow-color);
}
.notify-card h2{ margin:0; font-size:15px; }
.notify-card .hint{ margin:0 0 12px; font-size:12.5px; line-height:1.45; }
/* Метка состояния — тем же шрифтом и в том же ритме, что «за 30 дней» в сводке: это
   такая же подпись к заголовку, а не отдельная кнопка. Цветом выделено только
   «включены»: выключенное состояние не должно бросаться в глаза. */
.notify-state{
  font-family:var(--font-ui); font-size:11px; letter-spacing:0.04em;
  text-transform:uppercase; color:var(--ink-soft); white-space:nowrap;
}
.notify-state.is-on{ color:var(--success); }
/* Тревожное состояние той же метки: ночной копии нет вторые сутки. Розовым — тем же
   цветом, что и «новое устройство» в журнале входов: в панели это единственный цвет,
   означающий «посмотрите сюда сейчас». */
.notify-state.is-warn{ color:var(--rose); }
.notify-btn{ margin-top:0; width:100%; }
/* Пояснение под кнопкой (почему кнопки нет, что сделать на iPhone, чего не хватает в
   настройках проекта). Отделено линией: это не часть описания сверху, а ответ на
   вопрос «почему не работает». */
.notify-note{ margin:12px 0 0 !important; padding-top:10px; border-top:1px solid var(--border); }
.notify-note:empty{ display:none; }
#notify-error:empty,
#passkey-error:empty{ display:none; }
#notify-error,
#passkey-error{ margin-top:10px; }
/* На иврите пояснения перемешаны с латиницей (Firebase console, admin-app/firebase-config.js):
   plaintext не даёт браузеру перевернуть такие куски задом наперёд. */
[lang="he"] .notify-note,
[lang="he"] .notify-card .hint{ unicode-bidi:plaintext; }


/* Окно с ручной инструкцией — короткое, поэтому карточка по центру, а не экран во всю
   высоту: три шага незачем разворачивать на весь телефон. */
.install-modal{
  position:fixed; inset:0; margin:auto; padding:0; border:none;
  /* height:fit-content, а не auto: заданы разом top:0 и bottom:0 (inset:0 нужен, чтобы
     margin:auto поставил окно по центру), и при height:auto браузер растянул бы его на
     всю доступную высоту. */
  width:min(420px, calc(100% - 32px)); max-width:none;
  height:-webkit-fit-content; height:fit-content; max-height:min(80vh, 620px);
  border-radius:22px; overflow:hidden;
  background:var(--surface); color:var(--ink);
  box-shadow:0 30px 60px -20px var(--shadow-color), 0 2px 10px -2px var(--shadow-color);
  flex-direction:column;
}
.install-modal:not([open]){ display:none; }
.install-modal[open]{ display:flex; animation:installIn .28s cubic-bezier(.2,.9,.25,1) both; }
.install-modal::backdrop{ background:rgba(24,14,28,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
@keyframes installIn{ from{ opacity:0; transform:translateY(16px); } to{ opacity:1; transform:translateY(0); } }
@media (prefers-reduced-motion: reduce){ .install-modal[open]{ animation:none; } }
.install-modal-head{
  flex:none; display:flex; align-items:flex-start; gap:12px;
  padding:18px 20px 12px; border-bottom:1px solid var(--border);
}
.install-modal-head h2{
  flex:1; min-width:0; margin:0;
  font-family:var(--font-ui); font-size:17px; font-weight:600; line-height:1.25;
}
.install-close{
  flex:none; width:34px; height:34px; border:none; border-radius:50%; cursor:pointer;
  background:var(--bg-2); color:var(--ink); font-size:22px; line-height:1;
  display:flex; align-items:center; justify-content:center;
  transition:filter .15s ease, transform .15s ease;
}
.install-close:hover{ filter:brightness(.95); }
.install-close:active{ transform:scale(.92); }
.install-modal-body{
  flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch; padding:16px 20px 18px; font-size:14px; line-height:1.55;
}
.install-steps{ margin:0; padding-inline-start:22px; display:flex; flex-direction:column; gap:9px; }
.install-steps li{ padding-inline-start:2px; }
.install-steps li::marker{ color:var(--accent); font-weight:700; }
.install-modal-note{
  margin:16px 0 0; padding-top:12px; border-top:1px solid var(--border);
  font-size:12.5px; line-height:1.5; color:var(--ink-soft);
}
/* «У меня оно уже установлено» — последняя строка окна, для iPhone и всех браузеров,
   которые сами ответить на этот вопрос не умеют (см. install.js). Ссылка, а не кнопка:
   действие редкое и не должно спорить с «Понятно» внизу окна. */
.install-done{ display:block; margin:18px auto 0; font-size:13px; color:var(--ink-soft); }
/* Ожидание установки. Chrome отвечает «принято» сразу, а приложение появляется на
   телефоне заметно позже — он заказывает у Google сборку пакета. Это окно занимает
   паузу: кружок, пока идёт, и галочка, когда готово (install.js). */
.install-progress{
  display:flex; flex-direction:column; align-items:center; gap:14px;
  padding:14px 0 6px; text-align:center;
}
.install-spinner{
  width:44px; height:44px; flex:none; border-radius:50%;
  border:3px solid var(--border); border-top-color:var(--accent);
  animation:installSpin .9s linear infinite;
}
@keyframes installSpin{ to{ transform:rotate(360deg); } }
/* Движение — единственное, что говорит «идёт»; тем, кто его отключил, вместо вращения
   остаётся мигание, иначе кружок выглядел бы застрявшим. */
@media (prefers-reduced-motion: reduce){
  .install-spinner{ animation:installPulse 1.4s ease-in-out infinite; }
  @keyframes installPulse{ 0%,100%{ opacity:1; } 50%{ opacity:.45; } }
}
.install-check{
  width:44px; height:44px; flex:none; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:var(--accent); color:#fff;
  animation:installCheckIn .3s cubic-bezier(.2,.9,.25,1) both;
}
.install-check svg{ width:24px; height:24px; }
@keyframes installCheckIn{ from{ transform:scale(.6); opacity:0; } to{ transform:scale(1); opacity:1; } }
@media (prefers-reduced-motion: reduce){ .install-check{ animation:none; } }
.install-progress-text{ margin:0; max-width:32ch; font-size:14px; line-height:1.55; color:var(--ink-soft); }
/* Полоса и счётчик секунд под текстом. Долю установки браузер не сообщает никак, поэтому
   полоса идёт по времени и до конца сама не доходит (install.js): её дело — показать, что
   ожидание живое. Точную цифру говорит счётчик под ней, и он не врёт. */
.install-bar{
  width:min(240px, 100%); height:6px; flex:none;
  border-radius:999px; overflow:hidden; background:var(--border);
}
.install-bar > i{
  display:block; width:0; height:100%; border-radius:999px;
  background:var(--accent); transition:width .9s linear;
}
/* Последний шаг до ста процентов — быстрый: он отмечает готовность, а не продолжает ход. */
.install-progress.is-done .install-bar > i{ transition:width .35s ease-out; }
.install-progress-meta{
  margin:0; font-size:12.5px; line-height:1.4; color:var(--ink-soft);
  font-variant-numeric:tabular-nums;
}

/* Кнопок в подвале бывает две: «Открыть в Chrome» (только там, где браузер сам
   устанавливать не умеет) и «Понятно». Обе делят ширину поровну. */

.install-modal-foot{
  flex:none; display:flex; gap:10px;
  padding:12px 20px calc(env(safe-area-inset-bottom) + 14px);
  border-top:1px solid var(--border);
}
.install-modal-foot .btn-primary,
.install-modal-foot .btn-ghost{ flex:1; margin-top:0; }
/* .btn-ghost в админке мельче основной кнопки — рядом в подвале их выравниваем по ней. */
.install-modal-foot .btn-ghost{ padding:12px; font-size:14.5px; border-radius:13px; }

/* ---- Окно подтверждения (сейчас — только выход) ----
   Маленькое окно по центру: один вопрос и две кнопки, разворачивать его во весь экран
   незачем. height:fit-content по той же причине, что и у окна установки: при inset:0
   и height:auto браузер растянул бы окно на всю доступную высоту. */
.confirm-modal{
  position:fixed; inset:0; margin:auto; padding:0; border:none;
  width:min(360px, calc(100% - 40px)); max-width:none;
  height:-webkit-fit-content; height:fit-content;
  border-radius:22px; overflow:hidden;
  background:var(--surface); color:var(--ink);
  box-shadow:0 30px 60px -20px var(--shadow-color), 0 2px 10px -2px var(--shadow-color);
  flex-direction:column;
}
.confirm-modal:not([open]){ display:none; }
.confirm-modal[open]{ display:flex; animation:confirmIn .2s cubic-bezier(.2,.9,.25,1) both; }
.confirm-modal::backdrop{ background:rgba(24,14,28,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
@keyframes confirmIn{ from{ opacity:0; transform:scale(.94); } to{ opacity:1; transform:scale(1); } }
@media (prefers-reduced-motion: reduce){ .confirm-modal[open]{ animation:none; } }
.confirm-body{ padding:22px 22px 6px; text-align:center; }
.confirm-body h2{
  margin:0; font-family:var(--font-ui); font-size:18px; font-weight:600; line-height:1.3;
}
.confirm-text{ margin:8px 0 0; font-size:13.5px; line-height:1.5; color:var(--ink-soft); }
.confirm-lines{
  list-style:none; margin:12px 0 0; padding:10px 12px; text-align:start;
  border-radius:12px; background:color-mix(in srgb, var(--ink-soft) 8%, transparent);
  font-size:13.5px; line-height:1.6; color:var(--ink);
}
.confirm-lines li{ unicode-bidi:plaintext; }
.confirm-lines li + li{ margin-top:4px; }
/* Кнопки в столбец, а не в строку: «Отмена» и «Выйти» рядом на узком экране жмутся
   слишком тесно, а цена промаха — как раз тот выход, от которого мы и защищаемся. */
.confirm-foot{
  display:flex; flex-direction:column-reverse; gap:8px;
  padding:16px 22px calc(env(safe-area-inset-bottom) + 18px);
}
.confirm-foot .btn-primary{ width:100%; margin-top:0; }
.confirm-foot .btn-ghost{ width:100%; padding:10px; font-size:14px; border-radius:12px; }
[lang="he"] .confirm-modal{ direction:rtl; }
[lang="he"] .confirm-text{ unicode-bidi:plaintext; }

/* Иврит: инструкция — связный текст с нумерованным списком, цифры шагов должны стоять
   справа, поэтому здесь раскладку зеркалим по-настоящему. */
[lang="he"] .install-modal{ direction:rtl; text-align:right; }
[lang="he"] .install-steps li,
[lang="he"] .install-modal-note{ unicode-bidi:plaintext; }

/* ---- Чёрный список номеров ---- */
/* Значок в шапке карточки клиента. В обычном состоянии он такой же тихий, как значки
   выгрузки и обновления рядом со списком; на закрытом номере — красный и с рамкой,
   потому что это уже не кнопка, а состояние карточки, и видно его должно быть сразу. */
.block-btn{ flex:none; }
.block-btn.is-blocked{ color:var(--rose); border-color:var(--rose); }

/* Строка «номер закрыт» под фактами карточки: причина и дата. Стоит выше формы визита,
   потому что отвечает на вопрос раньше, чем сотрудник начнёт вбивать чек. */
.blocked-note{
  margin:10px 0 0; padding:8px 10px; border-radius:12px;
  background:color-mix(in srgb, var(--rose) 12%, transparent);
  color:var(--rose); font-size:12.5px; line-height:1.45;
}
.blocked-note.hidden{ display:none; }

/* Отметка в строке общего списка: карточку с закрытым номером видно, не открывая её. */
.client-row.is-blocked{ opacity:.78; }
.client-row-blocked{
  margin-top:3px; font-size:11px; font-weight:600; letter-spacing:.02em;
  text-transform:uppercase; color:var(--rose);
}

/* Окно панели: список закрытых номеров и приглашение в клуб. Шире окна подтверждения —
   внутри форма и строки, — и с внутренней прокруткой: содержимого может накопиться
   больше, чем помещается на экране, а расти вниз окну некуда. */
.panel-modal{
  position:fixed; inset:0; margin:auto; padding:0; border:none;
  width:min(420px, calc(100% - 32px)); max-width:none;
  max-height:min(86vh, 720px);
  border-radius:22px; overflow:hidden;
  background:var(--surface); color:var(--ink);
  box-shadow:0 30px 60px -20px var(--shadow-color), 0 2px 10px -2px var(--shadow-color);
  flex-direction:column;
}
.panel-modal:not([open]){ display:none; }
.panel-modal[open]{ display:flex; animation:confirmIn .2s cubic-bezier(.2,.9,.25,1) both; }
.panel-modal::backdrop{ background:rgba(24,14,28,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
@media (prefers-reduced-motion: reduce){ .panel-modal[open]{ animation:none; } }
.panel-modal-head{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:18px 20px 8px;
}
.panel-modal-head h2{ margin:0; font-family:var(--font-ui); font-size:17px; font-weight:600; }
.panel-modal-head .icon-btn{ flex:none; }
.panel-modal-body{
  padding:0 20px calc(env(safe-area-inset-bottom) + 18px);
  overflow-y:auto; -webkit-overflow-scrolling:touch;
}
/* Кнопка «Закрыть номер» — красная, как «Удалить навсегда»: обе про отказ человеку,
   и путать их с зелёным «Завершить» не нужно. */
.btn-block-submit{ background:var(--rose); box-shadow:none; }
.blocklist-list{ list-style:none; margin:14px 0 0; padding:0; }
.blocklist-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:10px 0; border-top:1px solid var(--border);
}
.blocklist-row-info{ min-width:0; }
/* unicode-bidi:plaintext — номер и причина могут быть на любом языке, а в ивритском
   интерфейсе строка ещё и течёт справа налево: пусть каждая ведёт себя по своему тексту. */
.blocklist-row-phone{ font-size:13.5px; font-weight:600; unicode-bidi:plaintext; }
.blocklist-row-reason{ margin-top:2px; font-size:12px; color:var(--ink-soft); line-height:1.4; unicode-bidi:plaintext; }
/* Кнопка «Открыть» в строке — крупнее обычной .btn-ghost: по ней попадают пальцем на
   телефоне, а промах здесь возвращает человека в клиенты. */
.blocklist-unblock{ flex:none; padding:10px 14px; border-radius:12px; }
[lang="he"] .panel-modal{ direction:rtl; }

/* ---- Окно «Пригласить в клуб» ---- */
/* Ссылка на виду и целиком: перенос по любому знаку, потому что переносить в ней нечего —
   это одно длинное слово с адресом, именем и кодом. Моноширинный шрифт не берём: строку
   не читают по буквам, её просматривают глазом и отправляют кнопкой. */
.invite-link{
  margin:12px 0 0; padding:10px 12px;
  border:1px solid var(--border); border-radius:12px;
  background:var(--bg);
  font-size:12px; line-height:1.45; color:var(--ink-soft);
  word-break:break-all; overflow-wrap:anywhere;
  /* Адрес всегда слева направо, даже в ивритском интерфейсе. */
  direction:ltr; text-align:left; unicode-bidi:plaintext;
}
/* Три кнопки в ряд, но на узком экране «Отправить» занимает всю ширину: это главное
   действие окна, и делить его строку с остальными незачем. */
.invite-actions{
  display:flex; flex-wrap:wrap; gap:8px; margin-top:14px;
}
.invite-actions button{ flex:1 1 auto; margin:0; }
/* Селектор с родителем не для красоты: строкой выше у кнопок задан flex:1 1 auto, и
   короткое .invite-send проиграло бы ему по весу — «Отправить» встала бы в общий ряд. */
.invite-actions .invite-send{ flex:1 1 100%; }
/* WhatsApp — зелёной кнопкой панели (.btn-secondary), а не фирменным зелёным самого
   WhatsApp: чужой цвет в сливовой панели читался бы как реклама, а не как кнопка. */

/* Язык приглашения: подпись и три чипа одним рядом, по образцу переключателя уровней
   (.tier-switch). Капса и мелкого кегля чипов не берём: подписи «עברית / Русский /
   English» — слова живых языков, и читаться должны как слова. */
.invite-lang-row{ display:flex; align-items:center; flex-wrap:wrap; gap:6px 10px; margin-top:12px; }
.invite-lang-name{ font-size:12px; color:var(--ink-soft); }
.invite-lang{ display:flex; flex-wrap:wrap; gap:6px; }
.invite-lang-btn{
  font-family:var(--font-ui); font-size:13px; font-weight:600;
  padding:5px 12px; border-radius:999px; cursor:pointer;
  border:1px solid var(--border); background:transparent; color:var(--ink-soft);
  /* «עברית» в русском ряду и «Русский» в ивритском — пусть каждая подпись течёт
     по своему письму. */
  unicode-bidi:plaintext;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.invite-lang-btn:hover{ border-color:var(--accent); color:var(--accent); }
.invite-lang-btn.active{
  cursor:default;
  background:var(--accent-soft);
  border-color:var(--accent);
  color:var(--accent);
}

/* ---- Большой экран (планшет/компьютер) ---- */
/* Только здесь панель снова становится карточкой по центру страницы. Условие по высоте
   так же важно, как по ширине: телефон, повёрнутый набок, бывает шире 768px, но остаётся
   телефоном — низкий экран отсекает этот случай. */
@media (min-width:768px) and (min-height:640px){
  body{ display:block; }
  .view{ max-width:460px; margin:0 auto; padding:32px 20px 32px; }
  /* Парно к padding .view выше: граница обрезки разделов должна совпадать с краями
     карточки, иначе уезжающий раздел обрывался бы на 20px раньше её края. */
  .tab-panes{ --gutter:20px; }
  #view-login{ display:flex; align-items:center; padding:32px 20px 32px; }
  .card{
    border:1px solid rgba(255,255,255,0.5); border-radius:22px;
    box-shadow:0 20px 44px -20px var(--shadow-color);
    animation:cardIn .45s cubic-bezier(.2,.9,.25,1.1) both;
  }
  .login-card{ flex:none; padding:26px; }
  @media (prefers-color-scheme: dark){ .card{ border-color:rgba(255,255,255,0.08); } }
}
@media (prefers-reduced-motion: reduce){ .card{ animation:none; } }

/* ---- Вкладка «Команда» (только владелец) ---- */
/* Список строк — по образцу журнала входов: карточка на строку, всё в один-два ряда,
   без таблиц — на вертикальном экране таблице некуда расти вширь. */
.team-list{ list-style:none; margin:10px 0 0; padding:0; display:flex; flex-direction:column; gap:8px; }
.team-row{
  background:var(--surface); border:1px solid var(--border); border-radius:14px;
  padding:10px 12px; display:flex; flex-direction:column; gap:8px;
}
.team-row.is-self{ border-color:var(--accent); }
.team-line{ display:flex; align-items:center; gap:8px; min-width:0; }
.team-email{
  font-weight:600; font-size:14px; color:var(--ink);
  overflow-wrap:anywhere; min-width:0;
}
.team-you{
  flex:none; font-size:11.5px; font-weight:600; color:var(--accent);
  background:var(--accent-soft); border-radius:999px; padding:2px 8px;
}
.team-controls{ display:flex; align-items:center; gap:8px; }
/* Селектор роли наследует вид полей ввода: у select своего вида в панели раньше не было. */
.team-role-select, #team-role{
  border:1px solid var(--border); border-radius:12px; padding:9px 11px; font-size:14px;
  background:var(--bg); color:var(--ink); font-family:inherit;
}
.team-role-select{ flex:1 1 auto; min-width:0; }
.team-role-select:focus, #team-role:focus{
  outline:none; border-color:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent);
}
.team-role-badge{
  font-size:13px; font-weight:600; color:var(--ink-soft);
  border:1px solid var(--border); border-radius:999px; padding:6px 12px;
}
/* «Убрать» — нарочно блеклая: рядом с селектором ролей яркая кнопка увольнения
   напрашивалась бы на случайное нажатие. */
.team-remove{ flex:none; font-size:13px; padding:8px 12px; color:var(--rose); }
.team-norole{ margin:0; color:var(--rose); }

/* Журнал перемен — строки попроще, без карточек: это лента, а не пульт. */
.team-log{ list-style:none; margin:10px 0 0; padding:0; display:flex; flex-direction:column; gap:10px; }
.team-log-row{ display:flex; flex-direction:column; gap:2px; }
.team-log-what{ font-size:13.5px; color:var(--ink); overflow-wrap:anywhere; }
.team-log-meta{ font-size:12px; color:var(--ink-soft); }
