:root {
  /* Значения по умолчанию; Telegram переопределит их своими темами. */
  --bg: var(--tg-theme-bg-color, #ffffff);
  --text: var(--tg-theme-text-color, #111111);
  --hint: var(--tg-theme-hint-color, #888888);
  --btn: var(--tg-theme-button-color, #2ea6ff);
  --btn-text: var(--tg-theme-button-text-color, #ffffff);
  --secondary-bg: var(--tg-theme-secondary-bg-color, #f2f2f7);

  /* Зелёный «план выполнен» для кольца и командной полосы. Живёт отдельно от
     цветов Telegram: смысл у него постоянный, а не «как в теме клиента». */
  --ok-fg: #1b7a1b;

  --brand-blue: #0820FE;
}

/* Веб-версия: в браузере нет --tg-theme-*, поэтому те же токены получают
   значения бренда. Единственный акцент — фирменный синий. Атрибут ставит
   platform.init() в public/platform.js. */
:root[data-platform="web"] {
  --bg: #ffffff;
  --text: #111111;
  --hint: #6b6b6b;
  --btn: var(--brand-blue);
  --btn-text: #ffffff;
  --secondary-bg: #f2f3f7;
}
@media (prefers-color-scheme: dark) {
  :root[data-platform="web"] {
    --bg: #121212;
    --text: #f2f2f2;
    --hint: #9a9a9a;
    --secondary-bg: #1e1e22;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  padding: 16px;
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
}

h1 { font-size: 20px; margin: 0 0 4px; }
h2 { font-size: 16px; margin: 20px 0 8px; }
.hint { color: var(--hint); font-size: 14px; margin: 0 0 16px; }

.card {
  background: var(--secondary-bg);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 16px;
}

label { display: block; font-size: 14px; margin-bottom: 6px; color: var(--hint); }

input, select {
  width: 100%;
  padding: 12px;
  font-size: 16px;
  border: 1px solid var(--hint);
  border-radius: 10px;
  background: var(--bg);
  color: var(--text);
  margin-bottom: 12px;
}

button {
  width: 100%;
  padding: 14px;
  font-size: 16px;
  font-weight: 600;
  border: none;
  border-radius: 10px;
  background: var(--btn);
  color: var(--btn-text);
  cursor: pointer;
  margin-bottom: 10px;
}
button.secondary { background: var(--secondary-bg); color: var(--text); border: 1px solid var(--hint); }
button:disabled { opacity: 0.5; }

.role-buttons { display: flex; gap: 10px; }
.role-buttons button { margin-bottom: 0; }
.role-buttons button.active { outline: 3px solid var(--btn); }

.result {
  /* Результат скана идёт двумя строками: решение и пояснение под ним. */
  white-space: pre-line;
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 16px;
  font-size: 16px;
  font-weight: 600;
  text-align: center;
}
.result.ok    { background: #e3f7e3; color: #1b7a1b; }
.result.warn  { background: #fff4e0; color: #a15c00; }
.result.deny  { background: #fde3e3; color: #a11b1b; }
.result.info  { background: #e3eefd; color: #1b4fa1; }

/* Служебный класс, поэтому !important: ниже по файлу есть правила (.app,
   .toggle-row), которые задают свой display, и при равной специфичности
   побеждало последнее — скрытые экраны показывались поверх рабочего. */
.hidden { display: none !important; }

.center { text-align: center; padding: 40px 16px; }

/* ---- Таблицы админки ---- */
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 8px; border-bottom: 1px solid var(--hint); text-align: left; }
th { color: var(--hint); font-weight: 600; }
.table-wrap { overflow-x: auto; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; }
.stat { background: var(--secondary-bg); border-radius: 10px; padding: 12px; }
.stat .num { font-size: 24px; font-weight: 700; }
.stat .lbl { font-size: 13px; color: var(--hint); }
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; }
.row > div { flex: 1; min-width: 120px; }
.small { font-size: 13px; color: var(--hint); }
.inline-btn { width: auto; padding: 6px 10px; font-size: 13px; margin: 0; }

/* Сворачиваемый заголовок (напр. журнал действий). */
.collapsible { cursor: pointer; user-select: none; }
.collapsible .caret { display: inline-block; width: 1em; font-size: 12px; color: var(--hint); }

/* Шаги процесса: сначала мероприятие, потом люди под него */
.step {
  font-size: 13px;
  color: var(--hint);
  margin: 20px 0 -4px;
}

/* Поля формы с подписями над ними */
.field { margin-bottom: 12px; }
.field-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
}
@media (max-width: 480px) {
  .field-pair { grid-template-columns: minmax(0, 1fr); }
}

/* Плашка «этот человек уже в базе» */
.notice {
  background: rgba(255, 170, 0, 0.14);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 14px;
  line-height: 1.4;
  margin-bottom: 12px;
}

/* Строка мероприятия */
.ev-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.ev-name { flex: 1; font-weight: 500; }

.badge {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 6px;
  background: rgba(128, 128, 128, 0.2);
  color: var(--hint);
  white-space: nowrap;
}
.badge.ok { background: rgba(52, 199, 89, 0.18); color: #1d7a37; }
.badge.warn { background: rgba(255, 59, 48, 0.15); color: #b3261e; }

/* Панель состава мероприятия */
.staff-panel {
  padding: 4px 0 12px 0;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.staff-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
}
.staff-name { flex: 1; }
.staff-name .small { display: block; }

/* Список набора: те же строки, что в составе, но кликается вся строка целиком
   — попасть пальцем в квадратик 13×13 на телефоне нереально. */
.pick-item {
  cursor: pointer;
  border-bottom: 1px solid rgba(128, 128, 128, 0.12);
}
.pick-item:last-child { border-bottom: none; }
.pick-item input[type="checkbox"] {
  width: 22px;
  height: 22px;
  flex: none;
  margin: 0;
}
.pick-item input[type="checkbox"]:disabled { opacity: 0.5; }

button.primary {
  background: var(--btn);
  color: var(--btn-text);
  border: none;
}

/* Метка назначения правится прямо в составе мероприятия. */
.label-input {
  width: 100%;
  padding: 6px 8px;
  margin: 0 0 2px;
  /* 16px обязательны: при меньшем размере iOS зумит страницу на фокусе поля,
     и админка «прыгает» под рукой прямо на площадке. */
  font-size: 16px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
}
.label-input:focus { border-color: var(--btn); background: var(--bg); }

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

.app {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  max-width: 640px;
  margin: 0 auto;
  /* Телефоны с вырезом: содержимое не должно уезжать под системные зоны. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.app-header {
  padding: 12px 16px;
  border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.app-event { font-size: 18px; font-weight: 700; line-height: 1.2; }
.app-label { font-size: 13px; color: var(--hint); margin-top: 2px; }
.app-header select { margin: 10px 0 0; }

.app-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.app-actions {
  padding: 12px 16px 16px;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
  background: var(--bg);
}

/* Главная кнопка смены: крупная мишень, промахнуться на ходу трудно. */
.btn-scan {
  min-height: 64px;
  font-size: 19px;
  margin-bottom: 6px;
}

.link-btn {
  width: 100%;
  background: none;
  border: none;
  color: var(--hint);
  font-size: 14px;
  font-weight: 400;
  padding: 10px;
  margin: 0;
  min-height: 44px;
}

/* Результат скана — главное, что промоутер читает за смену. Крупно, цветом,
   с расстояния вытянутой руки. */
.app-body .result {
  padding: 28px 16px;
  font-size: 22px;
  line-height: 1.3;
}

.manual { margin-top: 16px; }

/* Тёмная тема.
   Цвета результата и плашек заданы фиксированно (они несут смысл — «выдать» /
   «отказ» — и не должны зависеть от палитры клиента). На тёмном фоне светлые
   пастельные заливки выбивались бы ярким пятном, поэтому здесь заливка
   становится полупрозрачной, а текст — светлым. Telegram в тёмной теме
   выставляет prefers-color-scheme, поэтому отдельного переключателя не нужно. */
@media (prefers-color-scheme: dark) {
  :root { --ok-fg: #6ee7a0; }

  .result.ok   { background: rgba(52, 199, 89, 0.22);  color: #6ee7a0; }
  .result.warn { background: rgba(255, 159, 10, 0.22); color: #fbbf24; }
  .result.deny { background: rgba(255, 69, 58, 0.22);  color: #fb8a8a; }
  .result.info { background: rgba(10, 132, 255, 0.22); color: #7dd3fc; }

  .badge.ok   { background: rgba(52, 199, 89, 0.22); color: #6ee7a0; }
  .badge.warn { background: rgba(255, 69, 58, 0.22); color: #fb8a8a; }

  .notice { background: rgba(255, 170, 0, 0.22); }
}

/* =====================================================================
   Админка на телефоне
   ===================================================================== */

/* Нижняя панель вкладок. Прижата к краю и учитывает системный индикатор. */
.tabbar {
  display: flex;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
  background: var(--bg);
}
.tabbar .tab {
  flex: 1;
  min-height: 52px;
  margin: 0;
  padding: 8px 4px;
  font-size: 13px;
  font-weight: 500;
  background: none;
  border: none;
  color: var(--hint);
  border-radius: 0;
}
.tabbar .tab.active { color: var(--btn); font-weight: 700; }

/* Строка списка: слева суть, справа цифры. Заменяет таблицы, которые на
   375 px уезжали в горизонтальный скролл. */
.row-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 0;
}
.row-main { flex: 1; min-width: 0; }
.row-main .small { display: block; }
.row-nums { text-align: right; white-space: nowrap; }
.row-nums b { font-size: 17px; }

.person { padding: 12px 16px; }
/* Кнопки карточки переносятся по строкам. Без wrap ряд не сжимался ниже
   ширины самого длинного слова в кнопке и на телефоне (375 px) уезжал за
   правый край карточки: «Удалить» оказывалась за обрезом и нажать её было
   нечем. До четвёртой кнопки ряд помещался, поэтому проблема пряталась —
   вылезла, когда их стало четверо у каждого («Табельный»), хотя поймать её
   можно было и раньше на том, у кого есть «Выйти везде».
   flex-basis auto, а не доля: на широком экране ряд остаётся одной строкой. */
.person-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.person-actions .inline-btn { flex: 1 1 auto; min-height: 44px; }

.log-item {
  padding: 10px 0;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.log-top { display: flex; justify-content: space-between; gap: 12px; }

/* На телефоне кнопки в карточке идут в столбик и остаются тапабельными. */
.card button { min-height: 48px; }

.event-card { padding: 14px 16px; }
.ev-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.event-card .ev-name { font-size: 16px; font-weight: 600; }
.event-card .person-actions { margin-top: 10px; }
.event-card .badge.warn { display: block; padding: 8px 10px; line-height: 1.35; }

/* Строка с переключателем: подпись слева, чекбокс справа. */
.toggle-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.toggle-row label { flex: 1; margin: 0; color: var(--text); font-size: 15px; }
.toggle-row label .small { display: block; }
.toggle-row input[type="checkbox"] { width: 28px; height: 28px; margin: 0; flex: 0 0 auto; }

/* Вход в админку. Раньше это была мелкая серая ссылка внизу экрана, и найти её
   с телефона было заметно труднее, чем хотелось: организатор попадает в панель
   только отсюда, другого пути внутри Telegram нет. Поэтому обычная кнопка. */
.admin-btn {
  display: block;
  margin-top: 20px;
  padding: 14px;
  min-height: 48px;
  border: 1px solid var(--hint);
  border-radius: 10px;
  background: var(--secondary-bg);
  color: var(--text);
  font-size: 16px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

/* Шапка админки: заголовок слева, возврат в сканер справа. */
.header-row { display: flex; align-items: center; gap: 12px; }
.header-row .app-event { flex: 1; min-width: 0; }

.back-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--hint);
  border-radius: 10px;
  background: var(--secondary-bg);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

/* ---- Экран смены промоутера ---- */

.shift { text-align: center; margin-bottom: 16px; }
.shift-cap { font-size: 12px; color: var(--hint); margin-bottom: 2px; }

.ring { position: relative; width: 168px; height: 168px; margin: 0 auto 8px; }
.ring svg { width: 168px; height: 168px; transform: rotate(-90deg); }
.ring-track { fill: none; stroke: var(--secondary-bg); stroke-width: 9; }
.ring-bar {
  fill: none;
  stroke: var(--btn);
  stroke-width: 9;
  stroke-linecap: round;
  /* Длина окружности при r=52: 2 * PI * 52 = 326.7 */
  stroke-dasharray: 326.7;
  stroke-dashoffset: 326.7;
  transition: stroke-dashoffset 300ms ease-out, stroke 300ms ease-out;
}
.ring.done .ring-bar { stroke: var(--ok-fg); }
/* Плана нет — обод не рисуем вовсе, остаётся обычная крупная цифра. */
.ring.no-goal svg { display: none; }

.ring-center {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  /* В круг диаметром 168 вписывается квадрат со стороной ~0.7 диаметра.
     Всё, что шире, вылезает за обод — поэтому подписи внутри только короткие. */
  width: 94px;
}
.ring-num {
  font-size: 46px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums; /* иначе при 9→10 блок дёргается */
}
.ring-num.wide { font-size: 34px; }
.ring-sub { font-size: 13px; color: var(--hint); margin-top: 3px; white-space: nowrap; }
.ring.done .ring-sub { color: var(--ok-fg); }

.pace { font-size: 13px; color: var(--hint); margin-bottom: 12px; min-height: 18px; }
.pace strong { color: var(--text); font-weight: 600; }

.team { border-top: 1px solid var(--secondary-bg); padding-top: 10px; text-align: left; }
.team-head {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 12px; color: var(--hint); margin-bottom: 6px;
}
.team-head span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.team-head span:last-child { flex-shrink: 0; font-variant-numeric: tabular-nums; }
.team-track { height: 5px; background: var(--secondary-bg); border-radius: 3px; overflow: hidden; }
.team-bar {
  height: 100%; width: 0;
  background: var(--hint);
  border-radius: 3px;
  transition: width 300ms ease-out, background 300ms ease-out;
}
.team-bar.done { background: var(--ok-fg); }

/* Прирост счётчика виден боковым зрением, пока промоутер смотрит на клиента. */
@keyframes bump { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1); } }
.ring-num.bump { animation: bump 200ms ease-out; }

@media (prefers-reduced-motion: reduce) {
  .ring-bar, .team-bar { transition: none; }
  .ring-num.bump { animation: none; }
}

/* Шапка: длинное название обрезается, метка не сжимается. Без min-width: 0
   флекс-ребёнок не сжимается и распирает строку — это и есть «надпись вылезла».
   Флекс вешаем на внутреннюю строку, а не на .app-header: в шапке ещё живёт
   селектор мероприятий, и он должен остаться на своей строке ниже. */
.app-head-row { display: flex; align-items: baseline; gap: 8px; }
.app-head-row .app-event { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Метку НЕ обрезаем: номер фрилансера нужен промоутеру целиком — по нему
   сверяют работу и считают оплату. Место при нехватке уступает название
   мероприятия: его промоутер и так знает, и оно продублировано в селекторе. */
.app-head-row .app-label { flex-shrink: 0; margin-top: 0; white-space: nowrap; }

.btn-scan { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 52px; }
.app-actions { padding-bottom: env(safe-area-inset-bottom, 0px); }

/* ---------- Ведомость выплат ---------- */
/* Строка на человека: код и контакт слева, часы и сумма справа. На 375 px
   настоящая таблица не помещается, поэтому раскладка та же, что у остальных
   списков админки. */
.pay-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--hint);
}
.pay-who { flex: 1 1 auto; min-width: 0; }
.pay-code { font-weight: 700; }
.pay-hours {
  width: 56px;
  text-align: center;
  padding: 6px 4px;
}
/* Автоматический расчёт — серым: видно, что число никто не проверял глазами. */
.pay-hours.auto { color: var(--hint); }
.pay-sum { text-align: right; min-width: 92px; }
.pay-total {
  display: flex;
  justify-content: space-between;
  padding: 12px;
  font-weight: 700;
}

.qr-block { border-top: 1px solid #e6e6ea; margin-top: 12px; padding-top: 12px; }
.qr-block-head { font-weight: 600; margin-bottom: 8px; }
.qr-mode { display: flex; gap: 16px; }
.qr-mode label { display: flex; align-items: center; gap: 6px; }
.qr-preview { margin-top: 12px; }
.qr-preview img { width: 180px; height: 180px; display: block; border-radius: 8px; }

/* Экран QR всегда светлый: код должен быть чёрным на белом при любой теме. */
.qr-screen {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  /* Не белый, а холодная подложка бренда: белым остаётся сама карточка с
     кодом, и на этом фоне она читается как отдельный объект. Светлота
     подложки (~96%) выбрана так, чтобы белое поле кода оставалось самым
     светлым пятном экрана — камере клиента нужен именно этот контраст. */
  background:
    radial-gradient(120% 70% at 50% 0%, #e8ecff 0%, #f4f6ff 45%, #ffffff 100%)
    no-repeat #ffffff;
  color: #111111;
  /* Та же причина, что у .login-screen: синяя шапка не должна уезжать под
     статус-бар на телефонах с вырезом. */
  padding-top: env(safe-area-inset-top, 0px);
}
.qr-head {
  background: var(--brand-blue);
  padding: 14px;
  display: flex;
  justify-content: center;
  /* Плашка отбрасывает тень своего же цвета: без неё синий кончается резко
     и выглядит наклейкой поверх экрана, а не шапкой. */
  box-shadow: 0 6px 20px rgba(8, 32, 254, 0.22);
  position: relative;
  z-index: 1;
}
.qr-logo { width: 132px; height: auto; display: block; }
.qr-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 20px;
  /* min-height: 0 разрешает flex-элементу сжаться меньше содержимого — без
     него overflow-y на длинном заголовке на невысоком экране просто не
     сработает, и контент уедет под фиксированные шапку и подвал. */
  min-height: 0;
  overflow-y: auto;
}
.qr-card {
  background: #ffffff;
  border-radius: 16px;
  padding: 16px;
  /* Тень тонирована в синий бренда, а не нейтрально-серая: на холодной
     подложке серая тень выглядит грязью. */
  box-shadow: 0 10px 28px rgba(8, 32, 254, 0.16), 0 1px 3px rgba(8, 32, 254, 0.10);
  position: relative;
}
/* Синие уголки вокруг карточки — единственное место, где бренд подходит к
   коду близко. Они лежат ЗА пределами карточки, то есть за пределами белой
   чистой зоны кода (padding карточки + 4 модуля отступа в самом коде,
   см. QR_OPTIONS в server/qr.js), поэтому распознаванию не мешают: декодер
   ищет границы кода внутри белого поля и цветного за его краем не видит.
   Внутрь карточки синий не заходит никогда. */
.qr-card::before {
  content: '';
  position: absolute;
  inset: -12px;
  pointer-events: none;
  --c: linear-gradient(var(--brand-blue), var(--brand-blue));
  background:
    var(--c) left    top    / 2px 26px no-repeat,
    var(--c) left    top    / 26px 2px no-repeat,
    var(--c) right   top    / 2px 26px no-repeat,
    var(--c) right   top    / 26px 2px no-repeat,
    var(--c) left    bottom / 2px 26px no-repeat,
    var(--c) left    bottom / 26px 2px no-repeat,
    var(--c) right   bottom / 2px 26px no-repeat,
    var(--c) right   bottom / 26px 2px no-repeat;
}
.qr-card img {
  display: block;
  width: min(72vw, 320px);
  height: auto;
  image-rendering: pixelated;
}
/* Домен назначения — то единственное, по чему клиент решает, сканировать ли
   код. Синий бренда здесь работает как подпись: адрес перестаёт быть серой
   технической строкой и читается как «это действительно Лэтуаль». */
.qr-host {
  /* Уголки видоискателя выступают на 12px за карточку, а gap у .qr-body
     общий — без этого запаса плашка домена почти касается нижних уголков. */
  margin-top: 10px;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--brand-blue);
  background: rgba(8, 32, 254, 0.07);
  border-radius: 999px;
  padding: 6px 14px;
}
/* Пустой элемент не должен рисовать пустую плашку: до ответа сервера и на
   загруженной картинкой (не ссылкой) — домена нет. */
.qr-host:empty { display: none; }
/* Диагностика связи для промоутера, не для клиента: своя, тёплая от
   qr-host — отдельная роль не должна выглядеть как продолжение адреса. */
.qr-error { font-size: 13px; color: #a15c00; text-align: center; }
/* «Загружаем код…» — не отказ, а нейтральное ожидание первого ответа сервера;
   тёплый цвет ошибки здесь читался бы как то, что уже что-то пошло не так. */
.qr-error.qr-loading { color: #666666; }

/* Подвал тоже часть этого экрана для клиента: .app-actions и .secondary
   красятся переменными темы Telegram, а этот экран обязан оставаться
   светлым при любой теме — иначе в тёмной теме промоутера снизу приезжает
   тёмная полоса на белом фоне. */
.qr-screen .app-actions {
  background: #ffffff;
  border-top: 1px solid rgba(8, 32, 254, 0.14);
}
/* Кнопка промоутера, а не клиента: синяя обводка вместо серой связывает её с
   шапкой, но заливки нет — заливкой на этом экране обладает только шапка,
   иначе низ начинает спорить с кодом за внимание. */
.qr-screen button.secondary {
  background: #ffffff;
  color: var(--brand-blue);
  border: 1px solid rgba(8, 32, 254, 0.35);
  font-weight: 600;
}

/* ---------- Сканер на камере (веб) ---------- */
.scanner-screen {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #000;
  display: flex;
  flex-direction: column;
}
.scanner-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Рамка: квадрат по центру, тёмная вуаль вокруг через огромную тень. */
.scanner-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(70vw, 300px);
  height: min(70vw, 300px);
  transform: translate(-50%, -50%);
  border: 3px solid #fff;
  border-radius: 16px;
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.45);
  pointer-events: none;
}
.scanner-hint {
  position: absolute;
  left: 16px;
  right: 16px;
  top: calc(50% + min(35vw, 150px) + 16px);
  text-align: center;
  color: #fff;
  font-size: 15px;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
  pointer-events: none;
}
.scanner-feedback {
  position: absolute;
  top: calc(24px + env(safe-area-inset-top, 0px));
  left: 16px;
  right: 16px;
  padding: 14px 16px;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 600;
  text-align: center;
  white-space: pre-line;
  z-index: 1;
}
.scanner-feedback.ok   { background: #e3f7e3; color: #1b7a1b; }
.scanner-feedback.deny { background: #fde3e3; color: #a11b1b; }
.scanner-feedback.warn { background: #fff4e0; color: #a15c00; }
.scanner-actions {
  margin-top: auto;
  padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  position: relative;
  z-index: 1;
}
.scanner-actions .secondary {
  background: rgba(255, 255, 255, 0.92);
  color: #111;
}

/* ---------- Вход и смена пароля (веб) ---------- */
.login-screen {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  overflow-y: auto;
  /* Экран во весь экран, поэтому синяя плашка со знаком иначе уезжает под
     статус-бар на телефонах с вырезом. */
  padding-top: env(safe-area-inset-top, 0px);
}
.login-body {
  padding: 24px 16px;
  max-width: 420px;
  width: 100%;
  margin: 0 auto;
}
.login-body h1 { margin-bottom: 16px; }
.login-body label { color: var(--text); }
.pw-row { position: relative; }
.pw-row input { padding-right: 88px; }
.pw-toggle {
  position: absolute;
  right: 4px;
  /* Центр считаем по полю, а не по строке: у input снизу 12px margin, и
     ровно на их половину середина строки ниже середины поля. */
  top: calc(50% - 6px);
  transform: translateY(-50%);
  width: auto;
  min-height: 0;
  padding: 8px 10px;
  font-size: 14px;
  font-weight: 500;
  background: none;
  color: var(--text);
}
.form-error {
  margin: 0 0 12px;
  color: #a11b1b;
  font-size: 14px;
}
.login-body .secondary { margin-top: 8px; }
.login-body .hint { margin-top: 16px; }

.web-actions { display: flex; }
.web-actions .link-btn { width: auto; flex: 1; }

/* Своим блоком и в конце файла: базовое .form-error объявлено выше в этой же
   секции, а при равной специфичности побеждает то правило, что ниже. */
@media (prefers-color-scheme: dark) {
  /* Тот же тон, что у .result.deny: на #121212 бордовый #a11b1b даёт 2.4:1 и
     не читается. */
  .form-error { color: #fb8a8a; }
}

/* ---------- Диалог пароля в админке ---------- */
.pw-dialog {
  border: none;
  border-radius: 12px;
  padding: 20px;
  max-width: 360px;
  width: calc(100% - 32px);
  background: var(--bg);
  color: var(--text);
}
.pw-dialog::backdrop { background: rgba(0, 0, 0, 0.5); }
.pw-dialog h2 { margin-top: 0; }
.pw-dialog-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 26px;
  letter-spacing: 2px;
  text-align: center;
  padding: 14px;
  margin: 8px 0 12px;
  border-radius: 10px;
  background: var(--secondary-bg);
  user-select: all;
}
.web-access { margin: 4px 0 8px; color: var(--hint); }

/* Импорт мероприятия из Excel */
.import-event { margin: 8px 0; }
.import-notes { margin: 8px 0; padding-left: 20px; font-size: 14px; }
.import-errors li { color: #b3261e; }
.import-warnings li { color: #8a6d00; }
#import-file { margin: 8px 0; max-width: 100%; }

@media (prefers-color-scheme: dark) {
  /* Тёмный фон #121212 съедает тёмно-красный/оливковый — те же тона, что у
     .badge.warn ниже по палитре. */
  .import-errors li { color: #fb8a8a; }
  .import-warnings li { color: #e6c25a; }
}
