:root {
  --bg: #ffffff;
  --fg: #17181a;
  --fg-dim: #707579;
  --card: #f4f4f6;
  --border: rgba(0, 0, 0, 0.10);
  --accent: #2e7cf6;
  --accent-fg: #ffffff;
  --link: #2e7cf6;
  --danger: #ff6b6b;
  --ok: #57c785;
  --safe-b: env(safe-area-inset-bottom, 0px);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* ── единый тактильный отклик для всех кликабельных элементов ───────────────────────────────
   transition общий, конкретные :active/:hover ниже задают силу эффекта под тип элемента. */
button, .home-tile, .project-link, .tile, a.project-link {
  transition: transform .12s ease, opacity .12s ease, box-shadow .12s ease, background-color .12s ease, filter .12s ease;
}

@media (hover: hover) {
  button:not(:disabled):hover,
  .home-tile:hover,
  .project-link:hover,
  .tile:hover {
    transform: scale(1.02);
  }

  /* карточки/нейтральные кнопки на фоне var(--card) — заметная акцентная подсветка */
  .home-tile:hover,
  .project-link:hover,
  .back-btn:hover,
  .balance-chip:hover,
  .home-balance-action:hover,
  .balance-action-btn:hover {
    background-color: color-mix(in srgb, var(--accent) 28%, var(--card));
  }

  /* уже акцентно-заполненные кнопки (сплошной var(--accent)) — становятся ярче/темнее того же цвета */
  .submit-btn:hover,
  .topup-btn:hover,
  .home-balance-action.primary:hover,
  .balance-action-btn.primary:hover {
    filter: brightness(1.15) saturate(1.1);
  }

  /* кнопка «Предложить идею» — прозрачная с обводкой, тоже получает лёгкую подсветку */
  .home-suggest-btn:hover {
    background-color: color-mix(in srgb, var(--accent) 16%, transparent);
    border-color: var(--accent);
  }

  .topup-partner-btn:hover {
    background-color: color-mix(in srgb, var(--accent) 16%, transparent);
    border-color: var(--accent);
  }
}

button:not(:disabled):active,
.home-tile:active,
.project-link:active,
.tile:active {
  transform: scale(0.96);
}

@keyframes bg-drift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  background-image:
    radial-gradient(circle at 15% 8%, rgba(77, 163, 255, 0.10) 0%, transparent 45%),
    radial-gradient(circle at 85% 92%, rgba(77, 163, 255, 0.08) 0%, transparent 50%);
  background-size: 160% 160%;
  animation: bg-drift 22s ease-in-out infinite;
  color: var(--fg);
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

@supports (background: color-mix(in srgb, red 10%, transparent)) {
  body {
    background-image:
      radial-gradient(circle at 15% 8%, color-mix(in srgb, var(--accent) 10%, transparent) 0%, transparent 45%),
      radial-gradient(circle at 85% 92%, color-mix(in srgb, var(--accent) 8%, transparent) 0%, transparent 50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  body { animation: none; }
}

.topbar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 14px 16px 0;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 5;
}

/* шапка (баланс/статистика) занимает много места на телефоне — при открытой клавиатуре
   (фокус на текстовом поле формы) прячем её целиком, чтобы освободить экран под ввод */
/* Шапка при клавиатуре БОЛЬШЕ НЕ ПРЯЧЕТСЯ. Она закреплена сверху и занимает
   место в потоке; убирая её из потока в момент фокуса, страница мгновенно
   подскакивала на её высоту - человек касался поля, и экран уезжал у него
   из-под пальца.

   Задумка была освободить место под ввод, но клавиатура и так сжимает окно,
   а прыжок в момент касания пугает сильнее, чем помогают выигранные
   пятьдесят пикселей. В чате шапку и раньше не трогали - ровно по этой же
   причине. */

.balance-icon { font-size: 13px; line-height: 1; }

/* ── balance screen ──────────────────────────────────────────────────────────────────────── */
.balance-view { max-width: 480px; margin: 0 auto; padding: 4px 4px calc(24px + var(--safe-b)); }

.back-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: none;
  background: var(--card);
  color: var(--fg);
  font-size: 14.5px;
  font-weight: 600;
  font-family: inherit;
  padding: 9px 18px 9px 14px;
  border-radius: 999px;
  /* 14px здесь складывались с верхним полем экрана и отступом заголовка -
     втроём они съедали высоту карточки на КАЖДОМ экране приложения. */
  margin-bottom: 8px;
  cursor: pointer;
}

.back-btn:active {
  transform: scale(0.95);
  opacity: .85;
}

.balance-card {
  background: var(--card);
  border-radius: 16px;
  padding: 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.balance-card-row { display: flex; align-items: center; gap: 14px; min-width: 0; }

.balance-card-icon { font-size: 28px; line-height: 1; }

.balance-card-value { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.2; }

.balance-card-label { font-size: 13px; color: var(--fg-dim); margin-top: 1px; }

.balance-action-btn {
  padding: 10px 16px;
  border-radius: 10px;
  border: none;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: 14px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}

.balance-action-btn:active { opacity: .82; }

.balance-action-btn.secondary {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  border: none;
}

/* ── topup modal ──────────────────────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.55);
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.modal-card {
  width: 100%;
  max-width: 340px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--bg);
  border-radius: 20px;
  position: relative;
  padding: 28px 20px 20px;
}

.modal-card .detail-close {
  background: var(--card);
  color: var(--fg-dim);
  width: 28px;
  height: 28px;
  font-size: 14px;
  top: 12px;
  right: 12px;
}

.modal-body { display: flex; flex-direction: column; gap: 12px; }

.topup-question { font-size: 16px; font-weight: 600; text-align: center; margin-bottom: 2px; padding: 0 50px; }

.topup-custom-input {
  width: 100%;
  padding: 13px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 18px;
  font-weight: 600;
  text-align: center;
  font-family: inherit;
  outline: none;
}

.topup-custom-input:focus { border-color: var(--accent); }

.topup-partner-btn {
  width: 100%;
  padding: 13px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg);
  font-size: 14.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.topup-partner-btn:active { opacity: .78; }

.feedback-textarea {
  width: 100%;
  padding: 13px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 15px;
  font-family: inherit;
  line-height: 1.45;
  resize: none;
  outline: none;
}

.feedback-textarea:focus { border-color: var(--accent); }

.feedback-counter {
  font-size: 12px;
  color: var(--fg-dim);
  text-align: right;
  margin-top: -6px;
}

.feedback-success-text {
  font-size: 14px;
  color: var(--fg-dim);
  text-align: center;
  line-height: 1.5;
}

/* ── projects screen ──────────────────────────────────────────────────────────────────────── */
.projects-view { max-width: 480px; margin: 0 auto; padding: 4px 4px calc(24px + var(--safe-b)); }

.projects-heading {
  font-size: 14px;
  font-weight: 600;
  color: var(--fg-dim);
  margin: 0 4px 8px;
  letter-spacing: -0.1px;
}

.projects-list { display: flex; flex-direction: column; gap: 8px; }

.project-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  padding: 22px 16px;
  border-radius: 14px;
  border: none;
  background: var(--card);
  text-decoration: none;
  color: var(--fg);
  text-align: center;
  font-family: inherit;
  cursor: pointer;
}

.project-link:active {
  opacity: .88;
  background: var(--accent);
  color: var(--accent-fg);
}

.project-link:active .project-link-desc {
  color: var(--accent-fg);
  opacity: .85;
}

.project-link-title { font-size: 17px; font-weight: 700; letter-spacing: -0.2px; }

.project-link-desc {
  font-size: 12.5px;
  font-weight: 400;
  color: var(--fg-dim);
  line-height: 1.35;
}

/* Переливающийся градиент - фон плавно едет по цветам. Текст белый с лёгкой тенью,
   чтобы читался на любом участке градиента.

   Цвета приглушены (Кирилл 2026-08-27). Было розовый-фиолетовый-синий-бирюза-жёлтый:
   полная радуга на одной кнопке перетягивала внимание со всего экрана, а жёлтый участок
   вдобавок терял белый текст. Теперь четыре близких приглушённых тона - перелив
   чувствуется, но кнопка перестала быть самым ярким пятном приложения. */
.project-link-rainbow {
  background: linear-gradient(
    100deg,
    #3F6B78, #4A5A7C, #63577A, #47756B, #3F6B78
  );
  background-size: 300% 100%;
  animation: rainbow-slide 9s linear infinite;
}

.project-link-rainbow .project-link-title,
.project-link-rainbow .project-link-desc {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}

@keyframes rainbow-slide {
  0% { background-position: 0% 50%; }
  100% { background-position: 300% 50%; }
}

/* Наведение/нажатие меняют фон у обычных карточек — здесь фон нужен свой, оставляем
   только масштаб и лёгкое затемнение. */
.project-link-rainbow:hover,
.project-link-rainbow:active {
  background: linear-gradient(
    100deg,
    #3F6B78, #4A5A7C, #63577A, #47756B, #3F6B78
  );
  background-size: 300% 100%;
}

.project-link-rainbow:active .project-link-desc { color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .project-link-rainbow { animation: none; }
}

.projects-divider {
  height: 1px;
  background: var(--border);
  margin: 22px 4px;
}

/* ── news screen ──────────────────────────────────────────────────────────────────────────── */
.news-view { max-width: 480px; margin: 0 auto; padding: 4px 4px calc(24px + var(--safe-b)); }

.news-list { display: flex; flex-direction: column; gap: 12px; }

.news-card {
  padding: 16px 18px;
  border-radius: 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 4px 12px rgba(16, 24, 40, .05);
}

.news-card-title {
  font-size: 15.5px;
  font-weight: 700;
  line-height: 1.35;
  margin: 0 0 7px;
  letter-spacing: -0.25px;
}

.news-card-summary {
  font-size: 13px;
  color: var(--fg-dim);
  line-height: 1.45;
}

.news-updated {
  text-align: center;
  font-size: 12px;
  color: var(--fg-dim);
  margin-top: 16px;
}

/* ── earn points screen ───────────────────────────────────────────────────────────────────── */
.earn-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }

.earn-card {
  padding: 16px 18px;
  border-radius: 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 4px 12px rgba(16, 24, 40, .05);
}

.earn-card-action {
  display: block;
  width: 100%;
  text-align: left;
  border: 1px solid var(--border);
  color: var(--fg);
  font-family: inherit;
  cursor: pointer;
}

.earn-card-go {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  margin-top: 10px;
}

.earn-card-title { font-size: 13px; font-weight: 600; color: var(--fg-dim); margin-bottom: 6px; letter-spacing: -0.1px; }
.earn-card-amount { font-size: 21px; font-weight: 700; color: var(--accent); margin-bottom: 6px; letter-spacing: -0.4px; }
.earn-card-desc { font-size: 13px; color: var(--fg-dim); line-height: 1.45; }

.earn-note {
  font-size: 13px;
  color: var(--fg-dim);
  text-align: center;
  line-height: 1.5;
  margin: 18px 8px 18px;
}

main { padding: 16px 16px calc(24px + var(--safe-b)); max-width: 560px; margin: 0 auto; }

.state-msg { text-align: center; color: var(--fg-dim); padding: 40px 16px; font-size: 14px; }

.hidden { display: none !important; }

.title { font-size: 19px; font-weight: 600; letter-spacing: -0.3px; margin: 2px 0 18px; }

.field { display: block; margin-bottom: 18px; }

.field-label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--fg-dim);
  margin-bottom: 7px;
}

.field-optional { font-weight: 400; font-size: 12px; opacity: .75; }

input[type="text"], input[type="search"], input[type="file"], select {
  width: 100%;
  max-height: 46px;
  padding: 11px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 16px;
  font-family: inherit;
  outline: none;
  transition: border-color .15s;
}

textarea {
  width: 100%;
  padding: 11px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 16px;
  font-family: inherit;
  line-height: 1.45;
  resize: none;
  outline: none;
  transition: border-color .15s;
}

input[type="text"]:focus, input[type="search"]:focus, select:focus, textarea:focus { border-color: var(--accent); }

input[type="file"] { font-size: 14px; padding: 9px 12px; }

.price-row { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 10px; }

.markets-box { display: flex; flex-wrap: wrap; gap: 8px; }

.market-chip {
  padding: 8px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
  font-size: 14px;
  cursor: pointer;
  user-select: none;
  transition: background .12s, border-color .12s;
}

.market-chip.selected {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
  font-weight: 500;
}

.photo-preview-wrap { margin-top: 11px; }

.photo-preview {
  max-width: 100%;
  max-height: 260px;
  border-radius: 12px;
  display: block;
}

.hint { font-size: 12.5px; color: var(--fg-dim); margin-top: 7px; line-height: 1.4; }
.hint.error { color: var(--danger); }
.hint.ok { color: var(--ok); }

.submit-btn {
  width: 100%;
  padding: 14px;
  border-radius: 12px;
  border: none;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.2px;
  cursor: pointer;
  transition: opacity .15s;
}

.submit-btn:active { opacity: .82; }
.submit-btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* Блик по кнопке. Нужен там, где действие платное и человек колеблется:
   кнопка должна выглядеть живой, а не ждать, пока её заметят.
   Блик идёт РЕДКО - раз в четыре секунды: непрерывное движение через минуту
   превращается в раздражитель, который перестают видеть. */
.btn-shine { position: relative; overflow: hidden; }
.btn-shine::after {
  content: ""; position: absolute; top: -60%; bottom: -60%; left: -40%; width: 32%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .42), transparent);
  transform: skewX(-18deg);
  animation: btnShine 4s ease-in-out infinite;
  pointer-events: none;
}
/* Пока кнопка недоступна, блик обещает то, чего нажатие не даст. */
.btn-shine:disabled::after { display: none; }
@keyframes btnShine {
  0%   { left: -40%; }
  55%  { left: 115%; }
  100% { left: 115%; }
}
@media (prefers-reduced-motion: reduce) {
  .btn-shine::after { display: none; }
}

/* ── home screen ──────────────────────────────────────────────────────────────────────────── */
.home-view { max-width: 480px; margin: 0 auto; padding: 14px 16px calc(24px + var(--safe-b)); }

.home-balance-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px 10px;
}

.home-balance-item {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  border: none;
  background: none;
  padding: 4px 2px;
  color: var(--fg);
  font-family: inherit;
  cursor: pointer;
  border-radius: 8px;
}


.home-balance-action {
  padding: 7px 12px;
  border-radius: 999px;
  border: none;
  background: var(--card);
  color: var(--fg);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  margin-right: auto;
}

/* Пары прижаты каждая к себе; между парами - обычный промежуток, а не растянутый. */
.home-balance-group {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}

/* Баллы левее: пара с ними начинает строку, вторая пара уходит вправо. */
.home-balance-bar { justify-content: space-between; }
.home-balance-action { margin-right: 0; }
.home-balance-action:last-child { margin-right: 0; margin-left: 0; }

.home-balance-action.primary {
  background: var(--accent);
  color: var(--accent-fg);
}

.home-balance-action:active { opacity: .8; }

.home-stats-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 7px;
  padding: 0 4px 12px;
  font-size: 11.5px;
  color: var(--fg-dim);
}

.home-stats-item { white-space: nowrap; }

.home-stats-clickable {
  border: none;
  background: none;
  padding: 0;
  font-size: inherit;
  color: inherit;
  font-family: inherit;
  cursor: pointer;
  border-bottom: 1px dashed var(--border);
}

.home-stats-divider { color: var(--border); font-size: 11px; }

.home-stats-item b { color: var(--fg); font-weight: 600; }

.earn-cta {
  display: block;
  width: 100%;
  padding: 10px;
  margin: 0 0 10px;
  border-radius: 12px;
  border: none;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: 14px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  letter-spacing: -0.2px;
}

.home-quickbar {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 8px;
}

.home-quickbar-divider {
  height: 1px;
  background: var(--border);
  margin: 0 0 10px;
}

/* Быстрые кнопки: они вспомогательные и не должны занимать высоту наравне с плитками.
   ВАЖНО: padding отсюда перебивается правилом ниже по файлу - высоту менять ТАМ. */
.quick-btn {
  padding: 5px 6px;
  border-radius: 11px;
  border: none;
  background: var(--card);
  color: var(--fg);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.home-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.home-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  padding: 16px 14px;
  border-radius: 16px;
  border: none;
  background: var(--card);
  text-align: left;
  cursor: pointer;
  font-family: inherit;
  min-height: 96px;
}

.home-tile:active { opacity: .82; }

.home-tile-icon { font-size: 24px; line-height: 1; }

.home-tile-title { font-size: 13.5px; font-weight: 500; line-height: 1.3; color: var(--fg); }

/* Цена в правом верхнем углу плитки. Человек должен видеть её ДО того, как
   откроет раздел и начнёт заполнять форму, - иначе цена всплывает уже после
   работы, и это читается как подвох. */
.tile-price {
  position: absolute; top: 8px; right: 8px; z-index: 3;
  font-size: 11.5px; font-weight: 700; color: var(--fg-dim);
  background: rgba(255, 255, 255, .78); border-radius: 999px; padding: 2px 8px;
  line-height: 1.5; white-space: nowrap;
}
.home-tile { position: relative; }

.admin-entry-btn {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 11px;
  border-radius: 12px;
  border: 1px dashed var(--border);
  background: transparent;
  color: var(--fg-dim);
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
}

.admin-menu { display: flex; flex-direction: column; gap: 12px; }

/* Плитки админки по две в ряд. Каждая занимала полную ширину ради двух строк
   текста, и до нижних приходилось листать целый экран. Пояснение под числом
   в сетке прячем: в нём смысл только при первом знакомстве с разделом,
   а дальше оно просто съедает место. */
/* Три в ряд. Возможностей в админке много, и при двух колонках список уходил
   на три экрана - до нижних плиток человек просто не долистывал. */
.admin-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px;
}
.admin-grid .earn-card { padding: 10px 8px; min-height: 74px; }
.admin-grid .earn-card-title {
  font-size: 10.5px; line-height: 1.2; margin-bottom: 3px; letter-spacing: -.1px;
}
/* Число - главное на плитке, но в треть ширины длинная подпись вроде
   «579 человек» не влезает: переносим по словам, а не режем. */
.admin-grid .earn-card-amount {
  font-size: 13.5px; margin-bottom: 0; letter-spacing: -.2px;
  line-height: 1.2; word-break: break-word;
}
.admin-grid .earn-card-desc { display: none; }
/* Фоновый значок в узкой плитке занимает половину её ширины и мешает
   читать цифру. */
/* Значок в плитке админки виден - правила его размера и положения стоят
   в конце файла, там же, где остальная раскладка панелей. */

/* Цены - тоже квадратиками. Карточка во всю ширину ради названия и одного поля
   занимала полэкрана на услугу, и до нужной приходилось листать. */
/* Цены оставляем по две: там поле ввода и кнопка, в треть ширины они
   не помещаются. */
.price-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
.price-grid .price-row {
  flex-direction: column; gap: 8px; align-items: stretch;
  padding: 12px 11px; min-height: 0; margin-bottom: 0;
}
.price-grid .price-info b { font-size: 12.5px; line-height: 1.25; display: block; }
.price-grid .price-edit { display: flex; gap: 6px; }
.price-grid .price-input {
  flex: 1 1 auto; min-width: 0; text-align: center;
  font-size: 15px; font-weight: 700; padding: 8px 6px;
}
.price-grid .admin-approve-btn {
  flex: 0 0 auto; width: auto; padding: 8px 10px; font-size: 12px;
}
/* Пояснение под названием в сетке не помещается - оно и не нужно: услуг мало,
   и каждая понятна по имени. */
.price-grid .earn-card-desc, .price-grid .tile-line { display: none; }

/* Версия стоит на месте заголовка, справа от «Назад» - как заголовки
   на остальных экранах. */
/* Версия - обычный заголовок в общей строке с «Назад», как на остальных
   экранах. Отрицательный отступ, которым её раньше подтягивали, оставлял
   кнопку на отдельной строке ниже - строка сверху пустовала. */
#adminAppVersion { text-align: right; }

/* Кнопка «поделиться разделом» - сразу справа от названия, без рамки и фона.
   Рамка делала из неё отдельный элемент управления, спорящий с заголовком
   за внимание; здесь нужен именно значок при названии. */
.projects-heading[data-share] {
  display: flex; align-items: center; justify-content: flex-end; gap: 6px;
}
/* Селектор с двумя классами обязателен: .copy-btn задаёт рамку и фон, и одного
   .share-btn ему по весу не хватало - коробка оставалась на месте. */
.projects-heading .share-btn.copy-btn {
  width: 22px; height: 22px; flex: 0 0 auto; padding: 0; margin-right: -2px;
  border: none; background: none; box-shadow: none; border-radius: 0;
  /* Приглушён: это подсобный значок, а не действие, ради которого сюда зашли. */
  opacity: .38;
}
.projects-heading .share-btn.copy-btn:active { opacity: .7; }
.projects-heading .share-btn.copy-btn.is-done { opacity: 1; background: none; }
.projects-heading .share-btn .copy-ico { width: 14px; height: 14px; }

/* Тот же значок в панели экрана ссылок - правее кнопки «Новая кампания».
   Заголовка там нет, поэтому правила пишем отдельно, но вид один в один. */
.links-bar .share-btn.copy-btn {
  width: 22px; height: 22px; flex: 0 0 auto; padding: 0; margin-left: 2px;
  border: none; background: none; box-shadow: none; border-radius: 0;
  opacity: .38;
}
.links-bar .share-btn.copy-btn:active { opacity: .7; }
.links-bar .share-btn.copy-btn.is-done { opacity: 1; background: none; }
.links-bar .share-btn .copy-ico { width: 14px; height: 14px; }

/* Тот же значок в панели ленты товаров - правее «Загрузить товары». */
.screen-topbar .share-btn.copy-btn {
  width: 22px; height: 22px; flex: 0 0 auto; padding: 0; margin-left: 2px;
  border: none; background: none; box-shadow: none; border-radius: 0;
  opacity: .38;
}
.screen-topbar .share-btn.copy-btn:active { opacity: .7; }
.screen-topbar .share-btn.copy-btn.is-done { opacity: 1; background: none; }
.screen-topbar .share-btn .copy-ico { width: 14px; height: 14px; }

/* Строка «Назад + заголовок» на ленте: та же раскладка, что на остальных
   экранах, - кнопка слева, название справа. */
/* Строка «Назад + название». Плотная: у кнопки свой отступ снизу в 14px,
   у экрана свой сверху - вместе они съедали высоту целой карточки на пустом
   месте. Отступ кнопки снимаем, строке оставляем 8px. */
.feed-bar { display: flex; align-items: center; gap: 8px; }
/* Название прижато к ПРАВОМУ краю, как заголовки на всех остальных экранах.
   Кнопка «Назад» слева, название справа - глаз ищет его там же, где всегда.
   Правило с двумя классами и признаком: иначе его перебивают более общие,
   и заголовок сползает влево, к самой кнопке. */
/* Заголовок прижимаем к правому краю САМИМ ЭЛЕМЕНТОМ - авто-отступом слева,
   а не выравниванием содержимого внутри него.
   Прошлая попытка выравнивала контент внутри растянутого блока, и её перебивали
   более общие правила: блок оставался во всю ширину, а текст вставал слева -
   заголовок выглядел приклеенным к «Назад». Авто-отступ перебить нечем:
   он двигает сам блок, и тот прижимается к краю при любой раскладке. */

/* Подзаголовки разделов ВНУТРИ экрана - слева и без прижатия к правому краю:
   заголовок экрана стоит справа от «Назад», а эти относятся к спискам под ними. */
h3.projects-heading { text-align: left; justify-content: flex-start; margin-top: 4px; }
h3.projects-heading[data-share] .share-btn { margin-left: 2px; }

/* Карточка генерации: фото, что определили, чья, и оценка. */
.gen-card { margin-bottom: 10px; }
.gen-head { display: flex; align-items: center; gap: 10px; }
.gen-photo {
  width: 48px; height: 48px; flex: 0 0 auto; border-radius: 10px;
  object-fit: cover; background: var(--bg-page, #f5f5f7);
}
.gen-meta { flex: 1 1 auto; min-width: 0; }
.gen-title { font-size: 14px; font-weight: 700; color: var(--fg); word-break: break-word; }
.gen-sub { font-size: 12px; color: var(--fg-dim); margin-top: 2px; }
.gen-mark { flex: 0 0 auto; font-size: 20px; }
/* Отзыв - главное на карточке: ради него сюда и заходят. */
.gen-review {
  margin-top: 8px; padding: 8px 10px; border-radius: 10px;
  background: #fef2f2; color: #991b1b; font-size: 13px; line-height: 1.4;
}
/* Само описание - справочно, поэтому мельче и приглушено. */
/* Описание показываем ЦЕЛИКОМ. Обрезка по высоте была нужна, пока карточки
   не сворачивались: теперь раскрытая карточка и открывается ради того, чтобы
   прочесть текст, а обрезанный конец гнал бы искать его в другом месте. */
.gen-desc {
  margin-top: 8px; font-size: 12.5px; line-height: 1.45; color: var(--fg-dim);
  white-space: pre-line;
}

.gen-who-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.gen-who-link:active { opacity: .7; }

/* Ключевые слова - плитками: список через запятую в строку читается как
   продолжение описания, а это отдельная часть работы, за которую платят. */
.gen-keywords { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.gen-kw {
  font-size: 11.5px; line-height: 1.6; padding: 1px 8px;
  border-radius: 999px; background: var(--card); color: var(--fg-dim);
}

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

.admin-approve-btn,
.admin-reject-btn {
  flex: 1;
  padding: 10px;
  border-radius: 10px;
  border: none;
  font-size: 13.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.admin-approve-btn { background: var(--ok); color: #fff; }
.admin-reject-btn { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
.admin-approve-btn:disabled, .admin-reject-btn:disabled { opacity: .5; cursor: not-allowed; }

.admin-field { font-size: 13.5px; line-height: 1.6; }
.admin-field b { font-weight: 600; }
.admin-link { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }

.home-suggest-btn {
  display: block;
  width: 100%;
  margin-top: 22px;
  padding: 13px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg-dim);
  font-size: 14px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
}

.home-suggest-btn:active { opacity: .7; }

/* Лампочка плавно разгорается и затухает — ровное свечение, без резких вспышек. */
.bulb-blink {
  display: inline-block;
  animation: bulb-blink 1.6s ease-in-out infinite;
}

@keyframes bulb-blink {
  0%, 78% { opacity: 1; }
  88% { opacity: 0; }
  98%, 100% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .bulb-blink { animation: none; }
}

.feed-view { max-width: 560px; margin: 0 auto; padding: 12px 12px calc(24px + var(--safe-b)); }

.filter-bar { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 12px; }

.filter-bar select {
  flex: 1;
  padding: 9px 10px;
  border-radius: 10px;
  font-size: 14px;
}

.feed-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

#feedToFormBtn { margin-top: 14px; }

.tile {
  background: var(--card);
  border-radius: 14px;
  overflow: hidden;
  cursor: pointer;
  display: flex;
  flex-direction: column;
}

/* 2026-08-22 (Kirill): убрана дымка по возрасту — статус не флипается по TTL (см. feed_db.py),
   заявка полностью активна и старше суток; тусклость только вводила в заблуждение и роняла
   контраст фото. Дата-бейдж (aged-badge ниже) остаётся приглушённым как нейтральная метка. */

.tile-photo-wrap { position: relative; aspect-ratio: 1 / 1; background: var(--bg); }

.tile-photo { width: 100%; height: 100%; object-fit: cover; display: block; }

.tile-badge {
  position: absolute;
  top: 7px;
  left: 7px;
  font-size: 11px;
  font-weight: 500;
  padding: 3px 8px;
  border-radius: 999px;
  background: rgba(0,0,0,.6);
  color: #fff;
  backdrop-filter: blur(6px);
}

.tile-badge.aged-badge { background: rgba(60,60,60,.7); }

.tile-badge-offers { top: auto; bottom: 7px; left: auto; right: 7px; background: rgba(45,125,246,.9); }

.tile-body { padding: 9px 10px 11px; }

.tile-title {
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.32;
  margin: 0 0 5px;
  letter-spacing: -0.1px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tile-line { font-size: 12px; color: var(--fg-dim); margin: 2px 0; }
.tile-price-line { font-size: 13px; font-weight: 600; color: var(--fg); margin-top: 4px; }

.detail-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.55);
  z-index: 20;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.detail-card {
  width: 100%;
  max-width: 560px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--bg);
  border-radius: 18px 18px 0 0;
  position: relative;
  padding-bottom: var(--safe-b);
}

/* Квадрат задан САМОЙ рамке, а не фотографии внутри неё. Пока фото грузится,
   у картинки нет размеров, рамка схлопывается, и стрелки переключения - они
   стоят по середине рамки - оказываются наверху; когда фото доезжает, рамка
   вырастает, и стрелки прыгают на своё место. С высотой у рамки прыгать нечему:
   она известна до загрузки. */
.detail-photo-wrap { position: relative; aspect-ratio: 1 / 1; overflow: hidden; background: var(--card); }

.detail-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,.5);
  color: #fff;
  font-size: 16px;
  cursor: pointer;
  z-index: 2;
  backdrop-filter: blur(6px);
}

/* крупнее и контрастнее именно поверх фото товара — на однотонном фоне модалок (пополнение,
   фидбек) размер выше не нужен, там кнопка и так хорошо видна */
.detail-overlay .detail-close {
  width: 40px;
  height: 40px;
  font-size: 19px;
  background: rgba(0,0,0,.65);
  box-shadow: 0 2px 8px rgba(0,0,0,.25);
}

.detail-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,.5);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
}

.detail-nav:active { opacity: .8; }
.detail-nav:disabled { opacity: .25; cursor: not-allowed; }

/* Та же болезнь, что была у затвора камеры: общее правило для всех кнопок
   в начале файла ставит transform: scale() при наведении и нажатии - и
   затирает translateY(-50%), которым стрелка отцентрована по высоте фото.
   При каждом нажатии стрелка прыгала вниз на двадцать пикселей и
   возвращалась. Центрирование и масштаб пишем вместе, одним свойством. */
.detail-nav:not(:disabled):hover { transform: translateY(-50%) scale(1.02); }
.detail-nav:not(:disabled):active { transform: translateY(-50%) scale(0.94); }
/* Пока стрелка недоступна, общее правило её и не трогает - но и мы не должны
   оставлять её без центрирования. */
.detail-nav:disabled { transform: translateY(-50%); }

.detail-nav-prev { left: 10px; }
.detail-nav-next { right: 10px; }

.detail-photo { width: 100%; height: 100%; object-fit: cover; display: block; }

.detail-body { padding: 16px; }
.detail-title { font-size: 17px; font-weight: 600; letter-spacing: -0.2px; margin: 0 0 12px; }
.detail-title-divider { height: 1px; background: var(--border); margin: 0 0 12px; }
.detail-meta { font-size: 14px; color: var(--fg-dim); line-height: 1.65; margin-bottom: 11px; }
.detail-status { font-size: 13px; font-weight: 500; }
/* Срок и счётчики в одной строке по краям: слева сколько времени осталось,
   справа сколько людей уже посмотрело и ответило. */
.detail-status-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 12px;
}
.detail-counts { font-size: 12.5px; color: var(--fg-dim); white-space: nowrap; }
.detail-status.is-fresh { color: var(--ok); }
.detail-status.is-aged { color: var(--fg-dim); }
.detail-action { display: block; text-align: center; text-decoration: none; }

/* ── offers list (детальная карточка заявки) ─────────────────────────────────────────────── */
.detail-offers { margin-bottom: 15px; }

.detail-offers-heading { font-size: 13px; font-weight: 600; color: var(--fg-dim); margin-bottom: 10px; text-align: center; }

.offer-card {
  display: flex;
  gap: 10px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

.offer-card:last-child { border-bottom: none; }

.offer-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}

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

.offer-supplier-label { font-size: 12px; color: var(--fg-dim); margin-bottom: 2px; }

.offer-price-qty { font-size: 14.5px; font-weight: 600; margin-bottom: 4px; }

.offer-comment { font-size: 13px; color: var(--fg-dim); line-height: 1.4; margin-bottom: 8px; }

.quick-avatar {
  display: inline-block;
  position: relative;
  width: 18px;
  height: 18px;
  vertical-align: -4px;
  margin-right: 2px;
}

.quick-avatar-img {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  object-fit: cover;
  display: block;
}

.quick-avatar-icon {
  display: block;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  background: #17181a;
}

.my-request-wrap { display: flex; flex-direction: column; gap: 6px; }

.my-request-del {
  padding: 8px;
  border-radius: 10px;
  border: none;
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.my-request-del:disabled { opacity: .5; cursor: not-allowed; }

.name-field-wrap { position: relative; }

.name-field-wrap .profile-name-input { padding-right: 44px; }

/* Отцентрована трансформом - значит нуждается в той же защите от общего
   правила масштабирования кнопок, что стрелки и затвор. */
.name-save-check:not(:disabled):hover { transform: translateY(-50%) scale(1.02); }
.name-save-check:not(:disabled):active { transform: translateY(-50%) scale(0.94); }
.name-save-check {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border-radius: 9px;
  border: none;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: 15px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  line-height: 1;
}

.name-save-check:disabled { opacity: .55; cursor: default; }
.name-save-check.saved { background: var(--ok); }

.crop-actions { display: flex; gap: 8px; }
.crop-actions .topup-partner-btn { flex: 0 0 40%; margin: 0; }
.crop-actions .submit-btn { flex: 1; margin: 0; }

/* Пока открыто окно обрезки — страница под ним не прокручивается, иначе палец тянет и фото,
   и всё приложение сразу.

   touch-action: none здесь БЫЛО и ломало ползунок увеличения: Safari отдаёт перетаскивание
   бегунка <input type="range"> как обычный жест прокрутки, и запрет на всём body убивал его
   вместе с прокруткой — ползунок просто не двигался, увеличить фото было нечем. Прокрутку
   держит overflow: hidden, а жесты внутри кадра гасит сам кадр (.crop-viewport) и
   preventDefault в его обработчиках. */
body.crop-open { overflow: hidden; }

.crop-viewport {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 14px;
  background: #000;
  touch-action: none;
}

.crop-image {
  position: absolute;
  transform-origin: 0 0;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

.crop-frame {
  position: absolute;
  inset: 0;
  border: 2px solid rgba(255,255,255,.9);
  border-radius: 14px;
  pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(0,0,0,.28);
}

/* Ползунок увеличения — крупный и с собственным touch-action, чтобы палец попадал в бегунок
   с первого раза, а не таскал вместо него страницу. */
.crop-zoom {
  width: 100%;
  height: 34px;
  margin: 0;
  accent-color: var(--accent);
  touch-action: manipulation;
}

.crop-tip {
  margin: 10px 0 2px;
  font-size: 12.5px;
  color: var(--fg-dim);
  text-align: center;
}

.profile-head {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 14px;
  margin-bottom: 12px;
}

.profile-head-fields {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.profile-avatar-wrap {
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}

.profile-avatar {
  display: block;
  width: 84px;
  height: 84px;
  border-radius: 16px;
  object-fit: cover;
  background: var(--card);
}

.profile-avatar-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  border: 1px dashed var(--border);
}

.profile-name-input {
  width: 100%;
  padding: 11px 12px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 15px;
  font-weight: 600;
  font-family: inherit;
  outline: none;
}

.profile-name-input:focus { border-color: var(--accent); }

.points-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 11px 14px;
  border-radius: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
}

.points-main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.points-amount { font-weight: 700; font-size: 15px; white-space: nowrap; }
.points-amount.plus { color: var(--ok); }
.points-amount.minus { color: var(--danger); }
.points-reason { font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.points-source { font-size: 12.5px; font-weight: 600; color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.points-date { font-size: 11.5px; color: var(--fg-dim); }

.points-pager {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin-top: 12px;
}

.points-page-btn {
  min-width: 34px;
  padding: 7px 10px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.points-page-btn.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }

.broadcast-subscribe-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
}

.offer-photo-wrap { position: relative; width: 100%; max-width: 220px; margin: 8px 0; }

.offer-photo {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 12px;
}

/* «Открыть контакт» лежит на фотографии, снизу по центру: отдельной строкой
   кнопка забирала ряд у каждого предложения, а их под заявкой бывает с десяток.

   Тень снизу - не украшение: кнопка ложится на чужую фотографию, а там внизу
   может оказаться что угодно, от белой коробки до белого пола. Без затемнения
   белая кнопка на белом фоне пропадает. */
.offer-photo-wrap::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 42%;
  border-radius: 0 0 12px 12px; pointer-events: none;
  background: linear-gradient(to top, rgba(0,0,0,.42), transparent);
}
.offer-action-btn.offer-action-on-photo {
  position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%);
  z-index: 2; white-space: nowrap; max-width: calc(100% - 16px);
  box-shadow: 0 2px 8px rgba(0,0,0,.28);
}
/* И здесь то же: без этих двух строк кнопка на фотографии уезжала бы вправо
   при каждом нажатии. */
.offer-action-btn.offer-action-on-photo:not(:disabled):hover { transform: translateX(-50%) scale(1.02); }
.offer-action-btn.offer-action-on-photo:not(:disabled):active { transform: translateX(-50%) scale(0.94); }
.offer-action-btn.offer-action-on-photo:disabled { transform: translateX(-50%); }

.respond-camera-wrap { position: relative; }

.respond-video {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 14px;
  background: #000;
}

.respond-shutter {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  width: 62px;
  height: 62px;
  border-radius: 50%;
  border: 4px solid rgba(255,255,255,.85);
  background: rgba(255,255,255,.35);
  cursor: pointer;
  backdrop-filter: blur(4px);
}

.respond-shutter:active { background: rgba(255,255,255,.6); }

/* Затвор ПЕРЕСТАЁТ убегать от пальца. Общее правило для всех кнопок в начале
   файла ставит им transform: scale() при наведении и нажатии - и затирает
   translateX(-50%), которым затвор отцентрован. Кнопка прыгала вправо на
   половину своей ширины, а на отпускании возвращалась: ровно то самое
   «дёргается и убегает от мышки».

   Здесь центрирование и масштаб записаны вместе, одним transform - тогда
   затирать нечего. Тот же приём уже стоял у кнопки «переснять» ниже. */
.respond-shutter:not(:disabled):hover { transform: translateX(-50%) scale(1.02); }
.respond-shutter:not(:disabled):active { transform: translateX(-50%) scale(0.94); }

/* Крестик выхода из видоискателя. Тёмный, в правом верхнем углу - там же,
   где закрывается всё остальное в приложении. */
.camera-close {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  width: 32px; height: 32px; border-radius: 50%;
  border: none; background: rgba(0,0,0,.5); color: #fff;
  font-size: 15px; line-height: 1; cursor: pointer; font-family: inherit;
  display: flex; align-items: center; justify-content: center;
  backdrop-filter: blur(4px);
}

.offer-action-btn {
  padding: 8px 14px;
  border-radius: 999px;
  border: none;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.offer-action-btn:disabled { opacity: .5; cursor: not-allowed; }

.offer-action-btn.is-subscribed { background: var(--ok); color: #fff; }

.offer-contact {
  font-size: 13px;
  color: var(--ok);
  line-height: 1.6;
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border-radius: 10px;
  padding: 8px 10px;
}

.offer-hint { font-size: 12px; color: var(--danger); margin-top: 6px; }

.offer-confirm {
  padding: 10px 12px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
}

.offer-confirm-text { font-size: 13px; font-weight: 600; margin-bottom: 8px; }

.offer-confirm-btns { display: flex; gap: 8px; }

.offer-cancel-btn {
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg-dim);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}


/* Открытые контакты: фото тянется через Telegram-прокси и приезжает не мгновенно —
   вместо белой дыры показываем «мерцающий» плейсхолдер того же размера. */
.unlock-photo-wrap {
  width: 100%;
  max-width: 220px;
  aspect-ratio: 1 / 1;
  margin: 8px 0 0;
  border-radius: 12px;
  overflow: hidden;
  background: linear-gradient(90deg, #eef0f3 25%, #e2e5ea 37%, #eef0f3 63%);
  background-size: 400% 100%;
  animation: unlock-shimmer 1.3s ease-in-out infinite;
}

.unlock-photo-wrap.loaded {
  animation: none;
  background: none;
}

.unlock-photo-wrap .unlock-photo {
  width: 100%;
  height: 100%;
  max-width: none;
  margin: 0;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.unlock-photo-wrap.loaded .unlock-photo { opacity: 1; }

@keyframes unlock-shimmer {
  0% { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

.unlock-del { margin-top: 10px; width: 100%; }

/* Технические данные в карточке модерации — свёрнуты: нужны в одном случае из ста. */
.admin-tech { margin-top: 10px; }

.admin-tech summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted, #8a8f98);
  padding: 6px 0;
  list-style: none;
}

.admin-tech summary::-webkit-details-marker { display: none; }
.admin-tech summary::before { content: "▸ "; }
.admin-tech[open] summary::before { content: "▾ "; }
.admin-tech .admin-field { padding-top: 4px; word-break: break-word; }

/* Строки списков в админке — компактнее обычных карточек, их бывает много. */
.admin-row { text-align: left; }
.admin-row .earn-card-title { font-size: 14.5px; }
.admin-row .earn-card-desc { margin-top: 2px; }

/* Аватарка в админских списках и карточке. У большинства пользователей её нет — остаётся
   чёрный круг с первой буквой имени, чтобы список не выглядел рваным. */
.admin-avatar {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #111;
  color: #fff;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.admin-avatar-big { width: 56px; height: 56px; font-size: 22px; }

/* Фото лежит поверх буквы и проявляется, только когда действительно загрузилось. Так не
   бывает ни пустого круга на время загрузки, ни буквы поверх лица. */
.admin-avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity .18s ease;
}

.admin-avatar.has-photo .admin-avatar-img { opacity: 1; }
.admin-avatar.has-photo .admin-avatar-letter { visibility: hidden; }

.admin-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
}

.admin-row-text { min-width: 0; flex: 1; }

/* Строка списка — РОВНО две строки текста, ни больше. Раньше длинный хвост («поставщик:
   одобрен») переносился на третью строку у одних людей и не переносился у других, и список
   выглядел рваным: карточки разной высоты, аватарки на разных уровнях. Хвост, который не
   поместился, всё равно виден целиком в раскрытой карточке. */
.admin-row-text .earn-card-title,
.admin-row-text .earn-card-desc {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-person-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.admin-person-name { font-size: 17px; font-weight: 600; }

/* Поиск в админских списках — во всю ширину экрана: по нему водят пальцем и вводят длинные
   запросы, узкое нативное поле для этого не годится. */
.search-field { margin-bottom: 14px; }

.search-field input[type="search"] {
  width: 100%;
  max-height: none;
  height: 48px;
  padding: 12px 14px;
  font-size: 15px;
  -webkit-appearance: none;
  appearance: none;
}

.search-field input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

/* «Мои сообщения» — крупная кнопка сразу под шапкой профиля. */
.profile-messages-btn { margin: 4px 0 16px; }

/* Шапка экрана: «Назад» слева, действие справа. */
.screen-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 14px;
}

.screen-topbar .back-btn { margin: 0; }

.topbar-action {
  padding: 9px 14px;
  border: none;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.topbar-action:active { transform: scale(.97); }

/* Компактная шапка профиля: кнопка сообщений стоит справа от аватарки, над именем —
   не наезжает на фото и не съедает отдельную полосу экрана. */
.profile-head-fields {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  flex: 1;
}

.profile-messages-btn {
  width: 100%;
  padding: 10px 12px;
  border: none;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  font-size: 14.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.profile-messages-btn:active { transform: scale(.98); }

.profile-delete-btn {
  width: 100%;
  padding: 12px;
  margin: 4px 0;
  border: none;
  border-radius: 10px;
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
  font-size: 14.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.profile-delete-btn:disabled { opacity: .5; }

/* Карточки профиля: только заголовок и число, по центру. Пояснение и «Открыть →» убраны —
   вся карточка и так кликабельна, а три строки текста на каждой съедали целый экран. */
/* Плитки профиля - те же, что на главной: по две в ряд. Карточки во всю ширину
   занимали два экрана ради пяти строк текста, и до нижней приходилось листать. */
.profile-grid { margin-bottom: 16px; }
.profile-tile { min-height: 104px; justify-content: flex-start; gap: 6px; }
.profile-tile .home-tile-icon { margin-bottom: 2px; }
.profile-tile .home-tile-sub { margin-top: auto; font-size: 13px; }

/* Значок статуса поставщика в профиле. */
.supplier-badge {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  padding: 5px 11px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 600;
}

.supplier-badge-ok {
  background: var(--accent);
  color: #fff;
}

.supplier-badge-pending {
  background: #eceef1;
  color: var(--fg-dim);
}

/* Значок верификации покупателя. Красный — приглашение подтвердить номер одним тапом,
   зелёный — подтверждено. Синий остаётся за статусом поставщика. */
.supplier-badge {
  border: none;
  font-family: inherit;
  cursor: default;
}

.supplier-badge-verified {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  color: var(--ok);
}

.supplier-badge-unverified {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  cursor: pointer;
}

.supplier-badge-unverified:active { transform: scale(.97); }
.supplier-badge:disabled { opacity: .6; }

/* Имя и значок верификации в одной строке: значок справа, имя занимает остальное. */
.profile-name-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.profile-name-row .name-field-wrap { flex: 1; min-width: 0; }
.profile-name-row .supplier-badge { flex: 0 0 auto; align-self: center; }

/* Подтверждённый статус — сплошная зелёная плашка, а не бледная. */
.supplier-badge-verified {
  background: var(--ok);
  color: #fff;
}

/* Значок рядом с длинным именем: сам не сжимается и не переносится, а поле имени уступает
   ему место. Иначе строка разъезжается на две и съедает высоту экрана. */
.profile-name-row .supplier-badge {
  white-space: nowrap;
  padding: 5px 9px;
  font-size: 12px;
}

.profile-name-row .name-field-wrap { min-width: 80px; }
.profile-name-row .profile-name-input { min-width: 0; }

/* Затвор — красный круг, как у любой камеры: понятен без подписи. */
.respond-shutter {
  border-color: rgba(255, 255, 255, .9);
  background: #e5484d;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}

.respond-shutter:active { background: #c93b3f; }

/* После съёмки на том же месте — тот же круг со стрелкой «переснять». Отдельная полоса
   во всю ширину под фото занимала целый экран и выглядела как чужеродная кнопка. */
.shutter-retake {
  background: rgba(20, 20, 20, .6);
  color: #fff;
  font-size: 26px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
}

.shutter-retake:active { background: rgba(20, 20, 20, .8); }

/* ── Съёмка: вспышка затвора и появление кадра ──────────────────────────────────────────────
   Между «нажал» и «вот фото» раньше не было ничего — кадр просто подменялся, и момент съёмки
   не читался вообще. Три коротких эффекта: затвор проседает под пальцем, экран моргает белым,
   снимок въезжает с лёгким увеличением. Всё до 400 мс, чтобы не мешать быстрой работе. */
.respond-shutter {
  transition: transform 0.12s ease, background 0.15s ease;
}

.respond-shutter:not(:disabled):active { transform: translateX(-50%) scale(0.88); }

.camera-flash::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 14px;
  background: #fff;
  animation: camera-flash 0.32s ease-out forwards;
  pointer-events: none;
}

@keyframes camera-flash {
  0% { opacity: 0.95; }
  100% { opacity: 0; }
}

.shot-appear { animation: shot-appear 0.34s cubic-bezier(0.2, 0.9, 0.3, 1); }

@keyframes shot-appear {
  0% { opacity: 0; transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}

/* Кнопка «переснять» появляется с задержкой — сначала человек видит кадр, потом действие. */
.shutter-retake { animation: retake-in 0.3s ease-out 0.18s both; }

@keyframes retake-in {
  0% { opacity: 0; transform: translateX(-50%) scale(0.6) rotate(-90deg); }
  100% { opacity: 1; transform: translateX(-50%) scale(1) rotate(0); }
}

.shutter-retake:not(:disabled):active { transform: translateX(-50%) scale(0.88) rotate(-40deg); }

/* Над видоискателем системные жесты масштабирования и прокрутки не работают — щипок здесь
   принадлежит камере, а не странице. */
.respond-camera-wrap {
  /* pan-y, а НЕ none: одним пальцем страница должна листаться как обычно — иначе после съёмки
     человек упирается в снимок и не может прокрутить форму дальше. Щипок двумя пальцами
     перехватывается в коде и уходит в зум камеры. */
  touch-action: pan-y;
  overflow: hidden;
  border-radius: 14px;
}

/* Готовый снимок зумить нечего — на нём вообще никаких ограничений. */
#catalogPhotoPreviewWrap { touch-action: auto; }

.respond-camera-wrap .respond-video {
  transform-origin: center center;
  transition: transform 0.08s linear;
}

/* Потягивание за край страницы больше не «резинит» и не отдаёт жест окну Telegram —
   иначе на верхней границе списка свайп вниз закрывал приложение. */
html, body {
  overscroll-behavior: none;
  overscroll-behavior-y: none;
}

/* Вопросы под товаром: ответ визуально вложен, чтобы ветка читалась с одного взгляда. */
.comment-reply { margin-left: 20px; border-left: 3px solid var(--accent); }

.comment-tag {
  font-size: 10.5px;
  padding: 2px 7px;
  vertical-align: middle;
}

.comment-reply-btn {
  margin-top: 8px;
  padding: 6px 14px;
  font-size: 13px;
}

#productComments .earn-card { margin-bottom: 8px; }
#productBody .respond-video { max-height: 320px; object-fit: cover; border-radius: 14px; }

/* «Мои товары» — плитки с фото. Кнопки внутри плитки компактные, иначе одна карточка
   занимает пол-экрана и десять товаров невозможно окинуть взглядом. */
.my-tile { display: flex; flex-direction: column; }
.my-tile .tile-body { flex: 1; }

.tile-actions {
  padding: 0 10px 10px;
  gap: 6px;
  margin-top: 0;
}

.tile-actions button {
  padding: 7px 6px;
  font-size: 12px;
  border-radius: 8px;
}

.tile-hidden { color: var(--danger); }

/* Состояние товара в «Моих товарах»: ожидание, отказ и публикация различаются цветом —
   поставщик понимает ситуацию, не читая текст целиком. */
/* Было золото #b8860b - тот же жёлтый, только в мелком тексте. Терракота читается как
   «внимание» не хуже, но не спорит с зелёным и не выглядит выцветшей. */
.tile-pending { color: #9A6248; font-weight: 600; }
.tile-live { color: var(--ok); font-weight: 600; }

/* Кнопка «Профиль»: аватарка вдвое крупнее и прижата к левому краю кнопки, а подпись
   занимает остальное. Мелкий значок посреди текста читался как иконка, а не как фото
   человека — теперь сразу видно, чей это профиль. */
.quick-btn[data-quick="profile"] {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-left: 8px;
  padding-right: 10px;
  text-align: left;
}

/* Профилю столбец шире остальных двух. В равных третях его содержимое -
   аватар 36px, отступ и слово «Профиль» - занимало почти всю ширину, и слово
   упиралось в край кнопки. У «Ленты» и «Новинок» вместо аватара значок 16px,
   и запас ширины у них лишний: отдаём его туда, где не хватает.

   1.3fr, а не auto по содержимому: столбцы должны стоять на месте при любой
   длине подписи, иначе смена аватара или слова двигала бы соседние кнопки. */
/* Две кнопки вместо трёх - «Новинки» убраны. Столбцы теперь равные: у профиля
   аватар 36px, у витрины значок 16px, но на половине экрана места хватает
   обоим с запасом, и перекос долями стал бы заметен как кривизна. */
.home-quickbar { grid-template-columns: 1fr 1fr; }

.quick-btn[data-quick="profile"] .quick-avatar {
  width: 36px;
  height: 36px;
  flex: 0 0 auto;
  vertical-align: middle;
  margin-right: 0;
}

.quick-btn[data-quick="profile"] .quick-avatar-img,
.quick-btn[data-quick="profile"] .quick-avatar-icon {
  width: 36px;
  height: 36px;
  border-radius: 9px;
}

/* Цены услуг: название и пояснение сверху, поле с кнопкой — снизу. Кнопка «Сохранить»
   неактивна, пока число не изменилось: случайный тап не должен менять деньги. */
.price-row { display: flex; flex-direction: column; gap: 10px; }
.price-info .earn-card-desc { margin-top: 2px; }
.price-info .tile-line { font-size: 11.5px; opacity: .7; }

.price-edit { display: flex; gap: 8px; align-items: center; }

.price-input {
  flex: 1;
  max-height: 42px;
  padding: 10px 12px;
  font-size: 15px;
  font-weight: 600;
}

.price-edit .admin-approve-btn { flex: 0 0 auto; padding: 10px 16px; }
.price-edit .admin-approve-btn:disabled { opacity: .4; }

.blocked-mark { color: var(--danger); }

/* Счётчик в карточках админки — по левому краю, в одну линию с заголовком и описанием:
   по центру он выбивался из общей колонки текста. */
#adminView .earn-card-amount,
.admin-menu .earn-card-amount { text-align: left; }

/* Имя в профиле: пока не трогали — обычный серый текст. Открытое поле ввода на видном месте
   провоцирует «а что сюда вписать?», хотя имя меняют раз в жизни. */
.profile-name-input {
  color: var(--fg-dim);
  background: transparent;
  border-color: transparent;
  transition: color .15s, background .15s, border-color .15s;
}

.profile-name-input.editing,
.profile-name-input:focus {
  color: var(--fg);
  background: var(--card);
  border-color: var(--border, #e3e6ea);
}

/* Свой лист действий — не зависит от версии Telegram и не ограничен тремя кнопками. */
.sheet-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .4);
  display: flex;
  align-items: flex-end;
  z-index: 60;
}

.sheet {
  width: 100%;
  background: #fff;
  border-radius: 18px 18px 0 0;
  padding: 14px 14px calc(14px + env(safe-area-inset-bottom));
  animation: sheet-up .2s ease-out;
}

@keyframes sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }

.sheet-title { font-size: 13px; color: var(--fg-dim); text-align: center; margin-bottom: 10px; }

.sheet-btn {
  width: 100%;
  padding: 14px;
  margin-bottom: 8px;
  border: none;
  border-radius: 12px;
  background: var(--card);
  color: var(--fg);
  font-size: 15.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.sheet-danger { color: var(--danger); }
.sheet-cancel { margin-bottom: 0; color: var(--fg-dim); }

/* Фото на модерации во весь экран: по превью не видно ни бирки, ни качества. */
.photo-zoom {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .92);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 70;
  cursor: zoom-out;
}

.photo-zoom img { max-width: 100%; max-height: 100%; object-fit: contain; }

/* Раскрывающаяся строка в списках админки. */
.person-wrap { margin-bottom: 8px; }
.person-wrap .admin-row { margin-bottom: 0; width: 100%; }

.person-chevron { flex: 0 0 auto; color: var(--fg-dim); font-size: 13px; }

.person-details {
  padding: 12px 14px 14px;
  background: var(--card);
  border-radius: 0 0 14px 14px;
  margin-top: -6px;
}

.person-supplier { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e9ecef; }

/* Заголовок и действие в одной строке: отдельная полоса во всю ширину под кнопку
   «Заблокированные» съедала экран ради того, что нужно раз в неделю. */
.heading-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.heading-row .projects-heading { margin: 0; }

/* Два действия справа от заголовка. На узком экране они переносятся под него целой
   группой, а не разваливаются по одной кнопке в строку. */
.heading-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.heading-row { flex-wrap: wrap; }

.heading-action {
  flex: 0 0 auto;
  padding: 7px 12px;
  border: none;
  border-radius: 999px;
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  white-space: nowrap;
  cursor: pointer;
}

.heading-action:active { transform: scale(.97); }

/* «Онлайн» — зелёная, в тон точке на аватарках: один и тот же смысл должен иметь один цвет. */
.heading-action-ok { background: color-mix(in srgb, #22c55e 15%, transparent); color: #15803d; }

/* Пульс только пока кнопка действительно зовёт в список онлайна. Когда она переключилась в
   «Все · N», зелёное мигание врало бы: живого счётчика за ней в этот момент нет. */
@keyframes heading-ok-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
  50%      { box-shadow: 0 0 0 5px rgba(34, 197, 94, .20); }
}

.heading-action-ok.is-live { animation: heading-ok-pulse 2.2s ease-in-out infinite; }

/* Уважаем системную настройку «уменьшить движение»: постоянное мигание на экране — первое,
   что мешает людям, которые её включили. Цвет при этом остаётся, смысл не теряется. */
@media (prefers-reduced-motion: reduce) {
  .ava-dot-on,
  .heading-action-ok.is-live { animation: none; }
  .ava-dot-on { box-shadow: 0 0 0 1px rgba(34, 197, 94, .35); }
}

/* Просмотр аватарки: фото по центру, действия поверх него снизу. */
.avatar-viewer-img {
  max-width: 86%;
  max-height: 62%;
  object-fit: contain;
  border-radius: 16px;
}

.avatar-viewer-empty {
  color: rgba(255, 255, 255, .7);
  font-size: 15px;
}

.avatar-viewer-actions {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: calc(20px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 8px;
  cursor: default;
}

.viewer-btn {
  width: 100%;
  padding: 14px;
  border: none;
  border-radius: 12px;
  background: rgba(255, 255, 255, .95);
  color: #17181a;
  font-size: 15.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  backdrop-filter: blur(8px);
}

.viewer-btn:active { transform: scale(.98); }
.viewer-danger { color: var(--danger); }

/* Отступ под строкой заголовка: кнопка «Заблокированные» упиралась в поле поиска. */
.heading-row { margin-bottom: 14px; }

/* Карточка товара: название, цена и категория одной строкой. */
.product-head {
  font-size: 17px;
  line-height: 1.45;
  margin-top: 12px;
}

.product-name { font-weight: 700; }
.product-price { font-weight: 600; }
.product-sep { color: var(--fg-dim); margin: 0 2px; }
.product-cat { font-size: 15px; color: var(--fg-dim); }

.product-desc { margin-top: 8px; font-size: 14.5px; color: var(--fg-dim); line-height: 1.5; }

/* Аватарка с галочкой подтверждения — как в мессенджерах: значок в правом нижнем углу. */
.ava-box { position: relative; flex: 0 0 auto; }

.ava-verified {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ok);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #fff;
}

.ava-verified-big { width: 22px; height: 22px; font-size: 13px; }

/* Статусы человека — плашками сверху карточки, чтобы считывались без чтения полей. */
.person-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }

.tag {
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  white-space: nowrap;
}

.tag-ok { background: color-mix(in srgb, var(--ok) 16%, transparent); color: var(--ok); }
.tag-warn { background: #F3ECE8; color: #9A6248; }
.tag-blue { background: var(--accent); color: #fff; }
.tag-grey { background: #eceef1; color: var(--fg-dim); }
.tag-danger { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }

.person-handle { font-size: 13.5px; font-weight: 500; color: var(--fg-dim); margin-top: 2px; }

/* Значок состояния — в правом верхнем углу аватарки, как в мессенджерах. Он вынесен ЗА
   границу фото, поэтому не накладывается на лицо, что бы человек ни загрузил. */
.ava-mark {
  position: absolute;
  right: -3px;
  top: -3px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 999px;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  border: 2px solid #fff;
  box-sizing: border-box;
}

.ava-mark-big { min-width: 22px; height: 22px; font-size: 13px; line-height: 18px; top: -4px; right: -4px; }

/* Коробка аватарки по размеру самой аватарки, без внутренних отступов: любой отступ здесь
   сдвигал фото вниз и вправо относительно текста, и строка переставала быть ровной. Значки
   выступают за край на пару пикселей — запаса внутри карточки (16-18px) для этого хватает. */
.ava-box { line-height: 0; }

/* Точка «онлайн/офлайн» — ВВЕРХУ СЛЕВА, напротив метки верификации, чтобы два значка
   никогда не наезжали друг на друга. Ярко-зелёная = заходил в последние 15 минут. */
.ava-dot {
  position: absolute;
  left: -2px;
  top: -2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-sizing: border-box;
  z-index: 1;
}

.ava-dot-big { width: 17px; height: 17px; left: -3px; top: -3px; }

/* Зелёный берём ярче системного --ok: точка маленькая, приглушённый цвет на ней не читается.
   Пульсирующее кольцо расходится и гаснет — так точка читается как «живой прямо сейчас», а не
   как ещё одна нарисованная метка. Мигание включено ТОЛЬКО у зелёной: серая офлайн-точка
   должна оставаться неподвижной, иначе в списке рябит от движения на каждой строке. */
@keyframes ava-dot-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(34, 197, 94, .55); }
  70%  { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}

.ava-dot-on {
  background: #22c55e;
  animation: ava-dot-pulse 2.2s cubic-bezier(.4, 0, .2, 1) infinite;
}

.ava-dot-off { background: #b6bcc4; }

/* На своей аватарке в профиле — квадрат со скруглением, а не круг, поэтому точку смещаем
   внутрь угла, иначе она висит в воздухе. */
.profile-avatar-wrap { position: relative; }
.ava-dot-square { left: 4px; top: 4px; width: 16px; height: 16px; }

.ava-mark-ok { background: var(--ok); }
.ava-mark-supplier { background: var(--accent); }
.ava-mark-warn { background: var(--danger); }

/* Просмотр аватарки: кнопки лежат НА фото, тремя квадратами внизу снимка. */
.avatar-viewer-frame {
  position: relative;
  max-width: 90%;
  max-height: 78%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.avatar-viewer-frame .avatar-viewer-img {
  max-width: 100%;
  max-height: 100%;
  border-radius: 16px;
  display: block;
}

.avatar-viewer-actions {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: 8px;
  cursor: default;
}

.viewer-tile {
  padding: 12px 6px;
  border: none;
  border-radius: 12px;
  background: rgba(255, 255, 255, .92);
  color: #17181a;
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  line-height: 1.25;
  cursor: pointer;
  backdrop-filter: blur(8px);
}

.viewer-tile:active { transform: scale(.96); }
.viewer-tile.viewer-danger { color: var(--danger); }

/* Обсуждение под товаром: аватарка автора со значком статуса рядом с именем. */
.comment-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.comment-who { min-width: 0; font-size: 14.5px; }
.comment-tag { font-size: 10.5px; padding: 2px 7px; vertical-align: middle; }
.comment-text { font-size: 14.5px; line-height: 1.5; overflow-wrap: anywhere; }

/* Технические данные: обведённый блок, значения синим и копируются тапом. */
.admin-tech[open] {
  border: 1px solid #dfe3e8;
  border-radius: 12px;
  padding: 4px 12px 10px;
  background: #fbfcfd;
}

.admin-tech[open] summary { color: var(--fg); }

.tech-body { margin-top: 4px; }

.tech-copy {
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
  border-bottom: 1px dashed color-mix(in srgb, var(--accent) 45%, transparent);
}

.tech-copy:active { opacity: .6; }

/* Рекламный баннер вверху главной. Пустое место продаёт себя само: показывает вчерашнюю
   посещаемость и цену, посчитанную от неё. */
.ad-banner {
  display: block;
  width: 100%;
  padding: 14px 16px;
  margin-bottom: 14px;
  border: none;
  border-radius: 14px;
  background: linear-gradient(135deg, #1f6feb, #3b8bff);
  color: #fff;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}

.ad-banner:active { transform: scale(.99); }

.ad-banner-empty {
  background: repeating-linear-gradient(135deg, #f3f5f8, #f3f5f8 10px, #eef1f5 10px, #eef1f5 20px);
  color: var(--fg);
  border: 1px dashed #c9d1db;
}

.ad-banner-title { font-size: 15.5px; font-weight: 700; letter-spacing: .2px; }
.ad-banner-sub { font-size: 12.5px; margin-top: 4px; opacity: .9; line-height: 1.4; }

/* Баннер первым экраном: его видит каждый вошедший, поэтому он должен цеплять взгляд,
   но не мешать — движение медленное и не дёргает интерфейс. */
.ad-banner {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  margin-bottom: 10px;
  border: none;
  border-radius: 14px;
  overflow: hidden;
  font-family: inherit;
  cursor: pointer;
  background: linear-gradient(120deg, #1f6feb, #7b5cff, #1f6feb);
  background-size: 220% 100%;
  animation: ad-flow 7s ease-in-out infinite;
  box-shadow: 0 4px 16px rgba(31, 111, 235, .28);
}

@keyframes ad-flow {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* Блик, проезжающий по баннеру раз в несколько секунд. */
.ad-banner-glow {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 40%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .28), transparent);
  animation: ad-shine 4.5s ease-in-out infinite;
  pointer-events: none;
}

@keyframes ad-shine {
  0% { left: -45%; }
  55% { left: 105%; }
  100% { left: 105%; }
}

.ad-banner-body { position: relative; display: block; padding: 13px 16px; text-align: center; }
.ad-banner-title { display: block; font-size: 15px; font-weight: 800; letter-spacing: .6px; color: #fff; }
.ad-banner-sub { display: block; font-size: 12px; margin-top: 3px; color: rgba(255, 255, 255, .92); line-height: 1.35; }

.ad-banner:active { transform: scale(.99); }

/* Купленная реклама — спокойнее: она уже продана, привлекать к месту внимание незачем. */
.ad-banner.ad-banner-sold { animation: none; background: linear-gradient(120deg, #16324f, #1f4d72); }
.ad-banner.ad-banner-sold .ad-banner-glow { display: none; }

/* Баннер — крупный первый экран: он продаёт место, и мелким его делать бессмысленно. */
.ad-banner { margin-bottom: 0; border-radius: 16px; }
.ad-banner-body { padding: 22px 18px; }
.ad-banner-title { font-size: 19px; letter-spacing: 1px; }
.ad-banner-sub { font-size: 13px; margin-top: 6px; }

/* Разделитель между рекламой и содержимым шапки: рекламу нельзя путать с интерфейсом. */
.topbar-divider {
  height: 1px;
  background: #e6e9ee;
  margin: 12px 0 10px;
}

/* «Зарабатывай с нами» переехала вниз главной, над «Предложите идею». */
.home-earn-bottom { margin: 4px 0 10px; }

/* ── Баннер: деловой прямоугольник ─────────────────────────────────────────────────────────
   Скруглений почти нет, цвет строгий, движение сдержанное: это рекламное место для оптовиков,
   а не игровой промо-блок. Кричащий градиент здесь работает против доверия. */
.ad-banner {
  border-radius: 6px;
  margin: 0;
  background: linear-gradient(100deg, #16202e 0%, #1d2c40 45%, #16202e 100%);
  background-size: 200% 100%;
  animation: ad-flow 12s linear infinite;
  box-shadow: none;
  border: 1px solid #24334a;
}

.ad-banner-body { padding: 18px 18px; text-align: left; }

.ad-banner-title {
  display: block;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: #eaf1ff;
}

.ad-banner-sub {
  display: block;
  font-size: 12.5px;
  margin-top: 6px;
  color: #9fb3d0;
  line-height: 1.45;
}

/* Блик медленный и слабый — обозначает, что место живое, но не мигает в лицо. */
.ad-banner-glow {
  width: 30%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .10), transparent);
  animation: ad-shine 9s ease-in-out infinite;
}

.topbar-divider { margin: 14px 0 12px; }

/* Экран предложения рекламы. */
.pitch-hero {
  text-align: center;
  padding: 18px 12px 14px;
  background: #16202e;
  border-radius: 6px;
  margin-bottom: 14px;
}

.pitch-price { font-size: 34px; font-weight: 800; color: #fff; line-height: 1; }
.pitch-price-note { font-size: 12.5px; color: #9fb3d0; margin-top: 6px; }

/* Расписание рекламы: сетка суток, свободные кликабельны. */
.schedule-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }

.schedule-cell {
  padding: 10px 4px;
  border: 1px solid #dfe3e8;
  border-radius: 8px;
  background: #fff;
  font-family: inherit;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.schedule-cell.schedule-busy { background: #f3f5f8; color: var(--fg-dim); cursor: default; }
.schedule-date { font-size: 14px; font-weight: 700; }
.schedule-state { font-size: 11px; color: var(--fg-dim); }
.schedule-cell:not(.schedule-busy) .schedule-state { color: var(--accent); font-weight: 600; }

.pitch-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }

/* Предпросмотр поста — тот же вид, что будет на главной. */
.ad-preview {
  margin: 6px 0 14px;
  padding: 16px;
  border-radius: 6px;
  background: linear-gradient(100deg, #16202e, #1d2c40);
  border: 1px solid #24334a;
}

.ad-preview-title { font-size: 14px; font-weight: 700; letter-spacing: 1.4px; color: #eaf1ff; }
.ad-preview-sub { font-size: 12.5px; margin-top: 6px; color: #9fb3d0; }

/* Анимированный фон убран: он осветлил подложку, и карточки на ней перестали читаться —
   белые плитки слились с фоном. Контраст между содержимым и фоном важнее эффекта. */

/* Модальные окна по центру экрана: раздел не должен уезжать вниз, теряя место прокрутки. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  z-index: 80;
}

.modal-card {
  width: 100%;
  max-width: 460px;
  max-height: 82vh;
  overflow-y: auto;
  background: #fff;
  border-radius: 16px;
  padding: 16px;
  animation: modal-in .18s ease-out;
}

@keyframes modal-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: scale(1); } }

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 15px;
  font-weight: 700;
  margin-bottom: 12px;
}

.modal-close {
  border: none;
  background: none;
  font-size: 18px;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 4px 8px;
}

/* Картинка в баннере — фоном под затемнением, текст читается поверх. */
/* Баннер с фото получает ЖЁСТКУЮ форму. Без неё высота тянулась за длиной текста: два
   слова - один баннер, четыре - другой, и кадр, выбранный в обрезке, никогда не совпадал
   с тем, что показано. Теперь форма постоянна и равна той, под которую кадрируют. */
.ad-banner-photo {
  background-size: cover;
  background-position: center;
  aspect-ratio: 3.2 / 1;
  display: flex;
  align-items: flex-end;
}

.ad-banner-photo .ad-banner-body { width: 100%; }
.ad-banner-photo .ad-banner-body {
  background: linear-gradient(180deg, rgba(10, 16, 26, .10), rgba(10, 16, 26, .78));
  width: 100%;
}

/* Тень под текстом. Затемнения снизу мало: если человек загрузит светлый снимок, белые
   буквы на нём растворятся именно там, где градиент ещё прозрачный. Тень работает на
   любом фоне и не зависит от того, что именно на фотографии. */
/* ОБВОДКА по буквам, а не только тень. Тень размывается и на пёстрой картинке всё равно
   даёт слияние; тонкий тёмный контур отделяет букву от фона при любом снимке - именно это
   и не давало читать заголовок поверх логотипа.

   paint-order кладёт обводку ПОД заливку буквы: без него контур наползает на саму букву
   и съедает её тонкие части. */
.ad-banner-photo .ad-banner-title,
.ad-banner-photo .ad-banner-sub {
  -webkit-text-stroke: var(--ad-stroke, 0.6px) rgba(0, 0, 0, .55);
  paint-order: stroke fill;
}
/* Саму тень собирает скрипт и кладёт готовой строкой: у объёмных букв это не одна тень, а
   стопка слоёв со сдвигом на пиксель, и посчитать её в стилях нечем. */
.ad-banner-photo .ad-banner-title { text-shadow: var(--ad-shadow, 0 1px 2px rgba(0, 0, 0, .6), 0 2px 12px rgba(0, 0, 0, .45)); }
.ad-banner-photo .ad-banner-sub   { text-shadow: var(--ad-shadow-sub, 0 1px 2px rgba(0, 0, 0, .6)); }

.ad-banner-photo .ad-banner-sub { -webkit-text-stroke-width: calc(var(--ad-stroke, 0.6px) * .66); }

.ad-preview { overflow: hidden; padding: 0; }
.ad-preview-img { width: 100%; max-height: 140px; object-fit: cover; display: block; }
.ad-preview-text { padding: 14px 16px; }

/* Живой график за сутки: три линии, без внешних библиотек. */
/* Боковые поля срезаны: график тянется во всю ширину карточки, а заголовок и легенда
   получают свой отступ отдельно - иначе ради двух текстовых строк терялась ширина. */
.chart-card {
  background: #fff;
  border: 1px solid #e6e9ee;
  border-radius: 12px;
  padding: 12px 4px 10px;
  margin-bottom: 12px;
}

.chart-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--fg-dim);
  margin-bottom: 6px;
}

.chart-live { display: inline-flex; align-items: center; gap: 5px; color: var(--ok); }

.chart-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ok);
  animation: chart-pulse 1.8s ease-in-out infinite;
}

@keyframes chart-pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

.chart-box { width: 100%; }
.chart-max { font-size: 11px; color: var(--fg-dim); text-align: right; margin-top: 2px; }

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 11.5px;
  color: var(--fg-dim);
  margin-top: 6px;
}

.chart-legend .dot { display: inline-block; width: 9px; height: 3px; border-radius: 2px; margin-right: 4px; vertical-align: middle; }
/* Просмотры красным пунктиром, переходы синей сплошной - как на графике. */
.dot-visits { background: repeating-linear-gradient(90deg, #e5484d 0 2px, transparent 2px 5px); }
/* .dot-views удалён. Класса с таким именем в разметке нет - легенда рисуется через
   .dot-visits, - а правила остались с прежней версии графика и держали единственный
   оставшийся жёлтый в приложении, причём цвет уже не совпадал с линией: просмотры на
   графике красные. Мёртвые правила не «на всякий случай», они сбивают при следующей правке.
   Переходы - СПЛОШНОЙ синей, как линия на графике. */
.dot-clicks { background: #2563eb; }

/* Подсказка под графиком: значения того часа, которого коснулись. */
/* Подсказка лежит В ЛЕВОМ ВЕРХНЕМ УГЛУ САМОГО графика, а не строкой над ним.
   Держа палец на линии, человек смотрит на график, а не поверх него - и число
   должно быть там же, куда направлен взгляд. Угол выбран левый верхний:
   правый занят отметкой «онлайн», а низ - подписями дат.

   Поверх линий, но сквозная для касаний: иначе она перехватывала бы палец
   у самого края, где как раз лежат первые точки. */
.chart-tip {
  position: absolute; left: 6px; top: 4px; z-index: 2;
  max-width: calc(100% - 12px);
  font-size: 12.5px; line-height: 1.35;
  color: var(--fg);
  text-align: left;
  opacity: 0;
  transition: opacity .15s;
  padding: 4px 9px;
  border-radius: 10px;
  background: rgba(255, 255, 255, .88);
  backdrop-filter: blur(4px);
  pointer-events: none;
}

.chart-tip.visible { opacity: 1; }
/* Рамка для угловой подсказки: без относительного положения она уехала бы
   в угол всей страницы. */
#adChart { position: relative; }
.chart-card { padding: 12px 4px 10px; }

/* Выбор даты внутри формы поста. */
.schedule-compact { grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 8px; }
.schedule-compact .schedule-cell { padding: 8px 2px; }
.schedule-picked { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, #fff); }

/* Пояснения к графику и цене — раскрываются, а не кричат сразу. */
.chart-help { margin-top: 8px; }
.chart-help summary, .price-help summary { font-size: 12.5px; }
.price-help { margin-bottom: 14px; }
.price-help[open] { background: #fbfcfd; }

/* Сводка над графиком: при перетаскивании пальцем низ карточки закрыт рукой. */
/* Подсказка на графике рекламы - в том же ключе, что и сама карточка:
   светлая, со скруглением карточки и её же тенью. Она часть графика,
   а не всплывающее окно поверх него. */
#adChart .chart-tip {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 2px 10px rgba(20, 24, 28, .07);
  padding: 6px 10px;
  font-size: 12px;
  white-space: nowrap;
}
/* .dot-peak убран вместе с лентой активных часов - см. drawChart. */

/* Крестик закрытия — в рамке: без неё он читается как часть заголовка, а не как кнопка. */
.modal-close {
  width: 30px;
  height: 30px;
  border: 1px solid #dfe3e8;
  border-radius: 8px;
  background: #fff;
  line-height: 1;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-close:active { background: #f1f3f6; }

/* Услуга без заданной цены — приглушённое поле с подсказкой. */
.price-input::placeholder { color: #b6bcc5; font-weight: 500; }

/* Крестик закрытия — в рамке: без неё он читается как значок, а не как кнопка. */
.modal-close {
  width: 32px;
  height: 32px;
  border: 1px solid #dfe3e8;
  border-radius: 8px;
  background: #fff;
  font-size: 15px;
  line-height: 1;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-close:active { background: #f3f5f8; }

/* ── Экран сбоя запуска ────────────────────────────────────────────────────────────────────
   Показывается поверх всего, когда приложение не смогло запуститься. Своя вёрстка, не
   зависящая ни от одного класса приложения: если сломался app.js, полагаться на его разметку
   нельзя, а человек не должен смотреть в пустое окно. */
.boot-failure {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #fff;
  padding: 28px 20px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: #101828;
}

.boot-failure-title { font-size: 19px; font-weight: 700; }
.boot-failure-text { font-size: 14.5px; color: #667085; line-height: 1.5; }

.boot-failure-tech {
  margin: 0;
  padding: 12px 14px;
  border-radius: 12px;
  background: #f5f6f8;
  color: #475467;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}

.boot-failure-btn {
  padding: 14px 18px;
  border: none;
  border-radius: 14px;
  background: #2f80ed;
  color: #fff;
  font-size: 16px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

/* Журнал сбоев запуска в админке — моноширинный, читается как лог, а не как карточка. */
.boot-log-item { margin-bottom: 8px; }
.boot-log-head { font-size: 14px; font-weight: 600; }
.boot-log-meta { margin-top: 4px; font-size: 12.5px; color: var(--fg-dim); line-height: 1.6; word-break: break-word; }

/* ── Мои размещения (реклама) ──────────────────────────────────────────────────────────────
   Карточка отвечает на три вопроса подряд: что вышло, сколько это дало, в какие часы. */
.ad-mine-card { margin-bottom: 12px; }

.ad-mine-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 10px;
}

/* Превью — тот же тёмный баннер, что стоит на первом экране: человек должен увидеть ровно
   то, что видели люди, а не пересказ своими словами. */
.ad-mine-preview {
  position: relative;
  border-radius: 14px;
  padding: 16px 18px;
  background: #121a26;
  background-size: cover;
  background-position: center;
  min-height: 76px;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.ad-mine-preview.has-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(10, 14, 20, .88), rgba(10, 14, 20, .55));
}

.ad-mine-preview-body { position: relative; z-index: 1; }
.ad-mine-preview-title { color: #fff; font-size: 15px; font-weight: 700; letter-spacing: .3px; }
.ad-mine-preview-sub { margin-top: 4px; color: rgba(255, 255, 255, .72); font-size: 13px; line-height: 1.45; }

.ad-mine-stats { display: flex; gap: 8px; margin-top: 12px; }

.ad-mine-stat {
  flex: 1;
  padding: 10px 6px;
  border-radius: 12px;
  background: #f5f6f8;
  text-align: center;
}

.ad-mine-stat b { display: block; font-size: 18px; }
.ad-mine-stat span { font-size: 11.5px; color: var(--fg-dim); }

/* Часовая полоса: высота столбика — просмотры, тёмная часть снизу — переходы. */
.ad-hours { margin-top: 14px; }

.ad-hours-bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 54px;
}

.ad-hours-col { flex: 1; height: 100%; display: flex; align-items: flex-end; }

.ad-hours-bar {
  position: relative;
  width: 100%;
  border-radius: 3px 3px 0 0;
  background: color-mix(in srgb, var(--accent) 32%, transparent);
  min-height: 2px;
}

.ad-hours-click {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 3px 3px 0 0;
  background: var(--accent);
}

.ad-hours-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 10.5px;
  color: var(--fg-dim);
}

.ad-hours-note { margin-top: 8px; font-size: 12.5px; color: var(--fg-dim); }

.ad-mine-link {
  margin-top: 8px;
  font-size: 12px;
  color: var(--fg-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Плитка «Сейчас» — первой и во всю ширину: это главный ответ на «а можно прямо сегодня?».
   Раньше сегодняшняя дата стояла серой заглушкой «сегодня», и человек уходил ждать полуночи. */
/* «Сейчас» занимает всю ширину - этого достаточно, чтобы выделить её среди дат.
   Раньше здесь стояли рамка и подсветка цветом выделения, причём с !important:
   плитка выглядела ВЫБРАННОЙ всегда, даже когда выбраны были другие сутки.
   Человек читал это как «реклама намертво стоит на сегодня». Выделение теперь
   рисует только .schedule-picked - то же, что у остальных плиток. */
.schedule-now { grid-column: 1 / -1; }
.schedule-now .schedule-date { color: var(--fg); }
.schedule-now.schedule-busy .schedule-date { color: var(--fg-dim); }

/* Экран проверки опубликованной рекламы. */
.ad-review-meta { margin-top: 12px; line-height: 1.7; }
.ad-review-remove { margin-top: 18px; }

.ad-review-remove textarea {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-family: inherit;
  font-size: 15px;
  resize: none;
}

/* Кнопка снятия — красная, но не первой: основное действие «Оставить», снятие всегда реже. */
.ad-review-remove-btn {
  margin-top: 10px;
  width: 100%;
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

.ad-mine-reason { margin-top: 12px; }
.ad-mine-fix { margin-top: 10px; }

/* Свой товар: срок жизни и удаление прямо на экране товара, а не в отдельном разделе. */
.product-own {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.product-own-note {
  margin-bottom: 10px;
  font-size: 13px;
  color: var(--fg-dim);
  line-height: 1.5;
}

.product-own .admin-reject-btn { width: 100%; }

/* Кнопка заказа в пустом разделе «Мои размещения»: у .submit-btn нет отступа сверху,
   вплотную к тексту она читается как его продолжение, а не как действие. */
#adMineList .submit-btn { margin-top: 14px; }

/* Крестик удаления на своей плитке — быстрый путь, не заходя в карточку. Тёмная подложка
   с размытием: плитка это фото, и голый значок на светлом снимке просто теряется. */
.tile-del {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: rgba(16, 24, 40, .58);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  color: #fff;
  font-size: 14px;
  line-height: 28px;
  font-family: inherit;
  text-align: center;
  padding: 0;
  cursor: pointer;
  z-index: 2;
}

.tile-del:active { transform: scale(.92); background: rgba(16, 24, 40, .78); }
.tile-del:disabled { opacity: .5; }

/* Карандаш правки - ЛЕВЕЕ крестика, той же формы и подложки. Правый верхний угол самый
   удобный для пальца, и занимать его должно необратимое действие: к удалению тянутся
   осознанно, а безобидная правка может стоять чуть дальше.

   Подложка полупрозрачная и с размытием: плитка это фото, и голый значок на светлом
   снимке просто теряется. */
.tile-edit {
  position: absolute;
  top: 6px;
  right: 40px;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: rgba(16, 24, 40, .58);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  color: #fff;
  padding: 0;
  cursor: pointer;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Значок мельче, чем на баннере: кружок 28px против 34px. */
.tile-edit .pencil-ico { width: 15px; height: 16px; }

.tile-edit:active { transform: scale(.92); background: rgba(16, 24, 40, .78); }

@media (hover: hover) {
  .tile-edit:hover { background: rgba(16, 24, 40, .78); }
}

/* Вкладки списка пользователей: Все / Онлайн / Заблокированные. Равная ширина — ни одна не
   выглядит главной, и палец попадает по любой одинаково. */
.user-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 14px;
  padding: 4px;
  border-radius: 999px;
  background: #f0f2f5;
}

.user-tab {
  flex: 1 1 0;
  min-width: 0;
  padding: 9px 6px;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--fg-dim);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  transition: background .15s, color .15s;
}

/* Активная — белой «таблеткой» с тенью: видно, где ты, не читая подписей. */
.user-tab.is-active {
  background: #fff;
  color: var(--fg);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 2px 6px rgba(16, 24, 40, .08);
}

.user-tab:active { opacity: .8; }

/* Пульс — только на НЕактивной «Онлайн»: она зовёт посмотреть, кто сейчас в приложении.
   На открытой вкладке мигание было бы просто рябью. */
.user-tab.is-live:not(.is-active) { animation: heading-ok-pulse 2.2s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .user-tab.is-live:not(.is-active) { animation: none; }
}

/* Строка сортировки — одна полоса, а не блок фильтров: порядок меняют редко, а место под
   вкладками нужно самому списку. */
.sort-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.sort-label { font-size: 12.5px; color: var(--fg-dim); flex: 0 0 auto; }

.sort-select {
  flex: 1;
  min-width: 0;
  height: 34px;
  padding: 0 10px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  font-size: 13px;
  font-family: inherit;
}

/* Журнал сбоев: действия над всем списком и удаление отдельной записи. */
.boot-log-actions { display: flex; gap: 8px; margin-bottom: 12px; }
.boot-log-actions button { flex: 1; margin: 0; }

/* Лог — моноширинным, с сохранением переносов: тайминги и этапы читаются только столбиком,
   в сплошном абзаце они превращаются в кашу. */
.boot-log-meta {
  margin: 6px 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--fg-dim);
  white-space: pre-wrap;
  word-break: break-word;
}

.boot-log-del { width: 100%; margin-top: 10px; }

/* Кнопка выбора фото в заявке. Свой выбор вместо системного поля: системное на iPhone и
   Android открывает разное, и половина вариантов всегда была недоступна. */
.photo-pick-btn {
  width: 100%;
  margin-top: 6px;
  padding: 14px;
  border-radius: 12px;
  border: 1px dashed var(--border);
  background: var(--card);
  color: var(--fg);
  font-size: 15px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}

.photo-pick-btn:active { opacity: .8; }

/* Звёздочка обязательного поля. Красная и чуть крупнее подписи — она должна считываться
   боковым зрением при прокрутке, а не находиться при вчитывании. */
.req {
  color: var(--danger);
  margin-left: 3px;
  font-weight: 700;
}

.form-req-note {
  margin: -6px 0 16px;
  font-size: 12.5px;
  color: var(--fg-dim);
}

/* Окно приглашения — по центру экрана. Раньше ссылка раскрывалась под карточкой: её
   приходилось искать прокруткой, а на двух карточках она была одна и та же. */
.invite-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 0;
  padding: 12px;
  border-radius: 12px;
  background: var(--card);
  border: 1px solid var(--border);
}

/* Ссылка длинная — обрезаем многоточием, а не переносим на три строки: читать её целиком
   всё равно не будут, копирование и отправка рядом. */
.invite-link {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.invite-copy {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
  color: var(--fg);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}

.invite-copy:active { opacity: .75; }
.invite-send { margin-top: 12px; }
.invite-note { margin-top: 12px; }

/* Пояснение под цифрами размещения: показы и просмотры считаются по-разному, и без строчки
   разница выглядит как ошибка в данных. */
.ad-mine-note {
  margin-top: 8px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--fg-dim);
}

/* Плашка с ценой — кнопка. Решение о покупке принимается в момент взгляда на цену, и
   отправлять человека искать другую кнопку ниже значит терять часть решившихся. */
.pitch-hero {
  display: block;
  width: 100%;
  border: none;
  font-family: inherit;
  text-align: center;
  cursor: pointer;
}

.pitch-hero:active { transform: scale(.99); }

/* Выбранные сутки — их может быть несколько сразу, поэтому отметка должна читаться на
   нескольких плитках одновременно, а не как «одна активная». */
.schedule-cell.schedule-picked::after {
  content: "✓";
  position: absolute;
  top: 4px;
  right: 6px;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
}

.schedule-cell { position: relative; }

/* Разобранная заявка уходит с экрана. Раньше она бледнела и оставалась навсегда — через
   десяток решений экран превращался в список разобранного, среди которого приходилось
   выискивать то, что ещё ждёт ответа. */
.earn-card.card-done {
  opacity: .55;
  transition: opacity .25s ease;
}

/* Сворачиваем высоту и отступы разом, иначе карточка исчезает, а пустое место остаётся. */
.earn-card.card-collapse {
  opacity: 0;
  max-height: 0;
  margin-top: 0;
  margin-bottom: 0;
  padding-top: 0;
  padding-bottom: 0;
  border-width: 0;
  overflow: hidden;
  transform: translateX(24px);
  transition: opacity .22s ease, max-height .26s ease, margin .26s ease,
              padding .26s ease, transform .26s ease;
}

/* Без явной высоты max-height анимировать нечего: у карточки она auto. */
.earn-card.card-done { max-height: 600px; }

@media (prefers-reduced-motion: reduce) {
  .earn-card.card-done,
  .earn-card.card-collapse { transition: none; }
}

/* Рынок на плитке товара: покупателю важно не только что за товар, но и куда за ним ехать. */
.tile-market { color: var(--fg-dim); font-size: 12px; }

/* Вторая строка карточки товара: категория и рынок через разделитель. Отдельно от названия —
   его читают первым, а это уточнения. */
.product-meta {
  margin-top: 6px;
  font-size: 14px;
  color: var(--fg-dim);
}

.product-market { white-space: nowrap; }

/* Контакт поставщика в карточке товара — над разделителем, сразу под самим товаром. */
.product-contact-btn { margin-top: 16px; }

.product-contact-out {
  margin-top: 12px;
  font-size: 15px;
  line-height: 1.7;
  word-break: break-word;
}

/* Окно «не хватает средств» — по центру, с двумя выходами вместо тупика. */
.funds-card { text-align: center; }
.funds-title { font-size: 18px; font-weight: 700; margin-bottom: 10px; }

.funds-text {
  font-size: 14px;
  line-height: 1.55;
  color: var(--fg-dim);
  margin-bottom: 18px;
}

.funds-buy { margin-top: 10px; }

/* Контакты под кнопкой профиля: номер нужен сразу, не заставляя открывать профиль. */
.product-contact-rows { margin-top: 12px; line-height: 1.8; }

/* ══ Профиль поставщика ═══════════════════════════════════════════════════════════════════
   Экран, ради которого человек заплатил. Он должен читаться сверху вниз одним движением:
   кто это и можно ли доверять → где искать → как связаться. Всё в языке приложения —
   те же карточки, скругления и синий акцент, что на остальных экранах. */

.sp-head {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 18px;
}

/* Буква вместо аватарки: фото поставщика у нас нет, а пустой круг выглядит как незагрузка. */
.sp-ava {
  flex: 0 0 auto;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), #1e5fd0);
  color: #fff;
  font-size: 22px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sp-head-text { min-width: 0; }

.sp-name {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.25;
  word-break: break-word;
}

/* Подтверждение — плашкой, а не строкой текста: это метка доверия, её должно быть видно
   боковым зрением, до чтения. */
.sp-badge {
  display: inline-block;
  margin-top: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
}

.sp-badge.is-ok { background: rgba(87, 199, 133, .16); color: #2f8f5b; }
.sp-badge.is-no { background: rgba(0, 0, 0, .06); color: var(--fg-dim); }

.sp-card {
  background: var(--card);
  border-radius: 14px;
  padding: 4px 14px;
  margin-bottom: 12px;
}

.sp-row {
  display: flex;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
  font-size: 15px;
}

.sp-row:last-child { border-bottom: none; }

.sp-label {
  flex: 0 0 88px;
  color: var(--fg-dim);
  font-size: 13px;
  padding-top: 1px;
}

.sp-value { flex: 1; min-width: 0; word-break: break-word; }

.sp-bio {
  padding: 14px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--fg-dim);
}

.sp-contacts { margin-top: 18px; }

/* Каждый способ связи — строка-действие: значок, что произойдёт, и сам контакт мелким.
   Раньше это были одинаковые синие ссылки, из которых не видно, откроется звонок или чат. */
/* a.sp-contact — звонок; button.sp-contact — остальное. Правила общие, чтобы ссылка и
   кнопка выглядели одинаково: разный тег здесь продиктован не оформлением, а тем, что
   переход по tel: из кода в Telegram блокируется, а по самой ссылке — нет. */
a.sp-contact { text-decoration: none; }

.sp-contact {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  margin-bottom: 10px;
  padding: 13px 14px;
  border: none;
  border-radius: 14px;
  background: var(--card);
  color: var(--fg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.sp-contact:active { transform: scale(.985); filter: brightness(.97); }

.sp-contact-ico {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
}

/* Цвет кружка кодирует способ связи: синий Telegram, зелёный звонок, серый маршрут —
   различить их можно, не читая подписи. */
.sp-tg  .sp-contact-ico { background: rgba(46, 124, 246, .14); }
.sp-tel .sp-contact-ico { background: rgba(87, 199, 133, .18); }

.sp-tg  .sp-contact-ico::before { content: "💬"; }
.sp-tel .sp-contact-ico::before { content: "📞"; }

.sp-contact-text { flex: 1; min-width: 0; }

.sp-contact-label {
  display: block;
  font-size: 15px;
  font-weight: 600;
}

.sp-contact-sub {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  color: var(--fg-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sp-contact-go {
  flex: 0 0 auto;
  color: var(--fg-dim);
  font-size: 20px;
  line-height: 1;
}

/* Строка «Назад + Статистика»: заголовок «Реклама на первом экране» убран как повтор, а
   отдельная строка под кнопкой съедала высоту, которая нужна графику. */
.pitch-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}

/* Кнопка «Назад» несёт собственный отступ снизу (14px), и внутри этой строки он
   складывался с отступом самой строки - между «Статистикой» и карточкой графика зияла
   двойная щель. Внутри строки отступ кнопке не нужен: его задаёт сама строка. */
.pitch-topbar .back-btn { margin-bottom: 0; }

/* Сверху над строкой складывались ТРИ отступа: 12px у полосы статистики, 16px у main и
   4px у самого экрана - 32px пустоты на телефоне, где дорог каждый пиксель. Подрезаем
   отрицательным полем ТОЛЬКО на этом экране: main и полоса общие для всего приложения,
   трогать их значило бы поехать везде разом. */
.pitch-topbar { margin-top: -6px; }

.pitch-topbar-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--fg-dim);
}

/* Заголовок цены по центру: под ним плашка во всю ширину, и заголовок у левого края
   выглядел смещённым относительно неё. */
.pitch-price-heading { text-align: center; }

/* Блок цифр — по строке на число, поэтому нужен воздух между строками. */
/* Блок цифр читают слева направо: числа выстраиваются в столбик под своими подписями, и
   глазу не приходится искать начало каждой строки. Класс .earn-note центрирует текст и
   используется в десятках мест - переопределяем ТОЛЬКО здесь, сам класс не трогаем. */
#adPitchStats {
  line-height: 1.7;
  text-align: left;
  margin-left: 4px;
  margin-right: 4px;
}

/* Боковые поля карточки срезаны ради графика, поэтому текстовым строкам отступ возвращаем
   отдельно - иначе заголовок и легенда прилипают к краю. */
.chart-card > .chart-head,
.chart-card > .chart-legend,
.chart-card > .chart-help { padding-left: 8px; padding-right: 8px; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   ГЛАВНЫЙ ЭКРАН - НОВОЕ ОФОРМЛЕНИЕ (Кирилл 2026-08-26)

   Тексты, состав и порядок блоков НЕ ТРОНУТЫ. Меняются только форма, цвет и движение.
   Правила дописаны в конец файла осознанно: так они перекрывают прежние без правки
   двадцати мест выше, и старое оформление можно вернуть, удалив один блок.

   1. Плитка асимметричная: угол срезан, радиусы по диагонали разные. Такую форму не даёт
      по умолчанию ни один конструктор - потому она и не читается шаблоном.
   2. Значок раздела вынесен огромным и обрезан краем: работает фоном, а не иконкой над
      подписью. Отдельная разметка не понадобилась - взят существующий .home-tile-icon.
   3. Каждый раздел в своём тоне. Одинаково-серая сетка - главный признак шаблона: она
      говорит, что все разделы равны, хотя это неправда.
   4. Нажатие: заливка выезжает из срезанного угла по диагонали, а не масштаб и не тень.
   ══════════════════════════════════════════════════════════════════════════════════════ */

:root {
  --pine: #0E6B5B;
  --pine-soft: #E4F0EC;
  /* Жёлтого в палитре больше нет (Кирилл 2026-08-27). Песочный тон читался как «выцвело»
     и спорил с зелёным - фирменным цветом приложения. Остальные тона приглушены: тон
     раздела должен ЕДВА отличать плитки друг от друга, а не раскрашивать экран. Насыщенность
     срезана примерно вдвое, оттенок у каждого сохранён - разделы по-прежнему различимы. */
  --clay: #A0705C;
  --clay-soft: #F2ECE9;
  --slate: #4C6180;
  --slate-soft: #ECEFF3;
  --plum-soft: #EFECF4;
  --mist-soft: #EAF0F3;
  --rose-soft: #F1EBEE;
  --stone-soft: #EEF0F2;
}

/* ── Баннер: срезанный угол и дышащая цветом полоса ────────────────────────────────────── */
.ad-banner {
  border-radius: 18px 4px 18px 4px;
  position: relative;
  overflow: hidden;
  padding-left: 20px;
}
.ad-banner::after {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  /* Полоса была апельсин -> жёлтый: два самых кричащих цвета подряд на самом видном месте
     экрана. Теперь она в фирменном зелёном, от насыщенного к приглушённому. */
  background: linear-gradient(180deg, var(--pine), #6E9A8F);
  animation: ins-breathe 5s ease-in-out infinite;
}
@keyframes ins-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* ── Баланс ────────────────────────────────────────────────────────────────────────────── */
.home-balance-action {
  border-radius: 12px 3px 12px 3px;
  padding: 10px 15px;
  transition: transform .18s cubic-bezier(.2, .7, .3, 1), opacity .18s;
}
.home-balance-action:active { transform: translateY(1px) scale(.97); }
/* Была песочная с коричневым текстом - самое жёлтое пятно на главной. Нейтральная подложка
   с фирменным текстом: кнопка остаётся заметной, но не спорит с «Пополнить» рядом. */
.home-balance-action { background: var(--stone-soft); color: var(--pine); }
.home-balance-action.primary { background: var(--pine); color: #fff; }

/* ── Три быстрые кнопки ────────────────────────────────────────────────────────────────── */
/* ВНИМАНИЕ: это правило идёт ПОЗЖЕ базового (~строка 681) и перебивает его padding.
   Менять высоту кнопок надо ЗДЕСЬ - правка наверху ни на что не влияет. */
.quick-btn {
  position: relative;
  overflow: hidden;
  border: 1.5px solid var(--border);
  background: none;
  border-radius: 14px 3px 14px 3px;
  padding: 5px 6px;
  transition: border-color .25s, color .25s;
}
.quick-btn::after {
  content: "";
  position: absolute; left: 10px; right: 10px; bottom: 6px; height: 2px;
  background: var(--pine);
  transform: scaleX(0); transform-origin: left;
  transition: transform .4s cubic-bezier(.2, .7, .3, 1);
}
.quick-btn:active { border-color: var(--pine); color: var(--pine); opacity: 1; }
.quick-btn:active::after { transform: scaleX(1); }

/* Смайлик живёт сам по себе. У каждой кнопки СВОЙ сдвиг по времени: в такт они мигают
   гирляндой, вразнобой - выглядят живыми. */
.quick-emoji,
.quick-avatar {
  display: inline-block;
  animation: ins-sway 6s ease-in-out infinite;
  transition: transform .35s cubic-bezier(.2, .7, .3, 1);
}
.quick-btn:nth-child(2) .quick-emoji { animation-delay: -2s; }
.quick-btn:nth-child(3) .quick-emoji { animation-delay: -4s; }
.quick-btn:active .quick-emoji,
.quick-btn:active .quick-avatar { transform: scale(1.22) rotate(-8deg); }

@keyframes ins-sway {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  35%      { transform: translateY(-2.5px) rotate(-5deg); }
  70%      { transform: translateY(1px) rotate(4deg); }
}

/* ── Шесть разделов ───────────────────────────────────────────────────────────────────── */
.home-grid { gap: 9px; }

.home-tile {
  position: relative;
  overflow: hidden;
  justify-content: flex-end;      /* подпись прижата вниз, значок уходит вверх фоном */
  gap: 0;
  min-height: 116px;
  padding: 15px 14px 14px;
  border-radius: 18px 4px 18px 4px;
  /* Видимой плитка должна быть В БАЗЕ, а прятать её должна САМА анимация. Раньше было
     наоборот: opacity: 0 в базе и появление через forwards. Стоило скрипту снять
     отыгравшую анимацию - плитка возвращалась к базовому состоянию и ИСЧЕЗАЛА. */
  opacity: 1;
  animation: ins-rise .5s cubic-bezier(.2, .7, .3, 1) both;
}
@keyframes ins-rise {
  from { opacity: 0; transform: translateY(12px) scale(1); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.home-tile:nth-child(1) { animation-delay: .02s; }
.home-tile:nth-child(2) { animation-delay: .07s; }
.home-tile:nth-child(3) { animation-delay: .12s; }
.home-tile:nth-child(4) { animation-delay: .17s; }
.home-tile:nth-child(5) { animation-delay: .22s; }
.home-tile:nth-child(6) { animation-delay: .27s; }

/* Прежнее нажатие (масштаб всей плитки) отменяем - его делают все. Но НЕ через
   transform: none: тогда во время нажатия сбрасывалось бы и «дыхание». Держим текущий
   масштаб дыхания, который выставляет скрипт. */
.home-tile:active { transform: scale(var(--ins-s, 1)); opacity: 1; }

/* Дыхание: очень медленный переход, чтобы движение читалось как живое, а не как отклик
   на нажатие. Значения масштаба и яркости ставит скрипт - случайные и каждый раз разные,
   потому что любой набор ключевых кадров в CSS рано или поздно повторяется, и глаз это
   ловит. */
.home-tile {
  transform: scale(var(--ins-s, 1));
  filter: brightness(var(--ins-b, 1));
  transition: transform 2.6s cubic-bezier(.4, 0, .3, 1),
              filter 2.6s cubic-bezier(.4, 0, .3, 1);
}

/* Заливка из срезанного угла - подпись формы. Отдельный элемент не нужен: берём ::before. */
.home-tile::before {
  content: "";
  position: absolute; right: -20%; top: -60%; width: 160%; height: 160%;
  border-radius: 50%;
  transform: scale(0); transform-origin: top right;
  transition: transform .55s cubic-bezier(.2, .7, .3, 1);
  pointer-events: none;
}
.home-tile:active::before { transform: scale(1); }

/* Значок: тот же элемент, что был иконкой, — теперь фон. Разметку менять не пришлось. */
.home-tile-icon {
  position: absolute; right: -12px; top: -10px;
  font-size: 66px; line-height: 1;
  opacity: .16;
  pointer-events: none;
  transition: transform .45s cubic-bezier(.2, .7, .3, 1), opacity .3s;
  animation: ins-float 11s ease-in-out infinite;
  /* Отдельный слой. Без него бесконечное движение внутри обрезанной скруглённой плитки
     заставляет перерисовывать её текст каждый кадр - отсюда обрубленные буквы и цифры. */
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.home-tile:nth-child(2) .home-tile-icon { animation-delay: -1.8s; }
.home-tile:nth-child(3) .home-tile-icon { animation-delay: -3.6s; }
.home-tile:nth-child(4) .home-tile-icon { animation-delay: -5.4s; }
.home-tile:nth-child(5) .home-tile-icon { animation-delay: -7.2s; }
.home-tile:nth-child(6) .home-tile-icon { animation-delay: -9s; }
.home-tile:active .home-tile-icon {
  transform: rotate(-4deg) scale(1.12) translate(-5px, 5px);
  opacity: .26;
}

@keyframes ins-float {
  0%, 100% { transform: rotate(8deg) translate(0, 0); }
  50%      { transform: rotate(3deg) translate(-5px, 5px); }
}

.home-tile-title {
  position: relative; z-index: 2;
  font-size: 15.5px; font-weight: 700; line-height: 1.25; letter-spacing: -.025em;
}

/* Тон по разделу - через существующий data-атрибут, разметку трогать не нужно. */
/* Тон - ПЕРЕМЕННАЯ, а не жёсткая заливка. Скрипт меняет её у случайных плиток, и переход
   сглаживает смену. Прибитый к разделу цвет сменить было бы нельзя, не переписывая правило.

   Переход длиннее самой паузы между сменами: цвет должен ПЕРЕТЕКАТЬ. Мгновенная смена
   читается как перерисовка экрана и выглядит сбоем, а не жизнью. */
.home-tile {
  background: var(--tint, var(--card));
  transition:
    background 1.6s cubic-bezier(.4, 0, .3, 1),
    transform 2.6s cubic-bezier(.4, 0, .3, 1),
    filter 2.6s cubic-bezier(.4, 0, .3, 1);
}

.home-tile::before { background: var(--tint-fill, rgba(20, 24, 28, .07)); }

/* Начальные тона. Дальше их подхватывает и перемешивает скрипт. */
.home-tile[data-home="feed"]       { --tint: var(--clay-soft);  --tint-fill: rgba(160, 112, 92, .10); }
.home-tile[data-home="form"]       { --tint: #E9F0ED;           --tint-fill: rgba(14, 107, 91, .10); }
.home-tile[data-home="news"]       { --tint: var(--slate-soft); --tint-fill: rgba(76, 97, 128, .10); }
.home-tile[data-home="tools"]      { --tint: var(--plum-soft);  --tint-fill: rgba(107, 78, 155, .10); }
.home-tile[data-home="broadcasts"] { --tint: var(--mist-soft);  --tint-fill: rgba(56, 122, 156, .10); }
.home-tile[data-home="projects"]   { --tint: var(--rose-soft);  --tint-fill: rgba(150, 100, 120, .09); }

/* ── Три кнопки снизу ─────────────────────────────────────────────────────────────────── */
.home-idea-btn {
  border-radius: 14px 3px 14px 3px;
  border: 1.5px dashed var(--border);
  transition: border-color .25s, color .25s;
}
.home-idea-btn:active { border-color: var(--pine); color: var(--pine); }

.home-earn-btn {
  position: relative;
  overflow: hidden;
  border-radius: 18px 4px 18px 4px;
  background: var(--pine);
}
/* Блик раз в несколько секунд - ЕДИНСТВЕННОЕ громкое движение на экране. Если добавить
   такое всюду, получится ярмарка, а не дорогое приложение. */
.home-earn-btn::after {
  content: "";
  position: absolute; top: 0; bottom: 0; left: 0; width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .22), transparent);
  transform: translateX(-133.33%) skewX(-18deg);
  animation: ins-shine 5.5s ease-in-out infinite;
  /* Свой слой у блика: иначе браузер каждый кадр перерисовывает кнопку целиком вместе с
     надписью, и на телефоне буквы режутся посреди строки. */
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
/* Двигаем transform, а не left. left - это раскладка: её изменение заставляет браузер
   пересчитывать и перерисовывать содержимое кнопки 60 раз в секунду. transform живёт на
   видеокарте и текста не касается. Картинка та же, цена другая. */
@keyframes ins-shine {
  0%   { transform: translateX(-133.33%) skewX(-18deg); }
  45%  { transform: translateX(266.67%) skewX(-18deg); }
  100% { transform: translateX(266.67%) skewX(-18deg); }
}

.admin-entry-btn {
  border-radius: 14px 3px 14px 3px;
  border: 1.5px dashed var(--border);
}

@media (prefers-reduced-motion: reduce) {
  .home-tile { opacity: 1; transform: none; animation: none; }
  .ad-banner::after,
  .home-earn-btn::after,
  .home-tile-icon,
  .quick-emoji,
  .quick-avatar { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   ЖИВОЙ ФОН (Кирилл 2026-08-26)

   Фон был анимирован и раньше, но двумя СИНИМИ пятнами от прежней палитры - на белом их
   практически не видно, и к новым цветам они не подходили. Заменяем.

   Два независимых слоя пятен в цветах разделов: хвойный, абрикос, песок на одном; слива и
   сланец на другом. Периоды 34 и 47 секунд - НАМЕРЕННО не кратные: при кратных слои
   сходятся в исходное положение регулярно, глаз ловит петлю и фон начинает раздражать.
   При некратных рисунок не повторяется часами.

   ПОЧЕМУ ОТДЕЛЬНЫМ СЛОЕМ, а не фоном body. Анимация background-position заставляет
   браузер перерисовывать страницу целиком на каждом кадре - на телефоне это заметно по
   рывкам при прокрутке и по батарее. Здесь двигается только transform закреплённого
   слоя: он живёт на своём слое видеокарты и содержимого не касается.

   Прозрачность 16-20%. Первая версия была на 7-9% - её попросту не замечали. Выше 20% я
   бы не поднимался: карточки лежат на фоне, и при более плотных пятнах белый край плитки
   начинает спорить с подложкой, а экран выглядеть грязным.
   ══════════════════════════════════════════════════════════════════════════════════════ */

body {
  /* Старая двухпятновая анимация выключена - её заменяет слой ниже. Снимаем и
     background-size: без него правило bg-drift осталось бы висеть вхолостую и заставляло
     бы браузер держать у страницы фоновый слой, который уже не используется. */
  background-image: none;
  background-size: auto;
  animation: none;
  position: relative;
}

/* ЗАМША. Настоящая замша читается двумя вещами: мелким ворсом и тем, как по нему ходит
   свет, когда ворс приминается. Поэтому слоя три:
   1. цвет - мягкие пятна палитры (ниже);
   2. ворс - тонкое зерно, нарисованное фильтром турбулентности прямо в стилях. Это не
      картинка с диска: ничего не грузится, вес нулевой, на любом экране не мылится;
   3. блик - широкая мягкая полоса, медленно идущая по диагонали. Она и создаёт ощущение,
      что поверхность шевелится, а не просто переливается.  */
/* Постоянное притенение по ВСЕМУ периметру. Неподвижное - это основа, на которой уже
   играют вспышки. Середина остаётся чисто белой: белое поле в центре работает подсветкой
   для содержимого, и кнопки на нём читаются лучше всего. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(38, 44, 54, .13) 0%, transparent 17%),
    linear-gradient(0deg,   rgba(38, 44, 54, .13) 0%, transparent 17%),
    linear-gradient(90deg,  rgba(38, 44, 54, .12) 0%, transparent 19%),
    linear-gradient(270deg, rgba(38, 44, 54, .12) 0%, transparent 19%);
}

/* Ворс: отдельным неподвижным слоем поверх цвета. Двигать зерно нельзя - оно начнёт
   рябить и резать глаз; замша шевелится светом, а не ворсинками. */
/* ══════════════════════════════════════════════════════════════════════════════════════
   МАТОВЫЙ СЕРЫЙ ФОН (Кирилл 2026-08-26)

   Подложка перестала быть белой. Серый выбран ХОЛОДНЫЙ и приглушённый: тёплый серый на
   экране всегда отдаёт желтизной и выглядит несвежим, а насыщенный - грязным.

   ГЛАВНОЕ ОГРАНИЧЕНИЕ: карточки в приложении светло-серые (#f4f4f6), их используют 25
   элементов. Сделай фон такой же светлоты - и они исчезнут. Поэтому подложка заметно
   темнее карточек: разница в светлоте и создаёт ощущение, что карточки лежат ПОВЕРХ
   поверхности, а не нарисованы на ней. Отсюда и объём, без единой тени.

   Матовость даёт зерно: глянец на экране получается бликом по всей плоскости, матовость -
   мелкой шероховатостью, которая этот блик рассеивает. Поэтому зерно плотнее, чем было,
   а блик - узкий и мягкий по краям.
   ══════════════════════════════════════════════════════════════════════════════════════ */
body {
  /* Возврат к белому: серая подложка глушила карточки и весь экран становился тусклым.
     Объём теперь даёт не фон, а ТЕНЬ ПОД КАРТОЧКАМИ, которая ходит за наклоном телефона. */
  background-color: var(--bg);
}

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

.ins-grain {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Плотнее прежнего: именно зерно даёт матовость. На глянце свет отражается всей
     плоскостью, на матовой поверхности его рассеивает шероховатость. */
  /* На белом плотное зерно читается не как матовость, а как грязь на экране. Оставляем
     едва различимый след - он снимает «пластиковость» ровной заливки. */
  opacity: .16;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* Блик по ворсу: очень широкий, очень медленный, идёт по диагонали и меняет направление.
   Период 29 секунд - не кратен ни 34, ни 47 у цветных слоёв, поэтому три движения никогда
   не сходятся в одну точку и рисунок не зацикливается. */
.ins-sheen {
  position: fixed;
  inset: -40%;
  z-index: -1;
  pointer-events: none;
  /* Узкая полоса с мягкими краями - свет, скользящий по матовой поверхности. Резкая
     граница дала бы глянец, а нам нужен рассеянный отблеск. */
  background: linear-gradient(115deg,
    transparent 36%,
    rgba(255, 255, 255, .42) 47%,
    rgba(255, 255, 255, .60) 50%,
    rgba(255, 255, 255, .42) 53%,
    transparent 64%);
  animation: ins-sheen-move 23s ease-in-out infinite alternate;
}

/* ВСПЫШКИ. Мягкие пятна света, распускающиеся и гаснущие в СЛУЧАЙНЫХ местах по всей
   площади экрана. Положение, размер, цвет, длительность и пауза - каждый раз новые, их
   ставит скрипт: на ключевых кадрах CSS вспышки повторялись бы по кругу, и через минуту
   глаз ловит, где загорится следующая.

   Появление и угасание намеренно долгие (6-11 секунд): вспышка должна ощущаться сменой
   освещения в комнате, а не миганием. Быстрая вспышка на фоне - это уже реклама. */
.ins-bloom {
  position: fixed;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  will-change: opacity, transform;
}

@keyframes ins-sheen-move {
  from { transform: translate3d(-22%, -14%, 0); }
  to   { transform: translate3d(22%, 14%, 0); }
}

/* Второй слой с другим набором пятен и другим периодом: два независимых слоя дают
   переливание, которого одним не добиться - пятна то расходятся, то накладываются. */
/* Второй цветной слой и его движение убраны: фон стал белым, двигать на нём нечего.
   Всё движение теперь в вспышках по периметру - они и дают жизнь. */
/* Шапка и карточки остаются НЕПРОЗРАЧНЫМИ: цветной фон под текстом - первое, что делает
   интерфейс дешёвым и нечитаемым. Фон виден в промежутках, и этого достаточно. */
.topbar { background: var(--bg); }

@media (prefers-reduced-motion: reduce) {
  body::before,
  body::after,
  .ins-sheen { animation: none; }
  .ins-bloom { display: none; }
  /* Виньетку оставляем: она статична и даёт объём без движения. */
}

/* ── Заголовок в одну строку с «Назад» ──────────────────────────────────────────────────
   Экономит две строки высоты на каждом экране. В строке заголовок становится скромнее:
   в столбик он был главным, а рядом с кнопкой обязан уступить ей - иначе спорят два
   тяжёлых элемента и строка выглядит перегруженной. */
.screen-topbar {
  margin-bottom: 10px;
}

.screen-topbar .back-btn { margin: 0; flex: 0 0 auto; }
/* Действие прижато к ПРАВОМУ краю, вплотную к значку ссылки. Раньше строка
   растягивала промежутки поровну, и кнопка зависала посреди пустоты - между
   «Назад» и значком, не примыкая ни к чему. Правая сторона строки должна
   читаться как одна группа. */
.screen-topbar .topbar-action { margin-left: auto; flex: 0 0 auto; }
.screen-topbar > .share-btn { flex: 0 0 auto; }

.screen-topbar > h1,
.screen-topbar > h2,
.screen-topbar > h3 {
  margin: 0;
  text-align: right;
  font-size: 15px;
  font-weight: 600;
  color: var(--fg-dim);
  letter-spacing: -.01em;
  /* Длинные названия не должны ломать строку: обрезаем многоточием, кнопка важнее. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   Боковые полосы убраны: их работу теперь делают вспышки по периметру, и вместе они
   спорили - полоса тянула край постоянно, вспышка приходила поверх, и получалось два
   разных затемнения на одном месте.
   ══════════════════════════════════════════════════════════════════════════════════════ */

/* Тени под карточками - обычные, от света сверху. Никаких датчиков. */
.home-tile {
  box-shadow:
    0 2px 6px rgba(20, 24, 28, .07),
    0 8px 20px rgba(20, 24, 28, .08),
    inset 0 -1px 0 rgba(255, 255, 255, .55);
}

.quick-btn {
  box-shadow: 0 1px 4px rgba(20, 24, 28, .05), 0 4px 12px rgba(20, 24, 28, .05);
}

.home-earn-btn {
  box-shadow: 0 3px 8px rgba(14, 107, 91, .22), 0 10px 24px rgba(14, 107, 91, .18);
}

.ad-banner {
  box-shadow: 0 4px 12px rgba(20, 24, 28, .14), 0 12px 30px rgba(20, 24, 28, .10);
}

@media (prefers-reduced-motion: reduce) {
  .ins-edge { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   ЕДИНЫЙ СТИЛЬ ДЛЯ ВСЕХ КАРТОЧЕК ПРИЛОЖЕНИЯ (Кирилл 2026-08-26)

   Главный экран переоформили, а внутренние - админка, заработок, проекты - остались в
   прежнем виде: прямые углы, синие числа, ни теней, ни знаков. Переход между экранами
   выглядел как переход между двумя разными приложениями.

   Класс .earn-card общий и встречается 76 раз, поэтому одна правка приводит к единому
   виду весь список сразу, без правки разметки.
   ══════════════════════════════════════════════════════════════════════════════════════ */

.earn-card {
  position: relative;
  overflow: hidden;
  /* Та же асимметрия, что у плиток главного экрана: один угол срезан. */
  border-radius: 18px 4px 18px 4px;
  box-shadow:
    0 2px 6px rgba(20, 24, 28, .06),
    0 8px 20px rgba(20, 24, 28, .07),
    inset 0 -1px 0 rgba(255, 255, 255, .6);
}

/* Заливка из срезанного угла - тот же отклик на нажатие, что на главной. */
.earn-card-action::before {
  content: "";
  position: absolute; right: -20%; top: -60%; width: 160%; height: 160%;
  border-radius: 50%;
  background: rgba(14, 107, 91, .10);
  transform: scale(0); transform-origin: top right;
  transition: transform .55s cubic-bezier(.2, .7, .3, 1);
  pointer-events: none;
}
.earn-card-action:active::before { transform: scale(1); }
.earn-card-action:active { opacity: 1; }

/* Знак карточки: тот же приём, что на главной - огромный, обрезан краем, работает фоном.
   Ставится скриптом из смайлика в заголовке, поэтому разметку править не пришлось. */
.card-mark {
  position: absolute; right: -12px; top: -10px;
  font-size: 60px; line-height: 1;
  opacity: .13;
  pointer-events: none;
  animation: ins-float 11s ease-in-out infinite;
  /* То же, что у плиток главной: движение живёт своим слоем и не трогает цифры рядом. */
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.earn-card:nth-child(2n) .card-mark { animation-delay: -3.7s; }
.earn-card:nth-child(3n) .card-mark { animation-delay: -7.4s; }

/* Карточки со счётчиками: украшение стоит неподвижно. Цифры здесь меняются на живую, и
   любая перерисовка родителя рискует задеть их. Плитки главной, где текст статичный,
   движение сохраняют.

   Слой снимаем ВМЕСТЕ с анимацией, и это главное. will-change: transform держит у элемента
   собственный слой композиции навсегда - даже когда двигаться уже нечему. Внутри карточки
   с overflow:hidden и скруглением такой мёртвый слой заставлял iOS не дорисовывать текст
   рядом: счётчик показывал «0» вместо «0 товаров», а любое касание или прокрутка чинили
   картинку, потому что вызывали перерисовку. Слой без движения не ускоряет ничего, только
   занимает память и ломает соседей. */
.admin-menu .card-mark,
.profile-tile .card-mark,
.card-mark-still {
  animation: none;
  will-change: auto;
  -webkit-backface-visibility: visible;
  backface-visibility: visible;
}

/* Текст держим БЕЗ собственного слоя. Свой слой тут вреден: значение счётчика меняется
   скриптом уже после отрисовки, а iOS при смене текста в готовом слое иногда оставляет
   прежнее содержимое - прочерк "-" из разметки превращался в тёмную полосу вместо слова.
   Слои нужны анимированным украшениям, а не тексту, который меняется. */
.earn-card-title,
.earn-card-amount,
.earn-card-desc { position: relative; z-index: 2; }

/* Числа - хвойным вместо синего: на главной уже нет ни одного синего элемента, а здесь
   они оставались главным цветом и тянули экран в старую палитру. */
.earn-card-amount { color: var(--pine); }

/* Ссылки-проекты и прочие плитки - по тем же законам. */
.project-link,
.tile {
  border-radius: 18px 4px 18px 4px;
  box-shadow:
    0 2px 6px rgba(20, 24, 28, .06),
    0 8px 20px rgba(20, 24, 28, .07);
}

/* Кнопки действий: главная - хвойная, как «Зарабатывай с нами» на первом экране. */
.submit-btn,
.topup-btn {
  border-radius: 16px 4px 16px 4px;
  background: var(--pine);
  box-shadow: 0 3px 8px rgba(14, 107, 91, .20), 0 10px 24px rgba(14, 107, 91, .14);
}

.back-btn { border-radius: 999px; }

.admin-approve-btn,
.admin-reject-btn { border-radius: 12px 3px 12px 3px; }

/* Карандаш правки поста: только владельцу размещения и админу. Лежит поверх баннера в
   углу - отдельной строкой он занял бы место, а внутрь кнопки его класть нельзя. */
.ad-banner-wrap { position: relative; }

.ad-banner-edit {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  width: 34px;
  height: 34px;
  padding: 0;
  border: none;
  border-radius: 12px 3px 12px 3px;
  /* Подложка почти прозрачная. Прежняя плашка была видна как отдельный прямоугольник,
     наклеенный поверх баннера, - кнопка должна принадлежать баннеру, а не лежать на нём. */
  background: rgba(255, 255, 255, .07);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background .2s, transform .18s cubic-bezier(.2, .7, .3, 1);
}

/* Значок - контурный, а не эмодзи. Системный смайлик нельзя ни перекрасить, ни провести им
   линию: он картинка. Контур живёт теми же правилами, что остальной интерфейс, и умеет
   рисовать - на этом и держится вся анимация. */
.ad-banner-edit { display: flex; align-items: center; justify-content: center; }

.ad-banner-edit:active { background: rgba(255, 255, 255, .16); transform: scale(.96); }

/* ── Карандаш ведёт линию ────────────────────────────────────────────────────────────────
   Прежний вариант был эмодзи, который дёргался, и белая полоска, которая выезжала рядом.
   Дёрганье само по себе читается дёшево: настоящий карандаш не трясётся, он ведёт.

   Здесь линия ПРОРИСОВЫВАЕТСЯ - тем же приёмом, что подпись на бумаге: штрих задан пунктиром
   в длину всего пути и сдвинут за его конец, а движение возвращает сдвиг к нулю, и штрих
   проступает от начала к концу. Карандаш идёт вдоль неё и к концу возвращается на место,
   а линия тает - остаётся впечатление, что он только что расписался, а не что кнопка мигнула.

   Всё идёт ПРИ НАВЕДЕНИИ (Кирилл 2026-08-27): нажатие - уже решение, показывать что-то
   после него поздно, следом открывается окно и накрывает экран. */
.pencil-ico {
  width: 19px;
  height: 20px;
  display: block;
  overflow: visible;
}

.pencil-ico .pen-body,
.pencil-ico .pen-line {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.pencil-ico .pen-body {
  stroke-width: 1.7;
  opacity: .68;
  /* Поворот вокруг грифеля, а не середины. В SVG для этого обязателен transform-box:
     fill-box - без него отсчёт идёт от холста всего документа, и значок улетает. */
  transform-box: fill-box;
  transform-origin: 22% 84%;
  transition: opacity .25s;
}

/* Линия невидима в покое: она след, а не часть значка. 21 - НАСТОЯЩАЯ длина зигзага,
   посчитанная по его сегментам (четыре по 4 на 3.4 пикселя). Взять число на глаз нельзя:
   меньше длины - хвост штриха останется висеть, заметно больше - прорисовка начнётся не
   сразу, а после паузы, и движение будет казаться запаздывающим. */
.pencil-ico .pen-line {
  stroke-width: 1.9;
  opacity: 0;
  stroke-dasharray: 21;
  stroke-dashoffset: 21;
}

/* Кнопка приподнимается ПЛАВНО, без отскока: подпрыгивающая кнопка и есть та самая
   дешевизна. Работает переход, а не ключевые кадры. */
.ad-banner-edit:hover,
.ad-photo-edit:hover,
.ad-banner-edit.drawing,
.ad-photo-edit.drawing {
  background: rgba(255, 255, 255, .17);
  transform: scale(1.06);
}

.ad-banner-edit:hover .pen-body,
.ad-photo-edit:hover .pen-body,
.ad-banner-edit.drawing .pen-body,
.ad-photo-edit.drawing .pen-body {
  opacity: 1;
  animation: pen-glide 1.05s cubic-bezier(.35, .1, .2, 1) both;
}

.ad-banner-edit:hover .pen-line,
.ad-photo-edit:hover .pen-line,
.ad-banner-edit.drawing .pen-line,
.ad-photo-edit.drawing .pen-line {
  animation: pen-draw 1.05s cubic-bezier(.35, .1, .2, 1) both;
}

/* Карандаш опускается к линии, ведёт её слева направо и возвращается в исходное. Линия -
   зигзаг, поэтому грифель на ходу поднимается и опускается: три колебания на четыре
   сегмента. Без них он ехал бы ровно, а под ним рисовалась пила - и сразу было бы видно,
   что это две несвязанные анимации, а не один жест.

   Вершины расставлены по времени так же, как излом по длине пути: к 62% карандаш доходит
   до правого края ровно тогда, когда штрих дорисован, и только после этого идёт назад.

   Наклон меняется на девять градусов - ровно столько даёт живая рука, не больше. */
@keyframes pen-glide {
  0%   { transform: translate(-3.6px, 2.4px) rotate(-8deg); }
  14%  { transform: translate(-3.2px, 3.6px) rotate(-9deg); }
  26%  { transform: translate(-1.6px, 1.8px) rotate(-8deg); }
  38%  { transform: translate(0.4px, 3.6px) rotate(-7deg); }
  50%  { transform: translate(2.4px, 1.8px) rotate(-5deg); }
  62%  { transform: translate(4.4px, 3.6px) rotate(-4deg); }
  100% { transform: translate(0px, 0px) rotate(0deg); }
}

/* Штрих проступает за грифелем и гаснет позже него: сначала он есть, потом карандаш уходит,
   и только затем след тает. Обратный порядок читался бы как стирание. */
@keyframes pen-draw {
  0%   { stroke-dashoffset: 21; opacity: 0; }
  16%  { stroke-dashoffset: 19; opacity: .9; }
  62%  { stroke-dashoffset: 0;  opacity: .9; }
  82%  { stroke-dashoffset: 0;  opacity: .9; }
  100% { stroke-dashoffset: 0;  opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ad-banner-edit:hover .pen-body,
  .ad-banner-edit:hover .pen-line,
  .ad-photo-edit:hover .pen-body,
  .ad-photo-edit:hover .pen-line,
  .ad-banner-edit.drawing .pen-body,
  .ad-banner-edit.drawing .pen-line,
  .ad-photo-edit.drawing .pen-body,
  .ad-photo-edit.drawing .pen-line { animation: none; }
}

/* Окно обрезки ВЫШЕ остальных окон. Оно единственное открывается ПОВЕРХ другого окна -
   правки поста, - а слой у них был общий (30). Обрезка открывалась ЗА правкой, и человек
   видел, что нажатие не делает ничего. Именно это Кирилл и поймал. */
#cropOverlay { z-index: 90; }

/* Рамка выбора фото для баннера. Пунктиром и в пропорциях самого баннера: человек должен
   видеть, что именно попадёт на первый экран, ещё до загрузки. */
.ad-photo-frame {
  position: relative;
  margin-top: 6px;
  width: 100%;
  aspect-ratio: 3.2 / 1;
  border: 2px dashed var(--border);
  border-radius: 18px 4px 18px 4px;
  background: var(--card);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.ad-photo-frame:active { border-color: var(--pine); }

.ad-photo-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ad-photo-hint {
  position: relative;
  z-index: 2;
  font-size: 13px;
  color: var(--fg-dim);
}

.ad-photo-clear { margin-top: 8px; }

/* ── Карандаш на примере ─────────────────────────────────────────────────────────────────
   Тот же угол и та же форма, что у карандаша на настоящем баннере: одно действие - один
   знак, в двух местах он не имеет права выглядеть по-разному.

   Курсор меняется только у него: сама рамка с фотографией больше не кнопка, и рука над ней
   обещала бы нажатие, которого нет. */
.ad-photo-edit {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 3;
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 10px 3px 10px 3px;
  background: rgba(255, 255, 255, .1);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: background .2s, transform .18s cubic-bezier(.2, .7, .3, 1);
}

.ad-photo-edit { display: flex; align-items: center; justify-content: center; }

.ad-photo-edit:active { background: rgba(255, 255, 255, .28); transform: scale(.96); }

/* Рамка с фотографией больше не нажимается - и не притворяется нажимаемой. Пустая
   по-прежнему кнопка: там написано, что делать, и мишень обязана совпадать с надписью. */
.ad-photo-frame:has(.ad-photo-edit:not(.hidden)) { cursor: default; }

/* Полоса загрузки фото. Тонкая, по нижнему краю рамки - там она не спорит с самим фото и
   не сдвигает разметку, появляясь и исчезая. */
.ad-photo-bar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 3;
  height: 3px;
  background: rgba(20, 24, 28, .10);
  overflow: hidden;
}

.ad-photo-bar i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--pine);
  /* Медленный переход намеренно: полоса ползёт плавно, а не прыгает рывками между
     редкими отметками прогресса. */
  transition: width .5s cubic-bezier(.3, .8, .4, 1);
}

/* ── Фото на баннере: занимает всю кнопку ────────────────────────────────────────────────
   У баннера слева отступ 20px под цветную полосу, а фон по умолчанию рисуется ВНУТРИ
   отступов: снимок оказывался сдвинут вправо на 20 пикселей и срезан слева - это Кирилл и
   увидел.

   Правило дописано в САМЫЙ КОНЕЦ файла намеренно. Специфичность у .ad-banner-photo и
   .ad-banner одинаковая, поэтому побеждает то, что стоит ниже. Первая попытка стояла
   выше правила с отступом и молча перебивалась - проверка это и показала. */
.ad-banner-photo {
  background-origin: border-box;
  background-clip: border-box;
  padding-left: 0;
}

/* Отступ под полосу возвращаем ТЕКСТУ, а не всей кнопке - иначе он снова съест фото. */
.ad-banner-photo .ad-banner-body { padding-left: 20px; }

/* ГЛАВНОЕ. Класс .ad-banner-sold ставится ВМЕСТЕ с фото и задаёт сокращённое свойство
   background - а оно сбрасывает background-size в auto. Картинка рисовалась в натуральную
   величину и выходила увеличенным обрывком: именно это Кирилл и видел.

   Файл при этом был правильный - замер показал ровно 900×281, то есть 3.2:1. Дело было не
   в обрезке и не в Telegram, а в одной строке стилей.
   
   Повторяем размер и раскладку ПОСЛЕ всех правил, где встречается сокращённый background. */
.ad-banner.ad-banner-photo {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-origin: border-box;
  background-clip: border-box;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   ОФОРМЛЕНИЕ РЕКЛАМНОГО ПОСТА

   Шрифт, движение и размытие выбирает сам рекламодатель. Ни одного загружаемого шрифта:
   на рынке связь слабая, а баннер стоит первым экраном - лишний запрос задержал бы его
   показ ради начертания. Все четыре варианта собраны из системных наборов, поэтому
   рисуются мгновенно и выглядят родными для устройства.
   ══════════════════════════════════════════════════════════════════════════════════════ */

.ad-banner-photo .ad-banner-body {
  /* Текст ВНИЗ (Кирилл 2026-08-27). Наверху он накрывал середину логотипа - самую заметную
     его часть; внизу подпись лежит под изображением, как подпись и должна. */
  align-items: flex-start;
  justify-content: flex-end;
  text-align: left;
  height: 100%;
  display: flex;
  flex-direction: column;
}

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

   Силу задаёт рекламодатель ползунком: --ad-dim это ПЛОТНОСТЬ У НИЖНЕГО КРАЯ (0..0.9), а
   верх и середина считаются от неё долями. Фиксированные значения не годились: на светлом
   логотипе текст тонул без затемнения, а на тёмном снимке то же затемнение превращало
   баннер в чёрный прямоугольник. */
.ad-banner-photo .ad-banner-body {
  background: linear-gradient(180deg,
    rgba(10, 16, 26, calc(var(--ad-dim, .8) * .14)) 0%,
    rgba(10, 16, 26, calc(var(--ad-dim, .8) * .48)) 46%,
    rgba(10, 16, 26, var(--ad-dim, .8)) 100%);
}

/* Заголовок крупнее и слева. Прежний размер терялся на фотографии: текст обязан спорить с
   картинкой, иначе пост читается как картинка без подписи. */
.ad-banner-photo .ad-banner-title {
  display: block;
  font-size: var(--ad-title-size, 26px);
  line-height: 1.05;
  letter-spacing: var(--ad-track, -.03em);
}

.ad-banner-photo .ad-banner-sub {
  display: block;
  margin-top: 6px;
  /* Подпись считается от заголовка, а не задаётся отдельно: два независимых размера
     разъезжаются, и на крупном заголовке подпись выглядит случайно забытой. */
  font-size: calc(var(--ad-title-size, 26px) * .52);
  letter-spacing: calc(var(--ad-track, 0em) * .6);
  /* НЕПРОЗРАЧНАЯ, и это принципиально (Кирилл 2026-08-27). Здесь стояло opacity: .88 -
     а прозрачность применяется к самим буквам, не к фону под ними. Затемнение просвечивало
     СКВОЗЬ подпись: тянешь ползунок затемнения, и буквы уходят в синеву вместе с фотографией.
     Ровно это и было видно.
     Цвет тоже свой: общий .ad-banner-sub задаёт #9fb3d0 - голубовато-серый, он хорош на
     тёмной плашке без фото, но поверх снимка читается как выцветший. */
  color: #F2F5F9;
  opacity: 1;
}

/* ── Шрифты ──────────────────────────────────────────────────────────────────────────────
   Шестнадцать семейств, названных своими именами. Ни одно не загружается: баннер стоит
   первым экраном, и лишний запрос за начертанием задержал бы его показ на слабой связи
   рынка. Поэтому у каждого - стопка запасных: на iPhone встанет первое, на Android
   ближайшее похожее, и в худшем случае системное. Начертание, регистр, размер и разрядка
   вынесены в ОТДЕЛЬНЫЕ настройки: раньше они были вшиты в пресет, и «крупный» означал
   одновременно и шрифт, и жирность, и заглавные - поменять что-то одно было нельзя. */
.ad-font-helvetica   .ad-banner-title, .ad-font-helvetica   .ad-banner-sub { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
.ad-font-avenir      .ad-banner-title, .ad-font-avenir      .ad-banner-sub { font-family: "Avenir Next", Avenir, "Segoe UI", Roboto, sans-serif; }
.ad-font-futura      .ad-banner-title, .ad-font-futura      .ad-banner-sub { font-family: Futura, "Trebuchet MS", "Century Gothic", sans-serif; }
.ad-font-optima      .ad-banner-title, .ad-font-optima      .ad-banner-sub { font-family: Optima, Candara, "Gill Sans", sans-serif; }
.ad-font-verdana     .ad-banner-title, .ad-font-verdana     .ad-banner-sub { font-family: Verdana, Tahoma, sans-serif; }
.ad-font-trebuchet   .ad-banner-title, .ad-font-trebuchet   .ad-banner-sub { font-family: "Trebuchet MS", Tahoma, sans-serif; }
.ad-font-georgia     .ad-banner-title, .ad-font-georgia     .ad-banner-sub { font-family: Georgia, "Times New Roman", serif; }
.ad-font-times       .ad-banner-title, .ad-font-times       .ad-banner-sub { font-family: "Times New Roman", Times, serif; }
.ad-font-didot       .ad-banner-title, .ad-font-didot       .ad-banner-sub { font-family: Didot, "Bodoni MT", Georgia, serif; }
.ad-font-baskerville .ad-banner-title, .ad-font-baskerville .ad-banner-sub { font-family: Baskerville, "Palatino Linotype", Palatino, serif; }
.ad-font-palatino    .ad-banner-title, .ad-font-palatino    .ad-banner-sub { font-family: Palatino, "Palatino Linotype", "Book Antiqua", serif; }
.ad-font-copperplate .ad-banner-title, .ad-font-copperplate .ad-banner-sub { font-family: Copperplate, "Copperplate Gothic Light", Optima, sans-serif; }
.ad-font-typewriter  .ad-banner-title, .ad-font-typewriter  .ad-banner-sub { font-family: "American Typewriter", "Courier New", Courier, monospace; }
.ad-font-menlo       .ad-banner-title, .ad-font-menlo       .ad-banner-sub { font-family: Menlo, Consolas, "Courier New", monospace; }
.ad-font-marker      .ad-banner-title, .ad-font-marker      .ad-banner-sub { font-family: "Marker Felt", "Comic Sans MS", cursive; }
.ad-font-snell       .ad-banner-title, .ad-font-snell       .ad-banner-sub { font-family: "Snell Roundhand", "Brush Script MT", cursive; }

/* ── Насыщенность и регистр ──────────────────────────────────────────────────────────── */
/* Трёх ступеней достаточно, и все три РЕАЛЬНО есть у системных шрифтов. Четвёртой,
   «очень жирной», больше нет: начертания Black у Helvetica Neue, Georgia и почти всех
   остальных не существует, и её приходилось подделывать обводкой - выходила окантовка,
   а не жирный текст. Настройка, которая не может сработать, хуже её отсутствия.
   Регистр тоже убран: как ввели, так и показываем. */
/* «Обычная» = 400, а не 500 (Кирилл 2026-08-28). Начертание Medium есть у единиц - у
   Helvetica Neue и Avenir, - а у Georgia, Didot, Verdana, Times, Menlo его нет, и 500
   отдавало тот же Regular, что и 300. Выходило, что «тонкая» и «обычная» рисуют одно и то
   же: ровно это Кирилл и увидел на Didot.

   400 есть у КАЖДОГО шрифта - это и делает «обычную» настоящей опорной точкой. 700 тоже
   работает всегда: если жирного начертания нет, браузер утолщает сам. А вот 300 есть далеко
   не у всех, и там, где его нет, кнопка «Тонкая» гасится - проверка идёт замером, см.
   fontHasLight в app.js. */
.ad-w-light  .ad-banner-title { font-weight: 300; }
.ad-w-normal .ad-banner-title { font-weight: 400; }
.ad-w-bold   .ad-banner-title { font-weight: 700; }
.ad-w-light  .ad-banner-sub   { font-weight: 300; }
.ad-w-normal .ad-banner-sub   { font-weight: 400; }
.ad-w-bold   .ad-banner-sub   { font-weight: 600; }

/* ── Размытие фотографии ─────────────────────────────────────────────────────────────────
   Размывается ОТДЕЛЬНЫЙ слой, а не сама кнопка: иначе размылся бы и текст поверх - ровно
   то, ради чего размытие и делается, перестало бы работать. */
.ad-banner-photo { background-image: none !important; }

.ad-banner-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: inherit;
}

/* Размытие теперь плавное, а не три ступени. --ad-blur это готовое число пикселей, его
   считает и ставит сам скрипт.

   --ad-bg-scale обязателен: размытие съедает края слоя и по периметру появляется светлая
   кайма. Слой увеличивают ровно настолько, чтобы кайма ушла за границы баннера. Масштаб
   держим ОТДЕЛЬНОЙ переменной, а не жёстким числом, потому что движения «Наплыв» и
   «Панорама» тоже правят transform и обязаны отталкиваться от него - иначе включённое
   движение сбрасывало бы поправку и кайма возвращалась. */
.ad-banner-bg {
  filter: blur(calc(var(--ad-blur, 0) * 1px));
  transform: scale(var(--ad-bg-scale, 1));
}

.ad-banner-photo .ad-banner-body { position: relative; z-index: 2; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   ДВИЖЕНИЕ. Разделено на два независимых набора (Кирилл 2026-08-27): буквы отдельно, фон
   отдельно. Прежний общий список заставлял выбирать между «текст появляется» и «снимок
   плывёт», хотя это разные слои и мешать друг другу им нечем - теперь можно и то и другое
   разом.

   Длительности НЕ записаны числами: все кратны одной единице --ad-t, которую двигает
   ползунок скорости. Иначе «быстрее» пришлось бы прописывать двадцать раз и однажды где-то
   забыть. Умножение на безразмерное число - самый совместимый вид записи, деление времени
   на переменную местами не поддерживается.
   ══════════════════════════════════════════════════════════════════════════════════════ */

/* ── Движение букв ─────────────────────────────────────────────────────────────────────── */

.ad-tanim-fade .ad-banner-body { animation: ad-fade-in calc(var(--ad-t, 1s) * 1) ease-out both; }
@keyframes ad-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Подъём: заголовок и подпись выезжают снизу по очереди. Задержка у подписи маленькая -
   при большей она читается как отдельное событие, а это одна фраза. */
.ad-tanim-rise .ad-banner-title { animation: ad-rise calc(var(--ad-t, 1s) * .7) cubic-bezier(.2, .8, .25, 1) both; }
.ad-tanim-rise .ad-banner-sub   { animation: ad-rise calc(var(--ad-t, 1s) * .7) cubic-bezier(.2, .8, .25, 1) calc(var(--ad-t, 1s) * .18) both; }
@keyframes ad-rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }

/* Выезд сбоку. Текст прижат влево, поэтому и выезжает слева - справа он шёл бы навстречу
   собственному выравниванию. */
.ad-tanim-slide .ad-banner-title { animation: ad-slide calc(var(--ad-t, 1s) * .8) cubic-bezier(.2, .8, .25, 1) both; }
.ad-tanim-slide .ad-banner-sub   { animation: ad-slide calc(var(--ad-t, 1s) * .8) cubic-bezier(.2, .8, .25, 1) calc(var(--ad-t, 1s) * .16) both; }
@keyframes ad-slide { from { opacity: 0; transform: translateX(-28px); } to { opacity: 1; transform: none; } }

/* Разбег: буквы стоят сбитыми и расходятся по местам.

   Раньше движение шло НАОБОРОТ - от разрядки .42em к нормальной, - и в этом был весь косяк.
   На заголовке из шестнадцати знаков .42em это лишних полторы сотни пикселей: строка не
   влезала в баннер, переносилась на вторую, и в первый миг вся раскладка прыгала. Ширину
   строки в стилях не измерить, значит любое начальное значение рано или поздно кого-то
   сломает - поэтому разгон идёт в СЖАТУЮ сторону. Уже строки, чем есть, она не станет
   никогда, и сломать нечего.

   Конечное значение берём из настройки разрядки, а не ноль - иначе движение заканчивалось
   бы не там, где человек её выставил. */
.ad-tanim-spread .ad-banner-title { animation: ad-spread calc(var(--ad-t, 1s) * 1.2) cubic-bezier(.2, .8, .25, 1) both; }
@keyframes ad-spread {
  from { opacity: 0; letter-spacing: -.1em; }
  to   { opacity: 1; letter-spacing: var(--ad-track, 0em); }
}
.ad-tanim-spread .ad-banner-sub { animation: ad-fade-in calc(var(--ad-t, 1s) * .9) ease-out calc(var(--ad-t, 1s) * .45) both; }

/* Из тумана: текст наводится на резкость. Размытие снимается вместе с тенью - она часть
   букв, и оставить её чёткой значило бы показать тень раньше самих букв. */
.ad-tanim-sharpen .ad-banner-title,
.ad-tanim-sharpen .ad-banner-sub { animation: ad-sharpen calc(var(--ad-t, 1s) * 1.1) ease-out both; }
@keyframes ad-sharpen { from { opacity: 0; filter: blur(9px); } to { opacity: 1; filter: blur(0); } }

/* По буквам. Заголовок разбирается на отдельные буквы скриптом, номер каждой лежит в --i -
   из него и считается задержка. Пробелы сохраняем через white-space: pre, иначе слова
   слиплись бы: буква в inline-block пробел рядом с собой не удерживает. */
.ad-tanim-letters .ad-banner-title span,
.ad-tanim-flip .ad-banner-title span,
.ad-tanim-wave .ad-banner-title span {
  display: inline-block;
  white-space: pre;
}

.ad-tanim-letters .ad-banner-title span {
  animation: ad-letter-in calc(var(--ad-t, 1s) * .5) cubic-bezier(.2, .8, .25, 1) both;
  animation-delay: calc(var(--i, 0) * var(--ad-t, 1s) * .038);
}
@keyframes ad-letter-in { from { opacity: 0; transform: translateY(14px) scale(.94); } to { opacity: 1; transform: none; } }
.ad-tanim-letters .ad-banner-sub { animation: ad-fade-in calc(var(--ad-t, 1s) * .8) ease-out calc(var(--ad-t, 1s) * .5) both; }

/* Переворот: каждая буква поворачивается вокруг горизонтальной оси. Перспектива задана на
   заголовке - без неё rotateX даёт плоское сжатие по высоте, а не поворот. */
.ad-tanim-flip .ad-banner-title { perspective: 420px; }
.ad-tanim-flip .ad-banner-title span {
  transform-origin: 50% 65%;
  animation: ad-letter-flip calc(var(--ad-t, 1s) * .6) cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(var(--i, 0) * var(--ad-t, 1s) * .05);
}
@keyframes ad-letter-flip { from { opacity: 0; transform: rotateX(-92deg); } to { opacity: 1; transform: none; } }
.ad-tanim-flip .ad-banner-sub { animation: ad-fade-in calc(var(--ad-t, 1s) * .8) ease-out calc(var(--ad-t, 1s) * .5) both; }

/* Волна - единственное движение букв, которое идёт постоянно. Размах крошечный: баннер
   стоит первым экраном, и всё, что заметно прыгает, через минуту начинает раздражать. */
.ad-tanim-wave .ad-banner-title span {
  animation: ad-letter-wave calc(var(--ad-t, 1s) * 2.4) ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * var(--ad-t, 1s) * .06);
}
@keyframes ad-letter-wave { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3.5px); } }

/* Пульс. Точка роста - левый край: заголовок прижат влево, и от центра он уползал бы за
   границу баннера. */
.ad-tanim-pulse .ad-banner-title {
  transform-origin: left center;
  animation: ad-pulse calc(var(--ad-t, 1s) * 3.2) ease-in-out infinite;
}
@keyframes ad-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.045); } }

/* Свечение: подсветка самих букв. Добавляется К выбранной тени, а не заменяет её - иначе
   включённое свечение стирало бы и сторону тени, и объём букв, две настройки, которые
   человек только что выставил. */
.ad-tanim-glow .ad-banner-title { animation: ad-glow calc(var(--ad-t, 1s) * 3.6) ease-in-out infinite; }
@keyframes ad-glow {
  0%, 100% { text-shadow: var(--ad-shadow, 0 2px 10px rgba(0, 0, 0, .55)); }
  50%      { text-shadow: 0 0 20px rgba(255, 255, 255, .8), var(--ad-shadow, 0 2px 10px rgba(0, 0, 0, .55)); }
}

/* ── Движение фона ─────────────────────────────────────────────────────────────────────── */

/* Было 24 секунды на 12% - полпроцента в секунду, увидеть это нельзя. Размах 26%, а
   длительность зависит от ползунка скорости. */
.ad-banim-zoom .ad-banner-bg { animation: ad-slow-zoom calc(var(--ad-t, 1s) * 12) ease-in-out infinite alternate; }
@keyframes ad-slow-zoom {
  from { transform: scale(var(--ad-bg-scale, 1)); }
  to   { transform: scale(calc(var(--ad-bg-scale, 1) * 1.26)); }
}

/* Панорама: снимок медленно едет вбок. Запас по масштабу нужен обязательно - без него
   сдвиг обнажил бы пустой край. */
.ad-banim-pan .ad-banner-bg { animation: ad-pan calc(var(--ad-t, 1s) * 13) ease-in-out infinite alternate; }
@keyframes ad-pan {
  from { transform: scale(calc(var(--ad-bg-scale, 1) * 1.22)) translateX(-8%); }
  to   { transform: scale(calc(var(--ad-bg-scale, 1) * 1.22)) translateX(8%); }
}

/* Проплыв: наплыв и панорама разом, по диагонали. Самое спокойное из движений фона -
   ни одно направление не выражено, и глаз ни за что не цепляется. */
.ad-banim-drift .ad-banner-bg { animation: ad-drift calc(var(--ad-t, 1s) * 18) ease-in-out infinite alternate; }
@keyframes ad-drift {
  from { transform: scale(calc(var(--ad-bg-scale, 1) * 1.18)) translate(-4.5%, -2.5%); }
  to   { transform: scale(calc(var(--ad-bg-scale, 1) * 1.30)) translate(4.5%, 2.5%); }
}

/* Качание. Поворот меньше полутора градусов: больше - и баннер выглядит криво повешенным,
   а не живым. Запас по масштабу закрывает углы, которые поворот выводит за края. */
.ad-banim-sway .ad-banner-bg { animation: ad-sway calc(var(--ad-t, 1s) * 10) ease-in-out infinite alternate; }
@keyframes ad-sway {
  from { transform: scale(calc(var(--ad-bg-scale, 1) * 1.16)) rotate(-1.2deg); }
  to   { transform: scale(calc(var(--ad-bg-scale, 1) * 1.16)) rotate(1.2deg); }
}

/* Дыхание: свет над снимком то ярче, то мягче. Размытие в обеих точках повторяем явно -
   filter свойство целиком, и упоминание одной яркости стёрло бы выбранное размытие. */
.ad-banim-breathe .ad-banner-bg { animation: ad-breathe calc(var(--ad-t, 1s) * 4.5) ease-in-out infinite; }
@keyframes ad-breathe {
  0%, 100% { filter: blur(calc(var(--ad-blur, 0) * 1px)) brightness(1); }
  50%      { filter: blur(calc(var(--ad-blur, 0) * 1px)) brightness(1.16); }
}

/* Блик и Засветка живут на ::before, а НЕ на ::after. У ::after баннера уже есть работа -
   цветная полоса слева, - и специфичность у правил одинаковая, значит побеждало то, что
   ниже в файле. Блик стоял ниже и молча стирал полосу: включаешь движение, а с баннера
   пропадает деталь, к движению отношения не имеющая. */
.ad-banim-shine::before {
  content: "";
  position: absolute; top: 0; bottom: 0; width: 40%;
  z-index: 1;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .26), transparent);
  transform: skewX(-16deg);
  animation: ad-shine-move calc(var(--ad-t, 1s) * 6) ease-in-out infinite;
  pointer-events: none;
}
@keyframes ad-shine-move { 0% { left: -50%; } 55% { left: 120%; } 100% { left: 120%; } }

/* Засветка: мягкое пятно света проходит наискось. В отличие от блика у него нет кромки -
   это не полоса, а освещение, поэтому на пёстрой фотографии оно не режет глаз. */
.ad-banim-flare::before {
  content: "";
  position: absolute;
  top: -40%; bottom: -40%; left: -30%;
  width: 60%;
  z-index: 1;
  background: radial-gradient(ellipse at center, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 62%);
  animation: ad-flare-move calc(var(--ad-t, 1s) * 9) ease-in-out infinite;
  pointer-events: none;
}
@keyframes ad-flare-move {
  0%   { transform: translateX(-40%); opacity: 0; }
  25%  { opacity: 1; }
  75%  { opacity: 1; }
  100% { transform: translateX(280%); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ad-tanim-fade .ad-banner-body,
  .ad-tanim-rise .ad-banner-title,
  .ad-tanim-rise .ad-banner-sub,
  .ad-tanim-slide .ad-banner-title,
  .ad-tanim-slide .ad-banner-sub,
  .ad-tanim-spread .ad-banner-title,
  .ad-tanim-spread .ad-banner-sub,
  .ad-tanim-sharpen .ad-banner-title,
  .ad-tanim-sharpen .ad-banner-sub,
  .ad-tanim-letters .ad-banner-title span,
  .ad-tanim-letters .ad-banner-sub,
  .ad-tanim-flip .ad-banner-title span,
  .ad-tanim-flip .ad-banner-sub,
  .ad-tanim-wave .ad-banner-title span,
  .ad-tanim-pulse .ad-banner-title,
  .ad-tanim-glow .ad-banner-title,
  .ad-banim-zoom .ad-banner-bg,
  .ad-banim-pan .ad-banner-bg,
  .ad-banim-drift .ad-banner-bg,
  .ad-banim-sway .ad-banner-bg,
  .ad-banim-breathe .ad-banner-bg,
  .ad-banim-shine::before,
  .ad-banim-flare::before { animation: none; }
}

/* Выбор оформления: ряды кнопок-переключателей. Компактные, чтобы три ряда не растянули
   окно правки на два экрана. */
.style-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 14px;
}

.style-chip {
  padding: 8px 12px;
  border: 1.5px solid var(--border);
  border-radius: 12px 3px 12px 3px;
  background: none;
  color: var(--fg-dim);
  font: 600 12.5px/1 inherit;
  font-family: inherit;
  cursor: pointer;
  transition: border-color .2s, color .2s, background .2s;
}

.style-chip.on {
  border-color: var(--pine);
  color: var(--pine);
  background: var(--pine-soft);
}

/* Живой пример внутри рамки выбора. Это НАСТОЯЩИЙ баннер теми же классами - снимаем только
   то, что относится к его месту на главной: внешние отступы, тень и собственную форму
   (форму задаёт сама рамка). Так пример не может разойтись с оригиналом. */
.ad-preview-banner {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border-radius: inherit;
  box-shadow: none;
  aspect-ratio: auto;
  pointer-events: none;
}

/* Пример мельче настоящего баннера, поэтому текст пропорционально уменьшен - иначе
   заголовок в рамке выглядел бы крупнее, чем окажется на экране. */
/* Пример уже настоящего баннера, поэтому текст уменьшен ровно во столько же раз - иначе
   заголовок в рамке выглядел бы крупнее, чем окажется на экране. Коэффициент один на всё:
   отдельных размеров под каждый шрифт больше нет, размер задаёт сам человек. */
.ad-preview-banner .ad-banner-title { font-size: calc(var(--ad-title-size, 26px) * .77); }
.ad-preview-banner .ad-banner-sub   { font-size: calc(var(--ad-title-size, 26px) * .4); }
.ad-preview-banner .ad-banner-body { padding: 14px 14px 14px 16px; }

/* ── Ползунки оформления: размытие и затемнение ──────────────────────────────────────────
   Вместо ряда кнопок - линия. У этих двух настроек нет «правильных» ступеней: сколько
   размытия нужно, зависит от самого снимка, и три заготовленных варианта регулярно
   промахивались. Линия даёт любое значение и сразу показывает результат на живом примере.

   Значение печатаем рядом с названием: без него человек тянет ползунок вслепую и не может
   вернуться к тому, что уже подобрал. */
.slider-field {
  display: flex;
  flex-direction: column;
  margin: 0 0 14px;
}

.slider-field .field-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.slider-val {
  font-weight: 700;
  font-size: 12.5px;
  color: var(--pine);
  font-variant-numeric: tabular-nums;   /* иначе число дёргается при перетягивании */
}

/* appearance: none обязателен на всех трёх - иначе браузер рисует свой ползунок поверх
   нашего и на iOS он остаётся синим системным. */
.style-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 26px;              /* палец попадает по всей высоте, а не по нитке в 4px */
  margin: 6px 0 0;
  background: none;
  cursor: pointer;
  touch-action: none;        /* иначе тянущий жест перехватывает прокрутка окна правки */
}

.style-slider::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.style-slider::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}

.style-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  margin-top: -8px;          /* центрирует кружок на дорожке: webkit считает от её верха */
  border-radius: 50%;
  background: var(--pine);
  border: 3px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .22);
}
.style-slider::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--pine);
  border: 3px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .22);
}

/* ── Ряд шрифтов: каждая кнопка написана своим шрифтом ───────────────────────────────────
   Список названий бесполезен, пока не видно начертания: «Didot» и «Baskerville» человеку
   ничего не говорят, а разницу между ними он видит сразу. */
.font-row .style-chip { font-size: 13px; }
.f-helvetica   { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
.f-avenir      { font-family: "Avenir Next", Avenir, "Segoe UI", Roboto, sans-serif; }
.f-futura      { font-family: Futura, "Trebuchet MS", "Century Gothic", sans-serif; }
.f-optima      { font-family: Optima, Candara, "Gill Sans", sans-serif; }
.f-verdana     { font-family: Verdana, Tahoma, sans-serif; }
.f-trebuchet   { font-family: "Trebuchet MS", Tahoma, sans-serif; }
.f-georgia     { font-family: Georgia, "Times New Roman", serif; }
.f-times       { font-family: "Times New Roman", Times, serif; }
.f-didot       { font-family: Didot, "Bodoni MT", Georgia, serif; }
.f-baskerville { font-family: Baskerville, "Palatino Linotype", Palatino, serif; }
.f-palatino    { font-family: Palatino, "Palatino Linotype", "Book Antiqua", serif; }
.f-copperplate { font-family: Copperplate, "Copperplate Gothic Light", Optima, sans-serif; }
.f-typewriter  { font-family: "American Typewriter", "Courier New", Courier, monospace; }
.f-menlo       { font-family: Menlo, Consolas, "Courier New", monospace; }
.f-marker      { font-family: "Marker Felt", "Comic Sans MS", cursive; }
.f-snell       { font-family: "Snell Roundhand", "Brush Script MT", cursive; font-size: 15px; }

/* ── Квадрат направления тени ────────────────────────────────────────────────────────────
   Девять клеток вместо строки названий: направление - это место, и квадрат показывает его
   прямо. «Снизу-справа» в списке пришлось бы каждый раз мысленно переводить в картинку. */
.dir-pad {
  display: grid;
  grid-template-columns: repeat(3, 44px);
  gap: 4px;
  margin: 6px 0 14px;
}

.dir-pad button {
  height: 40px;
  border: 1.5px solid var(--border);
  border-radius: 10px 3px 10px 3px;
  background: none;
  color: var(--fg-dim);
  font: 600 15px/1 inherit;
  font-family: inherit;
  cursor: pointer;
  transition: border-color .2s, color .2s, background .2s;
}

.dir-pad button.dir-none { font-size: 11.5px; }

.dir-pad button.on {
  border-color: var(--pine);
  color: var(--pine);
  background: var(--pine-soft);
}

/* ── Живой пример прилипает к верху окна правки ──────────────────────────────────────────
   Настроек стало полтора десятка, на экран они не помещаются. При прокрутке вниз баннер
   уезжал, и человек тянул ползунок тени, не видя, что тень делает - настраивал вслепую и
   проверял, только закрыв окно. Теперь пример остаётся на месте, а под ним проезжает всё
   остальное.

   Отрицательные поля по бокам гасят внутренний отступ карточки: липкая шапка обязана
   закрывать содержимое ОТ КРАЯ ДО КРАЯ, иначе строки видны в двадцатипиксельных щелях
   слева и справа и это читается как поломка. */
.ad-preview-sticky {
  position: sticky;
  top: -14px;                /* заход под верх ровно на свой отступ - без щели над рамкой */
  z-index: 6;
  background: #fff;
  margin: 0 -16px 12px;
  padding: 14px 16px 8px;
  /* Тень только снизу и только на просвет: она показывает, что под шапкой что-то есть,
     и не рисует лишней рамки, когда прокрутка в самом верху. */
  box-shadow: 0 8px 12px -10px rgba(20, 24, 28, .5);
}

/* Кнопка под примером - компактная строкой, чтобы липкая шапка не съедала пол-экрана. */
.ad-preview-sticky .ad-photo-actions { margin-top: 8px; }
/* Объём убран (Кирилл 2026-08-27). 3D требовал стопки слоёв тени, градиента на самой букве
   и проверки, умеет ли браузер обрезать заливку по контуру текста, - много механики ради
   вида, который на баннере 3.2:1 и так почти не считывался. Контур делает ту же работу
   проще: отделяет букву от снимка, и его толщину можно вести плавно ползунком. Толщина
   приходит переменной --ad-stroke, правил под неё не нужно. */

/* ── Баннер без фото ─────────────────────────────────────────────────────────────────────
   Картинка в объявлении необязательна, и пост без неё выходит на тёмной подложке. Пример в
   форме заказа обязан показывать ровно это: раньше он просто прятался, а вместе с ним
   прятались все настройки - человек открывал форму и не видел ни шрифтов, ни теней.

   Цвет тот же, что у купленного баннера на первом экране (.ad-banner-sold), чтобы пример и
   результат совпадали, а не «примерно совпадали». */
.ad-banner.no-photo,
.ad-preview-banner.no-photo {
  background: linear-gradient(120deg, #16324f, #1f4d72);
  animation: none;
}

/* Свечение места продажи на своём же посте неуместно: место уже куплено. */
.ad-banner.no-photo .ad-banner-glow { display: none; }

/* Подсказка под рамкой, а не поверх примера. */
.ad-photo-hint { margin-top: 8px; text-align: center; }

/* ── Настройки, которым нужен снимок ─────────────────────────────────────────────────────
   Размытие фото и движение фона без картинки не могут сделать ничего. Раньше они прятались -
   и набор настроек выглядел разным до и после загрузки: высота панели прыгала, а элементы
   появлялись из ниоткуда. Теперь они на месте, но погашены: видно, что они есть и чего ждут.

   Прозрачность идёт вместе с pointer-events: ползунок, который двигается, но ни на что не
   влияет, врёт хуже, чем погашенный. Переход плавный - как только фото появляется, настройки
   оживают на глазах, а не возникают рывком. */
.style-locked {
  opacity: .3;
  pointer-events: none;
  filter: grayscale(1);
  transition: opacity .35s ease, filter .35s ease;
}

/* Обратный переход задан на самих полях, а не только на погашенном состоянии: иначе
   оживление было бы мгновенным, а гашение плавным - несимметрично и оттого заметно. */
#adEditBlurField,
#adEditBanimField {
  transition: opacity .35s ease, filter .35s ease;
}

/* Два действия над снимком в одну строку: поправить кадр (карандаш на самом снимке) и
   заменить его целиком. Раньше их пыталась делать одна кнопка, и поправить кадр было
   нечем - чтобы сдвинуть картинку, приходилось искать файл в галерее заново. */
.ad-photo-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}

.ad-photo-actions .idea-btn { margin-top: 0; }

/* Действия над поставщиком одной строкой: каждая кнопка во всю ширину съедала экран, а их
   до трёх. Удаление слева, одобрение справа - палец на телефоне чаще идёт вправо, и там
   должно стоять безобидное действие, а не то, которое стирает данные. */
.admin-sup-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.admin-sup-actions button {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  white-space: nowrap;
  padding-left: 6px;
  padding-right: 6px;
}

/* Одобрение - единственное здесь действие, которое ничего не ломает, поэтому оно и
   выглядит как обычная кнопка, а не как предупреждение. */
.admin-sup-actions .admin-approve-btn { color: var(--pine); font-weight: 600; }

/* ── Разбор записей о сбоях ──────────────────────────────────────────────────────────────
   Полсотни записей подряд читались как полсотни проблем. На деле сканеры, прямые заходы и
   медленная сеть выглядели так же тревожно, как ошибка в коде, - и понять, что именно чинить,
   было нельзя. Теперь приговор стоит ПЕРЕД подробностями, а сверху сводка. */
.boot-log-summary {
  padding: 10px 14px;
  margin-bottom: 10px;
  border-radius: 12px 3px 12px 3px;
  background: #FCEBEA;
  color: #8A2F28;
  font-size: 13.5px;
  font-weight: 600;
}

/* Когда чинить нечего - и цвет спокойный. Тревожный фон над строкой «ничего не нужно»
   противоречил бы собственному тексту. */
.boot-log-summary-ok { background: var(--pine-soft); color: var(--pine); }

.boot-log-verdict {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--fg-dim);
  margin-bottom: 6px;
  line-height: 1.35;
}

.boot-log-verdict-ours { color: #A3352C; }

/* ── Ссылки и статистика ─────────────────────────────────────────────────────
   Форма создания и карточки статистики. Графики рисуются инлайновым SVG:
   внешних библиотек в проекте нет и заводить их ради двух столбиков нельзя -
   любой CDN это ещё один домен, который у российского провайдера может не
   открыться. */
.link-form { display: flex; flex-direction: column; gap: 6px; }
.link-label { font-size: 14px; font-weight: 600; color: #1f2937; margin-top: 12px; }
.link-req { color: #dc2626; }
.link-input {
  width: 100%; box-sizing: border-box; padding: 12px 14px;
  border: 1px solid #e5e7eb; border-radius: 12px; font-size: 16px;
  background: #fff; color: #111827; outline: none;
}
.link-input:focus { border-color: #0f9d58; }
.link-hint { font-size: 13px; color: #6b7280; line-height: 1.4; margin-top: 4px; }

.link-modes { display: flex; gap: 8px; margin-top: 4px; }
.link-mode {
  flex: 1; padding: 12px 8px; border-radius: 12px; border: 1px solid #e5e7eb;
  background: #fff; color: #374151; font-size: 15px; font-weight: 600; cursor: pointer;
}
.link-mode.is-on { border-color: #0f9d58; background: #ecfdf5; color: #065f46; }

/* Карточка ссылки в списке */
.link-card { background: #fff; border-radius: 16px; padding: 14px 16px; margin-bottom: 10px; }
.link-card-title { font-size: 16px; font-weight: 700; color: #111827; }
.link-card-platform { font-size: 13px; color: #6b7280; margin-top: 2px; }
.link-url {
  font-size: 13px; color: #0f9d58; word-break: break-all; margin-top: 8px;
  background: #f9fafb; padding: 8px 10px; border-radius: 10px;
}
.link-funnel { display: flex; gap: 6px; margin-top: 10px; }
.link-step { flex: 1; text-align: center; background: #f9fafb; border-radius: 10px; padding: 8px 4px; }
.link-step-n { font-size: 18px; font-weight: 700; color: #111827; }
.link-step-l { font-size: 11px; color: #6b7280; margin-top: 2px; }
.link-actions { display: flex; gap: 8px; margin-top: 10px; }
.link-btn {
  flex: 1; padding: 10px; border-radius: 10px; border: 1px solid #e5e7eb;
  background: #fff; font-size: 14px; font-weight: 600; color: #374151; cursor: pointer;
}
.link-btn-main { background: #0f9d58; border-color: #0f9d58; color: #fff; }

/* Блоки статистики */
.stat-box { background: #fff; border-radius: 16px; padding: 14px 16px; margin-bottom: 10px; }
.stat-box-h { font-size: 14px; font-weight: 700; color: #111827; margin-bottom: 10px; }
.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 14px; }
.stat-row-name { color: #374151; }
.stat-row-val { font-weight: 700; color: #111827; }
.stat-money { font-size: 22px; font-weight: 700; color: #0f9d58; }
.stat-money-l { font-size: 12px; color: #6b7280; }
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.stat-cell { background: #f9fafb; border-radius: 12px; padding: 10px 12px; }

/* Горизонтальная полоска для разбивок */
.bar-wrap { margin: 6px 0; }
.bar-top { display: flex; justify-content: space-between; font-size: 13px; color: #374151; }
.bar-track { height: 6px; background: #f3f4f6; border-radius: 3px; margin-top: 4px; overflow: hidden; }
.bar-fill { height: 100%; background: #0f9d58; border-radius: 3px; }

/* Пояснение под выбором режима. Не мелкий серый хвостик, а заметный блок:
   от этого выбора зависит, увидит человек цену клиента или нет, и узнать об
   этом ПОСЛЕ запуска рекламы - значит потратить бюджет вслепую. */
.link-note {
  border-radius: 12px; padding: 12px 14px; margin-top: 10px;
  font-size: 13px; line-height: 1.5;
}
.link-note-ok { background: #ecfdf5; color: #065f46; }
.link-note-warn { background: #fffbeb; color: #92400e; }
.link-note b { font-weight: 700; }

/* ══════════════════════════════════════════════════════════════════════════
   Чаты рынков. Три вкладки сверху, лента, поле ввода снизу.
   Фото, видео и голосовых нет намеренно - решение Кирилла. Аватары и переход
   в профиль тоже: из чата нельзя провалиться в человека, бэкенд их и не отдаёт.
   ══════════════════════════════════════════════════════════════════════════ */

.home-chats-btn {
  width: 100%; margin: 4px 0 10px; padding: 15px 18px;
  border-radius: 18px 4px 18px 4px; border: 1px solid var(--border);
  background: var(--bg); color: var(--fg);
  font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  box-shadow: 0 2px 6px rgba(20, 24, 28, .06), 0 8px 20px rgba(20, 24, 28, .07);
}
.home-chats-badge {
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px;
  background: #dc2626; color: #fff; font-size: 12px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}

/* Чат - не обычный экран, а панель на всё свободное место под шапкой приложения.
   Причина: переключение рынков и кнопка отправки должны быть под рукой ВСЕГДА, а не
   уезжать вместе с историей. Поэтому прокручивается только лента; вкладки прибиты
   сверху, поле ввода - снизу. Верхнюю границу считает скрипт по фактическому низу
   шапки: шапка липкая, содержит рекламный баннер (то есть, то нет) и целиком прячется
   при открытой клавиатуре - заранее написанная формула ошибалась бы каждый раз. */
.chat-view {
  position: fixed; left: 0; right: 0; bottom: 0;
  max-width: 560px; margin: 0 auto;
  padding: 0 16px calc(8px + var(--safe-b));
  display: flex; flex-direction: column;
  z-index: 4;                 /* под шапкой (5), над остальным */
  background: var(--bg-page, #f5f5f7);
  box-sizing: border-box;
}
.chat-view.hidden { display: none; }

/* Шапка чата прибита: прокручивается только лента (chat-feed), а строка
   рынков остаётся на месте при любой длине переписки. */
.chat-head { flex: 0 0 auto; padding-top: 6px; }

/* Одна строка: «Назад», рынки, звук. Вертикаль на телефоне дороже горизонтали -
   каждая сэкономленная строка это ещё одно видимое сообщение.

   Всё в строке ровно 30 пикселей высотой и центрировано по одной линии. Раньше
   высота вкладки набиралась из полей и строки текста и выходила больше круглых
   кнопок - строка стояла ступенькой. Теперь высота задана числом всем троим,
   а знаки внутри круглых кнопок центрируются flex-ом, а не подбором полей:
   стрелка и колокольчик разной ширины, и на полях они вставали по-разному. */
.chat-bar { display: flex; align-items: center; gap: 6px; min-width: 0; }

.chat-back {
  flex: 0 0 auto; box-sizing: border-box; width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg);
  font-size: 15px; line-height: 1; cursor: pointer; font-family: inherit;
  padding: 0; display: flex; align-items: center; justify-content: center;
}

/* Кнопка звука. Оповещения о новых сообщениях легко надоедают, а искать выключатель
   в настройках человек не станет - он просто уйдёт из чата. Поэтому переключатель
   стоит там же, где сам чат. */
.chat-sound {
  flex: 0 0 auto; box-sizing: border-box; width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg);
  font-size: 13px; line-height: 1; cursor: pointer; font-family: inherit;
  padding: 0; display: flex; align-items: center; justify-content: center;
}
.chat-sound.is-off { opacity: .45; }

/* Черта стоит НАД строкой кнопок: отступ сверху ей больше не нужен - её
   и так отделяет поле шапки, - а снизу нужен, чтобы кнопки не лежали на ней. */
.chat-divider { height: 1px; background: var(--border); margin: 0 0 8px; }

/* Строку с именем рынка убрали: во вкладке оно и так подсвечено целиком,
   а повтор съедал высоту - на телефоне это минус одно видимое сообщение. */
.chat-foot { flex: 0 0 auto; padding-top: 6px; }

.chat-tabs { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; }
/* Шрифт мелкий намеренно: названия рынков должны помещаться ЦЕЛИКОМ, без сокращений.
   «Ю. Ворота» человек читает на полсекунды дольше, чем «Южные Ворота», а полсекунды
   на каждом переключении складываются в раздражение.

   Высота задана числом и равна высоте круглых кнопок по краям. Раньше она набиралась
   из полей и строки текста и выходила на четыре пикселя больше: три вкладки стояли
   выше «Назад» и звонка, и вся строка выглядела ступенькой.

   box-sizing здесь обязателен: рамка в один пиксель с каждой стороны иначе прибавляется
   К заданной высоте, и выравнивание, ради которого всё и делалось, снова уезжает.

   Многоточие - страховка на самый узкий экран, а не режим работы. На 360 и шире
   имена помещаются целиком; на 320 текст раньше вылезал за свою рамку и наезжал
   на соседнюю вкладку - это выглядело поломкой, а обрезанное слово выглядит
   обрезанным словом. */
.chat-tab {
  flex: 1 1 0; min-width: 0; box-sizing: border-box;
  height: 30px; padding: 0 4px; border-radius: 11px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-family: inherit; font-size: 11px; font-weight: 600; cursor: pointer;
  position: relative; white-space: nowrap; letter-spacing: -0.2px;
  /* Блок со строчной высотой, а НЕ flex-центрирование. В прошлый раз я поставил
     сюда flex вместе с многоточием - и многоточие не работало вовсе: во flex-
     контейнере голый текст становится безымянным элементом сетки, а text-overflow
     умеет резать только строчный поток. Рамка оставалась на месте, а слово
     по-прежнему вылезало за неё и наезжало на соседнюю вкладку.
     line-height 28 = высота 30 минус две рамки по пикселю: текст встаёт по центру
     сам, без второго механизма выравнивания. */
  display: block; text-align: center; line-height: 28px;
  overflow: hidden; text-overflow: ellipsis;
}
.chat-tab.is-on { border-color: var(--pine, #0f9d58); background: #ecfdf5; color: #065f46; }
.chat-tab-dot {
  position: absolute; top: 4px; right: 4px;
  width: 7px; height: 7px; border-radius: 50%; background: #dc2626;
}

/* Лента - единственное, что прокручивается. Высоту не задаём: её распределяет
   flex, забирая всё, что осталось между прибитыми вкладками и полем ввода. */
.chat-feed {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; gap: 8px;
  padding: 4px 0 8px;
  overscroll-behavior: contain;
}
/* Лента растёт СНИЗУ, как в Телеграме: пока сообщений мало, они прижаты
   к полю ввода, а не висят под шапкой посреди пустоты. Отступ на первом
   элементе, а не justify-content: flex-end - последний ломает прокрутку,
   когда сообщений становится больше экрана. */
.chat-feed > :first-child { margin-top: auto; }

.chat-day { text-align: center; font-size: 12px; color: var(--fg-dim); margin: 10px 0 2px; }

.chat-msg { max-width: 82%; align-self: flex-start; }
.chat-msg.is-mine { align-self: flex-end; }
/* Новое сообщение въезжает снизу и проявляется. Анимируем ТОЛЬКО свежие:
   полсотни пузырей истории, выезжающих разом, - это не появление, а рябь. */
.chat-msg.is-new { animation: chatIn .28s cubic-bezier(.2, .9, .3, 1.2) both; }
@keyframes chatIn {
  from { opacity: 0; transform: translateY(14px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-msg.is-new { animation: none; }
}

.chat-author {
  font-size: 12px; font-weight: 700; color: var(--pine, #0f9d58);
  margin: 0 0 3px 12px;
}
.chat-author-badge { font-size: 11px; }

.chat-bubble {
  padding: 9px 13px; border-radius: 16px 16px 16px 4px;
  background: var(--bg); color: var(--fg);
  font-size: 15px; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06);
}
.chat-msg.is-mine .chat-bubble {
  border-radius: 16px 16px 4px 16px;
  background: #ecfdf5; color: #06301f;
}
.chat-bubble.is-deleted { color: var(--fg-dim); font-style: italic; }

.chat-time { font-size: 11px; color: var(--fg-dim); margin: 3px 12px 0; }
.chat-msg.is-mine .chat-time { text-align: right; }

.chat-report {
  font-size: 11px; color: var(--fg-dim); margin: 2px 12px 0;
  background: none; border: none; padding: 0; cursor: pointer; font-family: inherit;
}

.chat-composer { display: flex; gap: 8px; align-items: flex-end; margin-top: 4px; }
.chat-input {
  flex: 1; box-sizing: border-box; padding: 11px 14px; resize: none;
  border: 1px solid var(--border); border-radius: 18px;
  font-family: inherit; font-size: 16px; line-height: 1.35;
  background: var(--bg); color: var(--fg); outline: none;
  max-height: 120px; overflow-y: auto;
}
.chat-input:focus { border-color: var(--pine, #0f9d58); }
.chat-send {
  width: 44px; height: 44px; flex-shrink: 0; border-radius: 50%;
  border: none; background: var(--pine, #0f9d58); color: #fff;
  font-size: 20px; font-weight: 700; cursor: pointer;
}
.chat-send:disabled { opacity: .4; }

.chat-note { font-size: 12px; color: var(--fg-dim); text-align: center; margin-top: 8px; min-height: 16px; }

/* Пока открыт чат, сама страница не прокручивается. Иначе «резиновая» прокрутка
   на телефоне утаскивает всю страницу вместе с прибитыми вкладками и полем ввода -
   именно то, от чего мы уходим. */
body.chat-open { overflow: hidden; }

/* ── Бизнес-инструменты плитками ─────────────────────────────────────────────
   Тот же вид, что на главной: экран перестал ощущаться отдельным приложением.
   Класс .home-tile общий, поэтому всё оформление - углы, значок фоном, появление
   снизу, «дыхание» - приезжает само, дублировать правила не нужно. */

/* Число под названием. У плиток главной подписи статичные, а здесь счётчик
   меняется скриптом - поэтому строка своя, не .home-tile-title. */
.home-tile-sub {
  font-size: 12px; font-weight: 600; color: var(--pine, #0f9d58);
  margin-top: 4px; position: relative; z-index: 2;
}

/* Раньше здесь значок замирал на плитках со счётчиком: движущийся сосед со своим слоем
   мешал iOS дорисовать меняющееся число, и оно обрубалось. Движение вернули - оно есть
   на главной, и раздел не должен от неё отличаться. Обрубленный текст лечится иначе:
   скрипт нудит перерисовку сразу после подстановки числа (см. repaintNudge). */

.tools-note {
  font-size: 13px; color: var(--fg-dim); line-height: 1.45;
  margin: 14px 4px 0;
}

/* Пока инструмент один, он занимает всю ширину: половина плитки рядом с пустотой
   читается как «вторую не загрузили». Появится второй - сетка сама станет в два
   столбца, править ничего не придётся. */
.home-grid .home-tile:only-child { grid-column: span 2; }

/* ── Ответы на сообщения ─────────────────────────────────────────────────────
   На телефоне - свайп вправо по пузырю, на компьютере - кнопка при наведении.
   Двух способов достаточно: длинное нажатие конфликтует с выделением текста,
   а меню по тапу мешало бы читать. */

/* Пузырь ездит за пальцем. transform, а не отступы: отступы пересчитывают
   раскладку каждый кадр и на слабом телефоне жест начинает дёргаться. */
.chat-msg { transition: transform .18s cubic-bezier(.2, .7, .3, 1); }
.chat-msg.is-swiping { transition: none; }

/* Стрелка проступает из-под пузыря по мере свайпа. */
.chat-msg { position: relative; }
.chat-msg::before {
  content: "\21A9";
  position: absolute; left: -30px; top: 50%; transform: translateY(-50%);
  font-size: 16px; color: var(--pine, #0f9d58);
  opacity: 0; transition: opacity .12s; pointer-events: none;
}
.chat-msg.is-mine::before { left: auto; right: -30px; }
.chat-msg.swipe-armed::before { opacity: 1; }

/* Кнопка ответа для компьютера. На телефоне наведения не бывает, поэтому
   она там не появится и место не займёт. */
.chat-reply-btn {
  position: absolute; top: 2px; right: -26px;
  width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-size: 11px; line-height: 1; cursor: pointer; font-family: inherit;
  opacity: 0; transition: opacity .15s;
}
.chat-msg.is-mine .chat-reply-btn { right: auto; left: -26px; }
.chat-msg:hover .chat-reply-btn { opacity: 1; }
@media (hover: none) { .chat-reply-btn { display: none; } }

/* Цитата внутри пузыря. Полоска слева - тот же приём, что во всех мессенджерах:
   человеку не нужно объяснять, что это ответ. */
.chat-quote {
  border-left: 2px solid var(--pine, #0f9d58);
  padding: 2px 0 2px 8px; margin-bottom: 5px;
  font-size: 13px; line-height: 1.3; cursor: pointer;
}
.chat-quote-who { font-weight: 700; color: var(--pine, #0f9d58); }
.chat-quote-text { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-quote.is-deleted .chat-quote-text { font-style: italic; }

/* Оригинал коротко подсвечивается, когда к нему перешли по цитате -
   иначе непонятно, куда именно тебя перенесло. */
@keyframes chat-flash {
  0%, 100% { background: transparent; }
  30%      { background: rgba(15, 157, 88, .16); }
}
.chat-msg.is-flash .chat-bubble { animation: chat-flash 1.1s ease-out; }

/* Панель над полем ввода: на что отвечаем. */
.chat-reply-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; margin-bottom: 6px;
  background: var(--bg); border-radius: 12px;
  border-left: 3px solid var(--pine, #0f9d58);
}
.chat-reply-bar.hidden { display: none; }
.chat-reply-body { flex: 1 1 auto; min-width: 0; }
.chat-reply-who { font-size: 12px; font-weight: 700; color: var(--pine, #0f9d58); }
.chat-reply-text {
  font-size: 13px; color: var(--fg-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chat-reply-x {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  border: none; background: transparent; color: var(--fg-dim);
  font-size: 19px; line-height: 1; cursor: pointer; font-family: inherit;
}

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

/* Рамка с фото. Высота ограничена: снимок здесь нужен, чтобы убедиться «то самое
   фото загрузилось», а не рассматривать товар - под это хватает трети экрана. */
.desc-drop {
  width: 100%; max-height: 220px; border-radius: 16px;
  background: var(--bg); overflow: hidden; margin-bottom: 10px;
}
.desc-drop.hidden { display: none; }
.desc-preview { width: 100%; max-height: 220px; object-fit: cover; display: block; }

/* Два отдельных выбора «снять / из галереи» убраны: телефон показывает этот выбор
   сам, а лишний шаг заставлял решать то, что система решает лучше. Рамка нажимается
   целиком, пока фото нет - мишень совпадает с надписью. */
.desc-drop { cursor: pointer; }

/* Блок результата. Кнопка копирования справа сверху - человек пришёл сюда
   за текстом, который унесёт в объявление, поэтому копирование главнее чтения. */
.desc-block {
  background: var(--bg); border-radius: 16px;
  padding: 14px 16px; margin-bottom: 12px;
}
.desc-block-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
.desc-block-title { font-size: 13px; font-weight: 700; color: var(--fg-dim); }
.desc-block-body { font-size: 15px; line-height: 1.5; color: var(--fg); white-space: pre-wrap; }

.copy-btn {
  width: 32px; height: 32px; flex: 0 0 auto; border-radius: 9px;
  border: 1px solid var(--border); background: var(--bg);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0;
}
.copy-btn.is-done { border-color: var(--pine, #0f9d58); background: #ecfdf5; }
.copy-ico {
  width: 16px; height: 16px; fill: none;
  stroke: var(--fg-dim); stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.copy-btn.is-done .copy-ico { stroke: var(--pine, #0f9d58); }

/* Квадратики превращаются в галочку. Обе фигуры лежат в одном значке и меняются
   местами по классу is-done: одна уезжает с поворотом, вторая приезжает.
   transform-box: fill-box обязателен - иначе поворот считается от угла холста
   SVG и фигура улетает за пределы кнопки. */
.copy-ico .ico-copy, .copy-ico .ico-ok {
  transform-box: fill-box; transform-origin: center;
  transition: opacity .16s ease, transform .28s cubic-bezier(.2, .9, .3, 1.5);
}
.copy-ico .ico-ok { opacity: 0; transform: scale(.3) rotate(-70deg); }
.is-done .copy-ico .ico-copy { opacity: 0; transform: scale(.3) rotate(70deg); }
.is-done .copy-ico .ico-ok { opacity: 1; transform: none; }

@keyframes copyPop {
  0%   { transform: scale(1); }
  38%  { transform: scale(.84); }
  100% { transform: scale(1); }
}
.copy-btn.is-done { animation: copyPop .28s cubic-bezier(.2, .9, .3, 1.4); }

/* Текстовая кнопка копирования подтверждает подписью - значка в ней нет.
   Цвет тот же, чтобы подтверждение читалось одинаково везде. */
.link-btn.is-done {
  border-color: var(--pine, #0f9d58); background: #ecfdf5; color: #065f46;
}

/* Что модель узнала на фото. Показываем над текстом: человек сначала должен
   убедиться, что товар определён верно, и только потом читать описание -
   если распознали не то, читать дальше незачем. */
.desc-found {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  margin-bottom: 10px; padding: 0 2px;
}
.desc-found.hidden { display: none; }
.desc-found-title { font-size: 17px; font-weight: 700; color: var(--fg); }
.desc-found-cat { font-size: 13px; color: var(--fg-dim); }

/* Дата заявки в углу карточки модерации. Без неё непонятно, ждёт человек час
   или третьи сутки - а от этого зависит, кого разбирать первым. */
.admin-person-when {
  margin-left: auto; flex: 0 0 auto; text-align: right;
  font-size: 11.5px; line-height: 1.35; color: var(--fg-dim);
  white-space: pre-line;
}

/* Кнопка заработка на экране описания. Не главное действие, поэтому светлая:
   человек пришёл за описанием, а не за баллами - но выход у него быть должен. */
.desc-earn-btn {
  background: var(--bg); color: var(--pine, #0f9d58);
  border: 1px solid var(--pine, #0f9d58); margin-top: 8px;
}
.desc-earn-btn.hidden { display: none; }

/* ── Кампании: группировка ссылок по компании, направлениям и площадкам ──────── */
.camp-sec-h {
  display: flex; align-items: baseline; justify-content: space-between;
  font-size: 15px; font-weight: 700; color: #111827; margin: 22px 0 8px;
}
.camp-sec-n { font-size: 13px; font-weight: 600; color: #9ca3af; }

.camp-wide-btn { width: 100%; margin-top: 8px; }
/* Кнопка архива идёт сразу за чертой - свой верхний отступ ей не нужен,
   промежуток списка уже есть. */
.earn-list > .camp-sep + .camp-wide-btn { margin-top: 0; }

.camp-row {
  background: #f9fafb; border: 1px solid #eef0f3; border-radius: 14px;
  padding: 12px; margin-bottom: 10px; display: flex; flex-direction: column; gap: 8px;
}
.camp-row-h { display: flex; align-items: center; justify-content: space-between; }
.camp-row-n { font-size: 13px; font-weight: 700; color: #6b7280; }
.camp-row-x {
  border: none; background: transparent; color: #9ca3af;
  font-size: 22px; line-height: 1; padding: 0 4px; cursor: pointer;
}
.camp-row-x:active { color: #dc2626; }
.camp-row .link-input { background: #fff; }
.camp-row .link-input:disabled { background: #f3f4f6; color: #6b7280; }
.camp-row .link-hint { margin-top: 0; }

.camp-mode {
  flex: 1; padding: 11px 8px; border-radius: 12px; border: 1px solid #e5e7eb;
  background: #fff; color: #374151; font-size: 14px; font-weight: 600; cursor: pointer;
}
.camp-mode.is-on { border-color: #0f9d58; background: #ecfdf5; color: #065f46; }
.camp-mode.is-locked { opacity: .65; cursor: default; }

.camp-total {
  margin-top: 18px; padding: 12px 14px; border-radius: 12px;
  background: #ecfdf5; color: #065f46; font-size: 14px; font-weight: 600;
}
.camp-total:empty { display: none; }

.camp-card { border: 1px solid #eef0f3; }
.camp-cpl { margin-top: 10px; font-size: 13px; font-weight: 600; color: #0f9d58; }
.camp-sub { display: block; font-size: 12px; font-weight: 400; color: #9ca3af; margin-top: 2px; }

.camp-link { padding: 10px 0; border-top: 1px solid #f3f4f6; }
.camp-link:first-of-type { border-top: none; }
.camp-link-name { font-size: 14px; font-weight: 600; color: #374151; }

/* Адрес и кнопки в одну строку. Две кнопки во всю ширину под каждой ссылкой
   съедали экран: у площадки с тремя ссылками это шесть широких прямоугольников
   подряд. Край строки рядом с адресом всё равно пустовал - туда их и убрали. */
.camp-link-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.camp-link-row .link-url { flex: 1 1 auto; min-width: 0; margin-top: 0; }
.camp-link-btn { width: 34px; height: 34px; }

/* Черта между действующими площадками и архивом: выше то, что ведут сейчас,
   ниже то, что убрали. Одна линия дешевле заголовка - раздел и так понятен
   по подписи кнопки. */
/* Черта прижата к обеим сторонам. Список идёт с промежутком в 12px между
   карточками, и собственные отступы черты складывались с ним - между последней
   площадкой и архивом получалась дыра в тридцать пикселей на пустом месте.
   Отрицательные поля съедают общий промежуток обратно. */
.camp-sep { height: 1px; background: var(--border); margin: -4px 12px -2px; }
/* В ленте товаров у черты свои отступы: там сетка карточек, а не список
   с общим промежутком, и отрицательные поля сплющили бы её с последним рядом. */
#catalogMySep { margin: 16px 12px 12px; }
#catalogMySep.hidden { display: none; }

/* Номера страниц архива. Ряд прокручивается вбок, а не переносится: перенос
   на второй ряд человек читает как два разных набора страниц. */
.pager-row {
  display: flex; align-items: center; gap: 6px; margin: 14px 4px 4px;
  overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 2px;
}
.pager-row::-webkit-scrollbar { display: none; }
.pager-btn {
  flex: 0 0 auto; min-width: 36px; height: 36px; padding: 0 8px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  transition: background .18s ease, border-color .18s ease, color .18s ease,
              transform .18s cubic-bezier(.2, .9, .3, 1.5);
}
.pager-btn:active { transform: scale(.92); }
.pager-btn.is-on {
  border-color: var(--pine, #0f9d58); background: var(--pine, #0f9d58); color: #fff;
}
.pager-gap { flex: 0 0 auto; color: var(--fg-dim); font-size: 14px; padding: 0 2px; }

/* Один блок графика с переключателем. Два блока подряд занимали два экрана
   и заставляли листать, хотя вопросы «растёт ли» и «в какое время заходят»
   человек задаёт по очереди, а не одновременно. */
.chart-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 10px;
}
.chart-tabs { display: flex; gap: 4px; flex: 0 0 auto; }
.chart-tab {
  padding: 5px 10px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  transition: background .18s ease, border-color .18s ease, color .18s ease,
              transform .18s cubic-bezier(.2, .9, .3, 1.5);
}
.chart-tab:active { transform: scale(.93); }
.chart-tab.is-on {
  border-color: var(--pine, #0f9d58); background: #ecfdf5; color: #065f46;
}
.chart-body {
  position: relative;
  /* Вертикальную прокрутку странице оставляем, горизонтальное движение
     забираем себе. Без этого браузер на первом же сдвиге пальца решал,
     что человек листает страницу, и обрывал жест - показания пропадали,
     не успев появиться. */
  touch-action: pan-y;
  animation: desc-in .26s cubic-bezier(.2, .7, .3, 1) both;
}

/* Показания под пальцем. Высоту столбика на глаз в число не переведёшь,
   а именно число человеку и нужно. Наведения на телефоне нет - работаем
   по касанию и по ведению. */
.chart-guide {
  position: absolute; top: 0; bottom: 18px; width: 1px;
  background: var(--pine, #0f9d58); opacity: .45; pointer-events: none;
}
.chart-guide.hidden { display: none; }
/* Тёмная подсказка осталась от графика кампаний, где она всплывает НАД
   линией и должна перебивать её собой. На графике рекламы она лежит в углу
   самой карточки - и чёрный прямоугольник в светлой карточке выглядит
   заплаткой из другого приложения.

   Правило оставлено только для графика кампаний, где оно и родилось. */
#campStatsBody .chart-tip,
.camp-chart .chart-tip {
  position: absolute; top: -6px; z-index: 3; pointer-events: none;
  background: #111827; color: #fff; border-radius: 8px;
  padding: 5px 9px; font-size: 11.5px; line-height: 1.25; white-space: nowrap;
  box-shadow: 0 4px 14px rgba(17, 24, 39, .22);
}
.chart-tip b { font-weight: 700; }
.chart-tip.hidden { display: none; }

/* Плитки статистики. Те, у которых есть ряд по дням, - переключатели линий
   на графике, и это должно быть видно до нажатия: точка сверху и цветная
   рамка во включённом виде. */
/* Плитки под графиком - это ручки управления им, а не главное на экране.
   Поэтому мельче: главное здесь картина, а не сами цифры. По четыре в ряд
   вместо трёх - весь набор влезает в два ряда вместо трёх. */
.tile-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-bottom: 10px;
}
.stat-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 1px; padding: 7px 3px; border-radius: 10px;
  border: 1px solid transparent; background: #f9fafb;
  font-family: inherit; cursor: pointer; text-align: center;
  transition: border-color .18s ease, background .18s ease,
              transform .18s cubic-bezier(.2, .9, .3, 1.5);
}
.stat-tile:active { transform: scale(.95); }
.stat-tile-n {
  font-size: 14px; font-weight: 700; color: #111827; letter-spacing: -.3px;
  white-space: nowrap;
}
.stat-tile-l { font-size: 9.5px; color: var(--fg-dim); line-height: 1.15; }
.stat-tile-dot {
  position: absolute; top: 4px; right: 4px; width: 5px; height: 5px; border-radius: 50%;
  background: var(--tile-line, #0f9d58); opacity: .28;
}
.stat-tile.is-on {
  border-color: var(--tile-line, #0f9d58); background: #fff;
}
.stat-tile.is-on .stat-tile-dot { opacity: 1; }

/* Ряда нет в текущем разрезе - плитка гаснет. Включённая плитка без линии
   на графике читается как поломка, а не как «этих данных тут не бывает». */
.stat-tile.is-off { opacity: .45; }
.stat-tile.is-off .stat-tile-dot { display: none; }

/* Пояснение к цифре без графика. Появляется под плитками по нажатию. */
.tile-help {
  font-size: 12.5px; line-height: 1.45; color: var(--fg-dim);
  background: #f9fafb; border-radius: 10px; padding: 10px 12px; margin-bottom: 10px;
  animation: desc-in .24s cubic-bezier(.2, .7, .3, 1) both;
}
.tile-help.hidden { display: none; }

/* Легенда графика: какая линия что значит. */
/* В легенде стоит и название, и ЗНАЧЕНИЕ. Без пальца - последняя точка ряда,
   с пальцем - та, на которой он стоит. Иначе единственный способ узнать число -
   попасть в точку, а на графике из трёхсот точек это лотерея. */
.chart-legend { display: flex; flex-wrap: wrap; gap: 12px; margin: 8px 0 2px; }
.chart-leg { display: inline-flex; align-items: baseline; gap: 5px; font-size: 12px; color: var(--fg-dim); }
.chart-leg i {
  width: 10px; height: 3px; border-radius: 2px; display: inline-block;
  align-self: center; flex: 0 0 auto;
}
.chart-leg-v {
  font-size: 13px; font-weight: 700; color: var(--fg);
  font-variant-numeric: tabular-nums;
}
/* Пока палец на графике, значения относятся к точке под ним, а не к «сейчас» -
   подсвечиваем, чтобы это не читалось как текущее состояние. */
.chart-legend.is-marked .chart-leg-v { color: var(--pine, #0f9d58); }

/* Время точки - одно на все ряды, стоит первым. Оформлено как метка,
   а не как ряд: у него нет цветной полоски, и спутать его с линией нельзя. */
.chart-leg-time {
  font-size: 12px; font-weight: 700; color: var(--fg);
  background: var(--bg-page, #f5f5f7); border-radius: 999px; padding: 2px 9px;
  font-variant-numeric: tabular-nums;
}
.chart-legend.is-marked .chart-leg-time {
  background: #ecfdf5; color: #065f46;
}

/* Подпись под названием плитки. Отдельно от .home-tile-sub: та зелёная и жирная,
   потому что показывает счётчик - число должно бросаться в глаза. Здесь же обычный
   текст, и зелёным он читался бы как ссылка. */
.home-tile-desc {
  font-size: 11.5px; line-height: 1.35; color: var(--fg-dim);
  margin-top: 4px; position: relative; z-index: 2;
}

/* Кнопки правки и снятия над карточкой размещения. Появляются только у будущих
   постов: идущее и прошедшее трогать нечего - его уже видели, деньги отработаны. */
.ad-mine-acts { display: flex; gap: 6px; margin-left: auto; }
.ad-mine-act {
  width: 28px; height: 28px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-size: 14px; line-height: 1; cursor: pointer; font-family: inherit;
  display: inline-flex; align-items: center; justify-content: center;
}
.ad-mine-act.is-danger { color: #dc2626; border-color: #fecaca; }

/* Поле для вставки поста. Высокое намеренно: пост в несколько абзацев должен быть
   виден целиком, иначе человек не заметит, что вставил не всё. */
.camp-post {
  min-height: 110px; resize: vertical; line-height: 1.45;
  font-family: inherit; font-size: 14px;
}

/* Готовый текст поста. Моноширинный не нужен, а вот переносы как в оригинале -
   обязательны: человек копирует это целиком и публикует без правок. */
.camp-ready {
  font-size: 13.5px; line-height: 1.5; white-space: pre-wrap;
  color: var(--fg); word-break: break-word;
  background: var(--bg-page, #f5f5f7); border-radius: 10px; padding: 10px 12px;
}

/* Шапка раздела кампаний: «Назад» и «Новая кампания» в одну строку.
   Заголовок убран - название раздела человек только что читал на плитке,
   а место на экране дороже повторения. */
/* Обе кнопки одного размера. У «Назад» свой нижний отступ - внутри этой строки он
   лишний, иначе кнопки стоят на разной высоте и выглядят разными. Метрики берём
   ровно те же, что у .back-btn: шрифт, отступы, скругление. */
.links-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.links-bar .back-btn { margin-bottom: 0; }
.links-new-btn {
  margin-left: auto;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 18px 9px 14px; border-radius: 999px;
  border: none; background: var(--pine, #0f9d58); color: #fff;
  font-family: inherit; font-size: 14.5px; font-weight: 600; cursor: pointer;
  white-space: nowrap;
}
.links-new-btn:active { transform: scale(0.95); }
/* Блик на круглой кнопке должен обрезаться по её скруглению, иначе полоса
   света вылезает за края и выглядит артефактом. */
.links-new-btn.btn-shine { border-radius: 999px; overflow: hidden; }

/* Процент под шагом воронки. Мельче числа и серым: главное - сколько человек,
   доля лишь помогает понять, много это или мало. */
.link-step-p { font-size: 11px; color: var(--fg-dim); margin-top: 2px; }

/* Цена результата - самое важное число карточки, поэтому оно крупное и стоит
   отдельно от воронки. Бюджет и переходы человек всё равно делит в уме;
   единственное, что он потом помнит и с чем сравнивает - вот эта цифра. */
.camp-price {
  display: flex; align-items: baseline; gap: 8px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
.camp-price-n { font-size: 22px; font-weight: 700; color: var(--pine, #0f9d58); }
.camp-price-l { font-size: 13px; color: var(--fg-dim); }

/* Название кампании строкой, а не заголовком: на экран уже смотрит кнопка
   «Добавить площадку», и второй крупный элемент рядом спорит с ней за внимание. */
.camp-title-line {
  font-size: 15px; font-weight: 700; color: var(--fg);
  margin: 0 4px 12px; text-align: right;
}

/* Карточка площадки. Крестик и галочка справа от названия - по ним видно,
   вышел пост или нет, не вчитываясь в цифры. */
.place-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.place-name {
  flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 700; color: var(--fg);
  word-break: break-all;
}
.place-mark {
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700;
}
.place-mark.is-ok { background: #ecfdf5; color: #0f9d58; }
.place-mark.is-no { background: #fef2f2; color: #dc2626; }
.place-stats { display: flex; gap: 6px; }

/* Название кампании строкой с карандашом. Заголовка больше нет: человек только что
   нажал на эту кампанию в списке и помнит, куда зашёл. */
.camp-title-row { display: flex; align-items: center; gap: 8px; margin: 0 4px 10px; }

/* Итог генерации. Появляется мягко и один раз - на любое следующее действие
   внутри кампании экран перерисовывается уже без неё. */
.camp-done {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 4px 12px; padding: 11px 13px; border-radius: 12px;
  background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46;
  font-size: 13px; font-weight: 600; line-height: 1.4;
  animation: campDoneIn .42s cubic-bezier(.2, .9, .3, 1.2) both;
}
.camp-done-ico {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
  background: var(--pine, #0f9d58); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
  animation: campDonePop .5s cubic-bezier(.2, .9, .3, 1.6) .12s both;
}
@keyframes campDoneIn {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes campDonePop {
  0%   { transform: scale(0) rotate(-90deg); }
  60%  { transform: scale(1.18) rotate(0deg); }
  100% { transform: scale(1); }
}
.camp-title-txt { flex: 1 1 auto; font-size: 15px; font-weight: 700; color: var(--fg); }
.camp-title-edit {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-size: 13px; cursor: pointer; font-family: inherit;
}

/* Дата публикации со значком состояния - одной кнопкой рядом с названием. Отдельная
   строка ввода занимала место ради значения, которое ставят один раз и не трогают.
   Поле datetime-local спрятано: оно нужно только чтобы открыть системный выбор даты. */
.place-when {
  flex: 0 0 auto; padding: 5px 10px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg-dim);
  font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  white-space: nowrap;
}
.place-when.is-ok { border-color: #a7f3d0; background: #ecfdf5; color: #065f46; }
.place-when.is-late { border-color: #fecaca; background: #fef2f2; color: #b91c1c; }
.place-when.is-wait { color: var(--fg); }
/* Панель настроек площадки. Раскрывается по нажатию на дату: настройки трогают
   один раз, а место занимали бы постоянно. */
.place-panel {
  margin: 10px 0 4px; padding: 12px; border-radius: 12px;
  background: var(--bg-page, #f5f5f7);
  animation: desc-in .3s cubic-bezier(.2, .7, .3, 1) both;
}
.place-panel.hidden { display: none; }

/* Подтверждение публикации - в самой карточке. Системное окно всплывало поверх
   всего, выглядело чужим и забирало фокус, после чего экран перерисовывался
   и прокрутка улетала наверх. */
.place-confirm {
  margin: 10px 0 4px; padding: 12px; border-radius: 12px;
  background: #ecfdf5; border: 1px solid #a7f3d0;
  animation: desc-in .28s cubic-bezier(.2, .7, .3, 1) both;
}
.place-confirm.hidden { display: none; }
.place-confirm-t { font-size: 13px; line-height: 1.45; color: #065f46; margin-bottom: 10px; }
.place-confirm-a { display: flex; gap: 8px; }
.place-panel-row { display: flex; gap: 8px; }
/* Поля панели - в столбик и во всю ширину. Раньше они стояли парами с зелёной
   кнопкой сохранения сбоку: три «ОК» подряд читались как форма, а не как правка
   одной карточки. */
.place-panel-in { width: 100%; margin-bottom: 8px; }
.place-panel .camp-when-box { margin: 0 0 4px; }

/* Часы - метка состояния, а не кнопка: правка открывается только карандашом. */
.place-when { cursor: default; }
.place-in {
  flex: 1 1 0; min-width: 0; padding: 9px 10px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg);
  font-family: inherit; font-size: 14px;
}
/* Контакт админа - строкой в самом низу карточки, рядом со ссылками этой же
   площадки. Поле и кнопка в одну линию: заполняют один раз, пишут регулярно. */
.place-adm-row { display: flex; gap: 8px; margin-top: 12px; }
.place-adm-in { flex: 1 1 auto; min-width: 0; }
.place-write {
  flex: 0 0 auto; padding: 9px 14px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--bg); color: var(--fg);
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  white-space: nowrap;
}

/* Название площадки стало кнопкой - по нему открывается статистика. Выглядит как
   текст: подчёркивание и цвет ссылки в карточке со своими цифрами были бы шумом. */
/* Название одной строкой с многоточием. break-all рвал адрес по буквам, и при
   узком месте он превращался в столбик из отдельных знаков. */
.place-name {
  flex: 1 1 auto; min-width: 0; text-align: left; padding: 0;
  border: none; background: none; cursor: pointer;
  font-family: inherit; font-size: 14px; font-weight: 700; color: var(--fg);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Кнопки отдельной строкой под названием: вчетвером в одной строке с именем
   они не оставляли ему места. */
.place-tools {
  display: flex; align-items: center; gap: 6px;
  margin: 8px 0 10px; flex-wrap: wrap;
}
.place-tools .place-when { margin-right: auto; }
.place-edit {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg);
  cursor: pointer; font-family: inherit; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.place-edit-ico {
  width: 17px; height: 17px; fill: none; stroke: var(--fg-dim);
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.place-edit.is-on { background: var(--pine, #0f9d58); border-color: var(--pine, #0f9d58); }
.place-edit.is-on .place-edit-ico { stroke: #fff; }

/* Карандаш превращается в галочку. Обе фигуры лежат в одном значке и меняются
   местами по классу is-on: подмена картинки браузеру анимировать нечем, а тут
   есть что - одна уезжает с поворотом, вторая приезжает.
   transform-box нужен, чтобы поворот считался от самой фигуры, а не от угла
   холста SVG - иначе значок улетает за пределы кнопки. */
.edit-morph .ico-pen, .edit-morph .ico-ok {
  transform-box: fill-box; transform-origin: center;
  transition: opacity .16s ease, transform .28s cubic-bezier(.2, .9, .3, 1.5);
}
.edit-morph .ico-ok { opacity: 0; transform: scale(.3) rotate(-70deg); }
.is-on .edit-morph .ico-pen { opacity: 0; transform: scale(.3) rotate(70deg); }
.is-on .edit-morph .ico-ok { opacity: 1; transform: none; }

/* Короткий щелчок самой кнопки в момент переключения: без него превращение
   выглядит как самопроизвольная смена значка, а не как ответ на нажатие. */
@keyframes editPop {
  0%   { transform: scale(1); }
  38%  { transform: scale(.84); }
  100% { transform: scale(1); }
}
.place-edit.is-pop { animation: editPop .28s cubic-bezier(.2, .9, .3, 1.4); }

/* Карандаш у названия кампании теперь такая же кнопка, как в списке. */
.place-edit.camp-title-edit { width: 30px; height: 30px; font-size: 0; }

/* Стрелка разворота - крайняя справа. В свёрнутом виде карточка показывает только
   то, по чему сравнивают площадки: имя, время, клики, цену, расход. Готовый пост,
   ссылки и контакт нужны реже, и платить за них высотой экрана не надо. */
.place-chev {
  flex: 0 0 auto; width: 30px; height: 34px; border: none; background: none;
  color: var(--fg-dim); font-size: 22px; line-height: 1; cursor: pointer;
  font-family: inherit; padding: 0;
  transition: transform .2s;
}
.place-chev.is-open { transform: rotate(90deg); }
.place-more { animation: desc-in .3s cubic-bezier(.2, .7, .3, 1) both; }
.place-more.hidden { display: none; }

/* Что распознали на фото. Стоит ДО кнопки оплаты: человек должен убедиться,
   что это его товар, прежде чем платить. Списать деньги за чужой товар -
   худший способ потерять человека навсегда. */
.desc-reco {
  background: #ecfdf5; border-radius: 14px; padding: 12px 14px; margin-bottom: 10px;
}
.desc-reco.hidden { display: none; }
.desc-reco-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
}
.desc-reco-row + .desc-reco-row { margin-top: 6px; padding-top: 6px; border-top: 1px solid #d1fae5; }
.desc-reco-l { font-size: 11px; color: #065f46; flex: 0 0 auto; }
/* Мельче и с переносом. Название товара от модели длинное - «Носки мужские
   серые в рубчик хлопок», - и прежним кеглем оно обрезалось на полуслове. */
.desc-reco-v {
  font-size: 12.5px; font-weight: 700; color: #06301f; line-height: 1.3;
  text-align: right; word-break: break-word; min-width: 0;
}

/* «Заменить фото» после загрузки уходит на второй план: главное действие теперь
   ниже, и две одинаково яркие кнопки спорили бы за нажатие. */
.desc-pick-secondary {
  background: var(--bg); color: var(--fg-dim);
  border: 1px solid var(--border);
}

/* Вводная строка. Не серый хвостик под кнопкой, а нормальный текст сверху:
   человек сначала должен понять, что тут вообще происходит, и только потом
   увидеть кнопку. */
.desc-lead {
  font-size: 14px; line-height: 1.5; color: var(--fg-dim);
  margin: 0 2px 14px;
}
.desc-lead.hidden { display: none; }

/* Разделитель между «дать фото» и «получить описание». Два действия подряд
   без границы читались как одно, и серая кнопка выглядела поломкой первой. */
.desc-divider { height: 1px; background: var(--border); margin: 18px 2px 16px; }

/* Дышим: кнопки на этом экране стояли впритык друг к другу. */
#descStep1 .submit-btn { margin-top: 0; }
#descStep2Box .submit-btn { margin-top: 2px; }
#descStep2Box .link-hint { margin-top: 8px; }

/* Живые статусы, пока модель смотрит фото. Несколько секунд немого экрана человек
   читает как «зависло» - строка со сменяющимся текстом отвечает на вопрос
   «оно вообще работает» раньше, чем он успеет его задать. */
.desc-status {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; border-radius: 14px;
  background: var(--bg); margin-top: 10px;
}
.desc-status.hidden { display: none; }

/* Подсказка под кнопкой держит свою высоту, даже когда пуста. Иначе её очистка
   укорачивает страницу, браузер прижимает прокрутку ко дну - и экран прыгает
   вверх ровно в момент нажатия на кнопку. */
#descHint { min-height: 1.35em; }

/* Оценка описания: два больших пальца под результатом. Крупные - в них целятся
   одним движением, не глядя, сразу после чтения текста. */
.desc-rate {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 4px 0 12px; padding: 10px 14px; border-radius: 14px;
  background: var(--bg);
  transition: opacity .3s ease, transform .3s ease;
}
.desc-rate.is-done { opacity: 0; transform: translateY(-6px); }
.desc-rate.hidden { display: none; }
.desc-rate-q { font-size: 13.5px; color: var(--fg-dim); }
.desc-rate-btns { display: flex; gap: 8px; flex: 0 0 auto; }
.desc-rate-btn {
  width: 44px; height: 40px; border-radius: 12px; font-size: 21px; line-height: 1;
  border: 1px solid var(--border); background: var(--card); cursor: pointer;
  transition: transform .18s cubic-bezier(.2, .9, .3, 1.5), border-color .18s ease;
}
.desc-rate-btn:active { transform: scale(.9); }

/* Всплывающее сообщение по центру. Живёт поверх всего и НЕ двигает вёрстку:
   строка, вставленная в поток, сдвинула бы текст, который человек читает. */
.toast {
  position: fixed; left: 50%; top: 50%; z-index: 120;
  transform: translate(-50%, -50%);
  background: rgba(17, 24, 39, .92); color: #fff;
  padding: 14px 20px; border-radius: 14px;
  font-size: 15px; font-weight: 600; text-align: center; max-width: 80vw;
  box-shadow: 0 14px 40px rgba(17, 24, 39, .3);
  animation: toastIn .34s cubic-bezier(.2, .9, .3, 1.5) both;
  pointer-events: none;
}
.toast.is-out { animation: toastOut .3s cubic-bezier(.4, 0, .8, .4) both; }
@keyframes toastIn {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.7); }
  60%  { opacity: 1; transform: translate(-50%, -50%) scale(1.06); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes toastOut {
  from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  to   { opacity: 0; transform: translate(-50%, -130%) scale(.9); }
}

/* Пояснение под вопросом в форме отзыва. */
.feedback-lead { font-size: 13.5px; color: var(--fg-dim); margin-bottom: 10px; }
.feedback-lead.hidden { display: none; }

/* Уход формы за экран после отправки. */
#feedbackOverlay.is-out .modal-card {
  animation: sheetOut .3s cubic-bezier(.4, 0, .8, .4) both;
}
@keyframes sheetOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(60px) scale(.97); }
}
/* Спокойный вращающийся ободок вместо пульсирующей точки: точка прыгала в размере
   и на фоне сменяющегося текста получалось два движения сразу - глазу не за что
   зацепиться, и всё вместе читалось как дёрганье. */
.desc-status-dot {
  width: 15px; height: 15px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid var(--border);
  border-top-color: var(--pine, #0f9d58);
  animation: desc-spin .8s linear infinite;
}
@keyframes desc-spin { to { transform: rotate(360deg); } }
/* Строка статуса. Высота держится минимум в одну строку, чтобы блок
   не подпрыгивал, когда фраза короче предыдущей. min-width: 0 обязателен -
   без него длинная фраза в flex-строке не переносится, а вылезает наружу
   и наезжает на соседей. */
.desc-status-txt {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px; color: var(--fg); line-height: 1.35; min-height: 1.35em;
}
/* Новая фраза въезжает снизу. Анимация на самом элементе, без отложенной
   подмены текста - двум фразам физически негде наложиться. */
.desc-status-txt.is-in { animation: descLineIn .3s cubic-bezier(.2, .8, .3, 1) both; }
@keyframes descLineIn {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .desc-status-txt.is-in { animation: none; }
}

/* Снимок нажимается - это и есть замена фото. */
.desc-drop { cursor: pointer; }

/* ── Появление на экране описания ────────────────────────────────────────────
   Блоки приезжают снизу с лёгким запозданием друг за другом. Одновременное
   появление читается как перерисовка страницы, последовательное - как ответ
   системы на действие. */
@keyframes desc-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

#descStep1 .desc-drop,
#descStep1 .desc-status,
#descStep2Box { animation: desc-in .45s cubic-bezier(.2, .7, .3, 1) both; }

/* Снимок появляется чуть медленнее и с приближением: он главный на экране,
   и резкий скачок картинки в полэкрана бьёт по глазам сильнее всего. */
.desc-drop { animation: desc-photo-in .55s cubic-bezier(.2, .7, .3, 1) both; }
@keyframes desc-photo-in {
  from { opacity: 0; transform: translateY(18px) scale(.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* Само изображение проявляется отдельно, уже внутри рамки: пока браузер
   раскодирует снимок, рамка успевает встать на место, и в ней не мигает
   белый прямоугольник. */
.desc-preview { animation: desc-fade .5s ease-out both; animation-delay: .1s; }
@keyframes desc-fade { from { opacity: 0; } to { opacity: 1; } }

/* Строчки распознанного - друг за другом, чтобы взгляд шёл сверху вниз. */
.desc-reco-row { animation: desc-in .4s cubic-bezier(.2, .7, .3, 1) both; }
.desc-reco-row:nth-child(1) { animation-delay: .05s; }
.desc-reco-row:nth-child(2) { animation-delay: .13s; }

/* Кнопка и подпись под ней - последними: сначала человек читает, ЧТО распознали,
   и только потом видит, за что платит. */
#descStep2Box .submit-btn { animation: desc-in .4s cubic-bezier(.2, .7, .3, 1) both; animation-delay: .18s; }
#descStep2Box .link-hint { animation: desc-fade .4s ease-out both; animation-delay: .26s; }

/* Блоки результата - тем же движением, с нарастающей задержкой. */
#descStep2 .desc-block { animation: desc-in .45s cubic-bezier(.2, .7, .3, 1) both; }
#descStep2 .desc-block:nth-of-type(1) { animation-delay: .04s; }
#descStep2 .desc-block:nth-of-type(2) { animation-delay: .14s; }
#descStep2 .desc-found { animation: desc-in .4s cubic-bezier(.2, .7, .3, 1) both; }

/* Кому движение мешает - его быть не должно. */
@media (prefers-reduced-motion: reduce) {
  #descStep1 .desc-drop, #descStep1 .desc-status, #descStep2Box,
  .desc-preview, .desc-reco-row, #descStep2Box .submit-btn,
  #descStep2Box .link-hint, #descStep2 .desc-block, #descStep2 .desc-found {
    animation: none;
  }
  .desc-status-dot { animation: desc-spin 1.6s linear infinite; }
}

/* Название - поле ввода, но выглядит как значение: рамка и фон появляются только
   при касании. Явное поле посреди карточки читалось бы как «заполни сам»,
   а система его уже заполнила - надо лишь дать возможность поправить. */
.desc-reco-in {
  flex: 1 1 auto; min-width: 0; width: 100%; text-align: right;
  border: 1px solid transparent; border-radius: 8px;
  background: transparent; padding: 3px 6px;
  font-family: inherit; font-size: 12.5px; font-weight: 700; color: #06301f;
  /* Поле - одна строка, поэтому длинное название всё равно упирается в край.
     Многоточие честнее обрыва: видно, что текст продолжается. */
  text-overflow: ellipsis;
}
.desc-reco-in:focus {
  outline: none; border-color: #6ee7b7; background: #fff; text-align: left;
}

/* Квадратная галочка справа от времени. Панель сохраняет по нажатию, а не по
   каждому изменению поля: раньше она перерисовывалась сразу после выбора даты
   и закрывалась прежде, чем человек успевал поставить время. */
.place-ok {
  flex: 0 0 auto; width: 42px; align-self: stretch;
  border-radius: 9px; border: none;
  background: var(--pine, #0f9d58); color: #fff;
  font-size: 17px; font-weight: 700; cursor: pointer; font-family: inherit;
}
.place-ok:active { transform: scale(.95); }

/* Карандаш в нажатом состоянии - видно, что карточка открыта на правку.
   Без этого человек не понимает, почему поля вдруг стали доступны. */
.place-edit.is-on {
  background: var(--pine, #0f9d58); border-color: var(--pine, #0f9d58); color: #fff;
}

/* Закрытое поле контакта выглядит как текст, а не как приглашение печатать:
   правка начинается с карандаша, и поле не должно звать до него. */
.place-adm-in[readonly] {
  border-color: transparent; background: transparent; color: var(--fg);
  padding-left: 0;
}
.place-adm-in[readonly]::placeholder { color: var(--fg-dim); }

/* Заголовок готового поста - кнопка сворачивания. Развёрнутый пост занимал экран
   целиком, и чтобы добраться до ссылок или контакта, приходилось листать мимо
   текста, который человек сам же и написал. */
.camp-ready-head {
  display: flex; align-items: center; gap: 8px; width: 100%;
  margin-top: 12px; padding: 10px 0; border: none; background: none;
  cursor: pointer; font-family: inherit; text-align: left;
}
.camp-ready-chev {
  flex: 0 0 auto; font-size: 18px; line-height: 1; color: var(--fg-dim);
  transition: transform .2s;
}
.camp-ready-head.is-open .camp-ready-chev { transform: rotate(90deg); }
.camp-ready-head .desc-block-title { flex: 1 1 auto; }
.camp-ready.hidden { display: none; }

/* Поле переименования площадки - на месте самого названия: правка происходит
   там, где человек на него смотрит, а не в отдельной панели ниже. */
.place-name-in {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px; font-weight: 700; color: var(--fg);
}
/* .place-adm-ok удалён: контакт сохраняется сам, кнопки подтверждения нет. */

/* Название кампании с кнопками правки и удаления. Правка живёт в общем списке:
   имя человек видит именно здесь и по нему сравнивает кампании между собой. */
.camp-card-head { display: flex; align-items: center; gap: 8px; }
.camp-card-head .link-card-title { flex: 1 1 auto; min-width: 0; }
.camp-card-btn { width: 30px; height: 30px; }
.camp-card-btn.is-danger { color: #dc2626; border-color: #fecaca; }
/* Переименование идёт ПРЯМО В СТРОКЕ названия, поэтому поле не должно выглядеть
   полем: рамка с фоном читается как всплывшее окно правки, хотя ничего не всплывало.
   Оставляем ровно тот же кегль и насыщенность, что у названия, и тонкую линию снизу -
   единственный признак, что текст стал редактируемым. */
/* Селектор с типом обязателен. Базовое правило input[type="text"] весом 0-1-1
   сильнее любого одиночного класса, поэтому прежний .camp-name-in проигрывал ему
   рамку, фон, скругление и отступы - поле снова выглядело всплывшим окном,
   хотя правило вроде бы было написано. Классу нужен тот же тип в селекторе,
   чтобы перевесить. */
input[type="text"].camp-name-in {
  flex: 1 1 auto; min-width: 0; width: auto;
  font-size: 16px; font-weight: 700; color: var(--fg);
  padding: 1px 0; border: 0; border-radius: 0; background: transparent;
  box-shadow: none; outline: none;
  border-bottom: 1px solid var(--border);
  transition: border-bottom-color .18s ease;
}
input[type="text"].camp-name-in:focus {
  border-color: transparent; border-bottom-color: var(--pine, #0f9d58);
}
.camp-title-row input[type="text"].camp-name-in { font-size: 15px; }

/* Дата и время площадки в форме - в одну строку: два поля рядом занимают
   столько же, сколько одно, а разнесённые по строкам растягивают форму. */
.camp-p-when { display: flex; gap: 8px; }
.camp-p-when .link-input { flex: 1 1 0; min-width: 0; }

/* Дата публикации готовыми ответами. Системный календарь просит выбрать точный
   день, хотя почти всегда это сегодня, завтра или послезавтра - и мелкую сетку
   чисел на телефоне ещё надо не промазать. Точный выбор остался под «Другой». */
.camp-when-box {
  margin-top: 10px; padding: 10px; border-radius: 12px;
  background: var(--bg-page, #f5f5f7);
}

/* Подпись над рядами. Без неё два ряда кнопок читались как один ворох:
   непонятно, где день, а где время. */
.camp-when-h {
  font-size: 11.5px; font-weight: 600; color: var(--fg-dim);
  margin: 0 2px 7px; letter-spacing: .2px;
}

/* Дата и время - двумя полями в одну строку. Кнопок «Сегодня/Завтра» больше
   нет: они закрывали собой то, ради чего человек сюда пришёл - выбрать
   конкретный день. */
/* Одна кнопка вместо двух системных полей: за ней барабаны. */
.camp-when-row { display: flex; }
.when-btn {
  flex: 1 1 auto; width: 100%; padding: 12px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--card); color: var(--fg-dim);
  font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  transition: border-color .18s ease, color .18s ease, transform .18s ease;
}
.when-btn:active { transform: scale(.985); }
.when-btn.is-set { color: var(--fg); border-color: #a7f3d0; background: #f7fdfa; }

/* Барабаны. Месяц и год - подпись сверху, а не колесо: их тут никто не крутит,
   они меняются сами, когда день переходит через конец месяца. */
.sheet-card { max-width: 320px; }
.sheet-head {
  text-align: center; font-size: 15px; font-weight: 700; color: var(--fg);
  margin-bottom: 10px;
}
/* Подписи над барабанами: где дата, где время. Три колонки цифр без них
   читаются как одно длинное число. */
.wheel-caps {
  display: flex; justify-content: center; gap: 4px; padding: 0 4px 4px;
}
.wheel-cap {
  font-size: 11px; font-weight: 600; color: var(--fg-dim);
  text-align: center; letter-spacing: .2px;
}
.wheel-cap-day { width: 68px; }
.wheel-cap-time { width: 152px; }

.wheel-rows {
  position: relative; display: flex; justify-content: center; gap: 4px;
  padding: 0 4px 6px;
}

/* Черта между датой и временем - тонкая, но по всей высоте: две группы должны
   читаться как две, а не как три равнозначные колонки. */
.wheel-divider {
  z-index: 2; width: 1px; align-self: stretch; margin: 8px 6px;
  background: var(--border);
}
.wheel {
  position: relative; z-index: 2;
  height: 190px; width: 68px; overflow-y: auto;
  scroll-snap-type: y mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Маска по краям: строки уходят в туман, и видно, что список крутится
     дальше, а не обрывается. */
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 26%, #000 74%, transparent);
          mask-image: linear-gradient(180deg, transparent, #000 26%, #000 74%, transparent);
}
.wheel::-webkit-scrollbar { display: none; }
.wheel-inner { padding: 76px 0; }
.wheel-item {
  height: 38px; line-height: 38px; text-align: center;
  scroll-snap-align: center;
  font-size: 19px; color: var(--fg-dim); font-variant-numeric: tabular-nums;
  transition: color .15s ease, transform .15s ease;
}
.wheel-item.is-on { color: var(--fg); font-weight: 700; transform: scale(1.12); }
.wheel-sep {
  z-index: 2; align-self: center; font-size: 19px; font-weight: 700;
  color: var(--fg-dim); padding-bottom: 2px;
}
/* Две полоски по центру вместо заливки: выбранная строка остаётся на белом,
   как в системных барабанах, и читается лучше - заливка приглушала цифру,
   ради которой всё и крутят. */
.wheel-band {
  position: absolute; left: 4px; right: 4px; top: 50%; height: 38px;
  transform: translateY(-50%); z-index: 1; pointer-events: none;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
@keyframes whenSlide {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* Решение словами. «04.09.2026 / 12:30» человек переводит в голове, а тут
   читает готовое: выйдет завтра в 14:00. */
/* Строки-пересказа под кнопкой больше нет: она повторяла то, что написано
   на самой кнопке. Подскок при выборе переехал на кнопку. */
.when-btn.is-new { animation: whenPop .34s cubic-bezier(.2, .9, .3, 1.4) both; }
@keyframes whenPop {
  0%   { opacity: .4; transform: translateY(-4px) scale(.98); }
  60%  { opacity: 1;  transform: translateY(0) scale(1.02); }
  100% { opacity: 1;  transform: none; }
}

/* Найденные в посте ссылки - просто список. Настройки направления человеку не нужны:
   адрес взят из его же поста и вести должен туда же, куда вёл. */
.camp-found { margin-top: 10px; }
.camp-found-row {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; margin-bottom: 6px; border-radius: 10px;
  background: #ecfdf5;
}
.camp-found-ico { flex: 0 0 auto; font-size: 14px; }
.camp-found-url {
  flex: 1 1 auto; min-width: 0; font-size: 13px; color: #065f46;
  word-break: break-all;
}
.camp-found-go { flex: 0 0 auto; font-size: 13px; color: #059669; }

/* Показанный адрес человек первым делом пытается нажать. Раз он нажимается -
   это должно быть видно до нажатия: подчёркивание и отклик на касание. */
.camp-found-row.is-tap, .link-url.is-tap { cursor: pointer; }
.camp-found-row.is-tap:active, .link-url.is-tap:active { opacity: .65; }
.camp-found-row.is-tap .camp-found-url { text-decoration: underline; }
.link-url.is-tap { text-decoration: underline; }

/* Кнопка проверки публикации. Обычный запрос к веб-превью канала, платные модели
   не участвуют - поэтому нажимать можно сколько угодно. */
.place-check {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg);
  font-size: 15px; line-height: 1; cursor: pointer; font-family: inherit; padding: 0;
}
.place-check.is-busy { opacity: .5; }

/* Звезда «в избранное». Стоит рядом с цифрами площадки намеренно: отмечают её
   по итогу размещения, глядя на цену перехода, а не в отдельном меню. */
.place-star {
  flex: 0 0 auto; width: 30px; height: 30px; border: none; background: none;
  color: #d1d5db; font-size: 19px; line-height: 1; cursor: pointer; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .2s ease, transform .2s cubic-bezier(.2, .9, .3, 1.5);
}
.place-star:active { transform: scale(.88); }
.place-star.is-on { color: #f0b429; }
@keyframes starPop {
  0%   { transform: scale(1) rotate(0deg); }
  45%  { transform: scale(1.35) rotate(18deg); }
  100% { transform: scale(1) rotate(0deg); }
}
.place-star.is-pop { animation: starPop .42s cubic-bezier(.2, .9, .3, 1.5); }

/* Канал или группа. У группы счётчика просмотров не существует, и знать это
   надо ДО размещения - при выборе, а не после оплаты. */
.place-tag {
  flex: 0 0 auto; padding: 2px 7px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg-page, #f5f5f7);
  color: var(--fg-dim); font-size: 10.5px; font-weight: 600; white-space: nowrap;
}
.place-tag.is-ok { border-color: #a7f3d0; background: #ecfdf5; color: #065f46; }

/* Своё окно подтверждения. Системное выглядит чужим, на разных клиентах
   Telegram рисуется по-разному и забирает фокус - после него экран
   перерисовывается сам собой. */
.ask-back {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center; padding: 22px;
  background: rgba(17, 24, 39, .42);
  animation: askIn .18s ease both;
}
.ask-back.is-out { animation: askOut .18s ease both; }
/* Прокрутка внутри самого окна остаётся - длинная форма должна листаться.
   Наружу жест не уходит: overscroll-behavior не даёт странице под окном
   подхватить движение, когда список внутри уже упёрся в край. */
.ask-back, .modal-overlay { overscroll-behavior: contain; touch-action: none; }
.ask-card, .modal-card { touch-action: auto; overscroll-behavior: contain; }
.ask-card {
  width: 100%; max-width: 340px; background: var(--card, #fff);
  border-radius: 16px; padding: 18px 16px 14px;
  box-shadow: 0 18px 48px rgba(17, 24, 39, .26);
  animation: askCard .24s cubic-bezier(.2, .9, .3, 1.3) both;
}
.ask-title { font-size: 16px; font-weight: 700; color: var(--fg); margin-bottom: 7px; }
.ask-text { font-size: 13.5px; line-height: 1.45; color: var(--fg-dim); margin-bottom: 14px; }
.ask-acts { display: flex; gap: 8px; }
.ask-acts .link-btn { padding: 11px; }
.ask-acts .link-btn-main.is-danger { background: #dc2626; border-color: #dc2626; }
.ask-input { margin-bottom: 14px; }
/* Меню действий - кнопки в столбик: горизонтально три подписи не помещаются
   и режутся посередине слова. */
.ask-menu { display: flex; flex-direction: column; gap: 8px; }
.ask-menu-btn { width: 100%; padding: 12px; }
.ask-menu-btn.is-danger { color: #dc2626; border-color: #fecaca; }
@keyframes askIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes askOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes askCard {
  from { opacity: 0; transform: translateY(12px) scale(.96); }
  to   { opacity: 1; transform: none; }
}
/* Кнопка отметки в раскрытом состоянии: видно, что вопрос задан именно ей. */
.place-check.is-on {
  border-color: var(--pine, #0f9d58); background: #ecfdf5; color: #065f46;
}

/* Подтверждение сохранения строкой, а не всплывающим окном: окно требовало
   нажать «Закрыть» ради сообщения, которое и так видно по обновившемуся тексту,
   и выбрасывало экран наверх - проверить результат было негде. */
.link-hint.is-ok { color: var(--pine, #0f9d58); font-weight: 600; }


/* Четыре плитки в ряду: скорость встала рядом с остальными цифрами. Шрифт
   мельче, чтобы «1 500 ₽» не переносилось. */
.place-stats .link-step { flex: 1 1 0; min-width: 0; padding: 8px 4px; }
.place-stats .link-step-n { font-size: 15px; }
.place-stats .link-step-l { font-size: 10px; }


/* Запрет объясняем, а не прячем: молчаливое отсутствие кнопки человек читает
   как поломку, а не как правило. */
.camp-wide-btn:disabled { opacity: .55; cursor: default; font-size: 13px; }

/* Запертый текст: видно, что это запись о том, что уже опубликовано,
   а не поле для ввода. */
.camp-post.is-locked {
  background: var(--bg-page, #f5f5f7); color: var(--fg-dim);
  border-color: transparent;
}

/* Поле поста не меняет высоту при наборе: растущее поле сдвигает всё под собой
   на каждом символе, и экран дёргается прямо под пальцами. */
.camp-post.is-fixed { height: 170px; resize: none; overflow-y: auto; }

/* Запертые поля карточки: видно, что это запись о том, что уже произошло,
   а не место для ввода. */
.place-in:disabled {
  background: transparent; border-color: transparent;
  color: var(--fg-dim); -webkit-text-fill-color: var(--fg-dim); opacity: 1;
}


/* ═══ Строка «Назад + название» ══════════════════════════════════════════════
   Правила стоят В КОНЦЕ файла намеренно. Первые попытки я вписал рядом
   с остальным оформлением заголовков - выше по файлу, чем базовые .feed-view
   и .projects-heading. Базовые шли следом и перекрывали их: заголовок оставался
   у левого края, а верхнее поле экрана возвращалось. Здесь перекрывать нечем. */
.feed-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; width: 100%; margin: 0 0 8px;
}
.feed-bar > .back-btn { margin: 0; flex: 0 0 auto; }
/* Название прижато вправо распоркой между двумя элементами строки, а не
   выравниванием текста: так оно стоит у края при любой ширине экрана
   и не зависит от того, растянулся блок или нет. */
.feed-bar > .projects-heading {
  margin: 0; flex: 0 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 6px;
  text-align: right; line-height: 1.25;
}

/* Верхние поля экранов. Над ними уже есть отступ шапки приложения, ещё
   двенадцать пикселей сверху - пустая полоса на каждом экране. */
.feed-view { padding-top: 4px; }
.news-view { padding-top: 2px; }
.feed-view .filter-bar { margin-bottom: 10px; }
.screen-topbar { margin-bottom: 8px; }


/* ═══ Одинаковая шапка на ВСЕХ экранах ═══════════════════════════════════════
   Панелей исторически три вида - feed-bar, screen-topbar, links-bar, - и каждая
   жила по своим правилам: где-то содержимое расползалось по центру, где-то
   у кнопки оставался лишний отступ снизу, где-то экран добавлял своё верхнее
   поле поверх отступа шапки приложения.
   Здесь они сведены к одному поведению: «Назад» вплотную к левому верхнему
   углу, всё остальное - вплотную к правому. */
.feed-bar,
.screen-topbar,
.links-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  margin: 0 0 8px;
}

/* Кнопка возврата ничего вокруг себя не занимает: её собственный отступ снизу
   складывался с отступом панели и давал двойной провал под строкой. */
.feed-bar > .back-btn,
.screen-topbar > .back-btn,
.links-bar > .back-btn {
  margin: 0;
  flex: 0 0 auto;
}

/* Всё, что напротив кнопки, прижато к правому краю распоркой. Авто-отступ
   слева нужен для случаев, когда элементов в строке больше двух: тогда
   распорка растащила бы их поровну, а нам нужна плотная группа справа. */
.feed-bar > *:not(.back-btn),
.screen-topbar > *:not(.back-btn),
.links-bar > *:not(.back-btn) {
  flex: 0 1 auto;
  min-width: 0;
}
.feed-bar > *:nth-child(2),
.screen-topbar > *:nth-child(2),
.links-bar > *:nth-child(2) {
  margin-left: auto;
}

/* Заголовок в такой строке - обычный текст, а не блок с полями. */
.feed-bar > .projects-heading,
.screen-topbar > .projects-heading,
.links-bar > .projects-heading {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  text-align: right;
  line-height: 1.25;
}

/* Верхние поля экранов. Над ними уже есть отступ шапки приложения - второй
   давал пустую полосу высотой с карточку на каждом экране. */
main { padding-top: 8px; }
.feed-view { padding-top: 4px; }
.news-view { padding-top: 2px; }
.chat-view { padding-top: 0; }
.feed-view .filter-bar { margin-bottom: 10px; }


/* В ЧАТЕ шапка приложения при клавиатуре не прячется. На остальных экранах
   прятать её полезно - форма длинная, каждая строка на счету. В чате наоборот:
   вместе с шапкой уезжали вкладки рынков, и человек, начав печатать, переставал
   видеть, в каком чате он пишет, и не мог переключиться, не убрав клавиатуру. */
/* Правило-исключение для чата больше не нужно: шапка не прячется нигде. */


/* Список людей: фото, имя, дата появления. Карточка НЕ нажимается - вид
   у неё соответствующий, без рамки и стрелки: нажимаемая на вид, но мёртвая
   карточка раздражает сильнее, чем её отсутствие. */
.person-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: 12px; background: var(--card);
  margin-bottom: 6px;
}
.person-ava {
  position: relative; flex: 0 0 auto; width: 38px; height: 38px;
  border-radius: 50%; overflow: hidden; background: var(--bg-page, #f5f5f7);
  display: flex; align-items: center; justify-content: center;
}
.person-ava img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 2;
}
.person-letter { font-size: 15px; font-weight: 700; color: var(--fg-dim); }
.person-info { flex: 1 1 auto; min-width: 0; }
.person-name {
  font-size: 14px; font-weight: 600; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.person-since { font-size: 12px; color: var(--fg-dim); margin-top: 1px; }

/* Просветы между людьми сжаты (Кирилл). Общий отступ .earn-list в 12px плюс
   свой margin в 6px давали между строками 18px - на карточках с текстом это
   нормально, а здесь строка низкая, и список из десяти человек растягивался
   на два экрана. Список читают глазами сверху вниз, а не по одной карточке:
   плотный он читается быстрее. Отступ задаём в одном месте - у контейнера,
   свой margin у строки снимаем, иначе они складываются. */
/* Два столбца (Кирилл): страница - это ровно десять человек, и в одну колонку
   они уходили под сгиб. В два столбца по пять рядов страница видна целиком,
   и листать её глазами не приходится.

   Строка страниц и сообщения о состоянии тянутся на оба столбца: номер
   страницы, зажатый в половину ширины, читался бы как ещё один человек. */
#peopleList {
  display: grid;
  /* minmax(0, 1fr), а не 1fr: у элемента сетки минимальная ширина по умолчанию
     равна ширине его содержимого, и «1fr» её НЕ ограничивает - это потолок,
     а не пол. Длинное имя в одну строку («Косметика Москва садовод») распирало
     свой столбец шире половины, второй столбец уезжал за край экрана, а
     многоточие не срабатывало: обрезать было нечего, места хватало всегда.
     Ноль в minmax разрешает столбцу быть уже содержимого - только после этого
     text-overflow получает работу. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 4px;
  align-items: start;
}
/* То же самое на два уровня внутрь: строка - элемент сетки, «информация» -
   элемент flex. Обе по умолчанию тоже не ужимаются меньше содержимого, и без
   этих двух нулей ограничение столбца просто передавалось бы дальше вглубь. */
#peopleList .person-row { min-width: 0; }
#peopleList .person-info { min-width: 0; }
/* Дата тоже в одну строку с многоточием: «с 4 сентября 2025» на узкой карточке
   переносилось на второй ряд и делало соседние строки разной высоты. */
#peopleList .person-since {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#peopleList > .pager-row,
#peopleList > .state-msg { grid-column: 1 / -1; }
/* Окантовка (Кирилл): карточка #f4f4f6 на белом фоне отличается от него на
   три процента яркости, и десять таких строк подряд сливаются в одно серое
   поле - границу между двумя людьми глаз не находит. Белый фон плюс тонкая
   рамка разделяют строки надёжнее, чем заливка: рамку видно и на солнце,
   и в тёмной теме, а разницу в три процента - нет.

   Рамка, а не тень: тень под каждой из десяти строк даёт рябь, а на плотном
   списке с отступом в четыре пикселя тени соседей ещё и накладываются. */
#peopleList .person-row {
  margin-bottom: 0; padding: 7px 8px; gap: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
}
/* В половине ширины имя длиннее полутора слов не помещается - обрезаем
   многоточием, как и раньше, но теперь это работает на каждом втором имени,
   а не на редком длинном. Аватар чуть меньше: он тут опознавательный знак,
   а не портрет. */
#peopleList .person-ava { width: 32px; height: 32px; }
#peopleList .person-name { font-size: 13px; }
#peopleList .person-since { font-size: 11px; }


/* Крупный значок фоном в плитке админки - как на карточках главной.
   Он НЕПОДВИЖЕН, и это принципиально: движущийся слой внутри карточки
   со скруглением заставляет iOS не дорисовывать соседний текст, и счётчик
   показывает «6» вместо «6 платных». Слой снимаем вместе с анимацией -
   will-change без движения ничего не ускоряет, только держит мёртвый слой.
   Размытие делает значок фоном, а не картинкой: он не спорит с цифрой. */
.admin-grid .card-mark {
  display: block;
  position: absolute; right: -6px; bottom: -8px; top: auto;
  font-size: 42px; line-height: 1;
  opacity: .12;
  filter: blur(0.6px);
  animation: none;
  will-change: auto;
  pointer-events: none;
}
.admin-grid .earn-card { position: relative; overflow: hidden; }


/* Строка счётчиков: только значок и число. Слова убраны - с приростом
   в скобках строка не помещалась и переносилась на второй ряд. Разделители
   между счётчиками сохраняем: без них три числа подряд читаются как одно. */
.home-stats-bar { flex-wrap: nowrap; }
.home-stats-item { white-space: nowrap; }

/* Счётчики снова в одну строку, слово рядом с числом. Помещается потому, что
   у людей слова нет: «пользователей» - самое длинное из трёх, и именно оно
   выталкивало строку на второй ряд. Значок человечка говорит то же самое. */


/* Цена и рынок на плитке заявки. Цена - главное после названия: поставщик
   решает по ней, стоит ли вообще открывать. Рынок - вторая половина решения:
   Садовод и Южные Ворота в разных концах города.

   Рынок в одну строку с многоточием: названий может быть три, и перенос
   съедал бы карточку целиком ради строки, которую и так читают до запятой. */
.tile-market {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* «цена не указана» - у заявок старше 22.08, когда цену ещё не спрашивали.
   Тише обычной строки: это отсутствие данных, а не данные. */
.tile-noprice { opacity: .62; font-style: italic; }


/* Заголовок списка людей мельче остальных: это больше не название экрана,
   а его счёт - «579 человек · страница 1 из 58». Строка длиннее обычного
   заголовка, и в прежнем размере она вплотную подходила к кнопке «Назад».
   Отдельной строки со счётом под шапкой больше нет: она повторяла то самое,
   ради чего сюда и заходят, занимая целый ряд. */
#peopleTitle { font-size: 12px; font-weight: 500; }


/* Нижняя строка плитки заявки: слева сколько её смотрели, справа сколько
   обсуждают и сколько уже предложили. Разведены по краям намеренно - это
   величины разной природы: слева чужое внимание, справа ответы на заявку.
   Рядом друг с другом они читались бы как один ряд однородных чисел. */
/* Просмотры - метка на фотографии, слева внизу, напротив предложений справа.
   Тёмная, как метка срока сверху: это счётчик, а не действие; синий цвет
   у предложений значит «тебе ответили», и второй такой же его обесценил бы. */
.tile-badge-views { top: auto; bottom: 7px; left: 7px; right: auto; }


/* Живой счётчик просмотров. Щелчок на каждую единицу: число подрастает
   и коротко подсвечивается. Прибавку в одну единицу без движения глаз
   не замечает вовсе - а ради неё всё и делается. */
@keyframes count-bump {
  0%   { transform: scale(1);    color: var(--fg-dim); }
  35%  { transform: scale(1.35); color: var(--ok, #0f9d58); }
  100% { transform: scale(1);    color: var(--fg-dim); }
}
.count-bump { display: inline-block; animation: count-bump .5s ease-out; }

/* Просмотры в карточке товара - строкой под местом, тем же тоном, что
   категория и рынок: это справка, а не заголовок. */
.product-views { font-size: 12.5px; color: var(--fg-dim); margin-top: 6px; }

/* Тем, кто просил систему не двигать картинку, движение не нужно и здесь. */
@media (prefers-reduced-motion: reduce) {
  .count-bump { animation: none; }
}


/* Прокрутка не перетекает с карточки на страницу под ней: дотянув список
   комментариев до конца, палец продолжает тянуть - и без этого правила
   дальше едет уже лента за карточкой. */
.detail-overlay { overscroll-behavior: contain; }
.detail-card { overscroll-behavior: contain; }


/* Блик на кнопке ответа в карточке заявки. Она ссылка, а не кнопка, поэтому
   свои скругления и обрезку задаём явно: без overflow блик уезжает за угол
   и висит поверх соседних элементов. */
.detail-action.btn-shine { position: relative; overflow: hidden; }


/* Подтверждён ли номер поставщика - рядом с его именем, до всякой оплаты.
   Зелёная метка это довод в его пользу, красная - предупреждение покупателю;
   обе набраны мелко и не спорят с ценой за главное место на карточке. */
.verif-tag {
  display: inline-block; margin-left: 6px; vertical-align: middle;
  font-size: 10.5px; font-weight: 600; line-height: 1.6;
  padding: 1px 7px; border-radius: 999px; white-space: nowrap;
}
.verif-yes { color: #0b6b4f; background: color-mix(in srgb, var(--ok, #0f9d58) 16%, transparent); }
.verif-no  { color: #b02a1e; background: color-mix(in srgb, #dc2626 14%, transparent); }


/* «Открыть контакт» пульсирует: под заявкой бывает десяток предложений, и
   кнопка на фотографии легко теряется среди самих фотографий.

   Пульсирует СВЕЧЕНИЕ, а не размер. Кнопка отцентрована через transform,
   и анимация с трансформом отобрала бы у неё центрирование - тот же самый
   дефект, который мы только что вычищали по всему файлу. Тень ничего
   не двигает. */
@keyframes contact-pulse {
  0%, 100% { box-shadow: 0 2px 8px rgba(0,0,0,.28); }
  50%      { box-shadow: 0 2px 8px rgba(0,0,0,.28), 0 0 0 7px rgba(45,125,246,.30); }
}
.offer-action-btn.offer-action-on-photo {
  animation: contact-pulse 2.2s ease-in-out infinite;
}
/* Открытый контакт мигать не должен - там уже нечего нажимать. */
.offer-action-btn.offer-action-on-photo:disabled { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .offer-action-btn.offer-action-on-photo { animation: none; }
}

/* Имя поставщика вместо безличного номера - крупнее и тёмнее прежней подписи:
   это теперь имя человека, а не служебная нумерация. */
.offer-supplier-label { font-size: 12.5px; }


/* Значок подтверждения у кнопки «Мой профиль». Зелёная галка спокойно
   пульсирует - это награда, её приятно заметить. Красный знак пульсирует
   заметнее: он приглашение к действию, и человек должен на него нажать. */
/* Значок подтверждения вплотную к слову «Профиль»: он относится к нему,
   а не к кнопке. Отодвинутый к краю, он читался как отдельный элемент
   управления - будто на него можно нажать отдельно.

   Без анимации (Кирилл): на главной уже двигаются прирост в счётчиках и
   блики на кнопках; ещё одна пульсирующая точка превращает экран в вывеску. */
.quick-verify {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; flex: 0 0 auto;
  border-radius: 50%; font-size: 10px; font-weight: 700; line-height: 1;
  color: #fff;
  /* Отступ ОТРИЦАТЕЛЬНЫЙ, и это не опечатка. Кнопка профиля - flex-строка
     с gap: 8px, и этот зазор ставится между текстом и значком автоматически,
     сколько бы я ни убавлял свой margin. Минус пять съедает чужой отступ
     и оставляет три пикселя - значок стоит вплотную к слову. */
  margin-left: -5px;
}
.quick-verify-ok { background: var(--ok, #0f9d58); }
.quick-verify-no { background: #dc2626; }


/* Строка «Назад + статус номера». Статус прижат вправо распоркой, как названия
   на остальных экранах: так он стоит у края при любой ширине. */
.profile-topbar { margin-bottom: 10px; }
.profile-topbar .supplier-badge { margin: 0; flex: 0 0 auto; }

/* Карандаш превращается в галочку - это ОДНА кнопка в двух состояниях, поэтому
   и переход рисуем как превращение, а не как подмену: знак поворачивается
   и меняет цвет за те же доли секунды, что человек убирает палец.

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

   Оба значка лежат в кнопке ОДНОВРЕМЕННО и меняются местами: уходящий
   отлетает вниз с поворотом, приходящий влетает сверху и слегка перелетает
   через размер. Подмена содержимого дала бы мгновенный скачок вместо
   превращения. */
.name-save-check { transition: background-color .2s ease, color .2s ease; overflow: hidden; }
.name-save-check.is-save { background: var(--accent); color: var(--accent-fg); }
.name-save-check.saved { background: var(--ok); color: #fff; }

.nsc-ico {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  transition: transform .34s cubic-bezier(.34, 1.56, .64, 1), opacity .2s ease;
}
.nsc-ico svg { width: 17px; height: 17px; display: block; }

/* Карандаш смотрит остриём вправо-вниз, как его и рисуют обычно. Зеркальный
   вариант Кирилл забраковал: перевёрнутый значок читается непривычно, сколько
   бы смысла в направлении ни было. */
.nsc-pencil { transform: none; }
.nsc-check { transform: translateY(-120%) rotate(-90deg); opacity: 0; }

.name-save-check.is-save .nsc-pencil,
.name-save-check.saved .nsc-pencil { transform: translateY(120%) rotate(90deg); opacity: 0; }
.name-save-check.is-save .nsc-check,
.name-save-check.saved .nsc-check { transform: translateY(0) rotate(0); opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .nsc-ico { transition: none; }
}

/* Закрытое поле имени не ловит касание вовсе: на iOS readonly-поле всё равно
   поднимает клавиатуру и ставит курсор, и это читается как разрешение печатать.
   Менять имя можно только карандашом. */
.profile-name-input.locked { pointer-events: none; user-select: none; }

/* Закрытое поле имени не должно выглядеть полем: рамка и фон появляются
   только когда его открыли карандашом. */
.profile-name-input[readonly] {
  border-color: transparent; background: transparent; cursor: default;
}

/* Короткая надпись по центру экрана. Гаснет сама - закрывать нечего. */
.center-flash {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 60; pointer-events: none;
  padding: 12px 20px; border-radius: 14px;
  background: rgba(20, 24, 28, .88); color: #fff;
  font-size: 15px; font-weight: 600; white-space: nowrap;
  box-shadow: 0 8px 28px rgba(0,0,0,.25);
  animation: flash-in .22s ease-out;
}
.center-flash.is-out { opacity: 0; transition: opacity .4s ease; }
@keyframes flash-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(.9); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}


/* Экран ИИ-описаний плотнее: строка сводки теперь одна короткая, а карточки
   не должны отъедать по экрану на запись. Текст описания обрезаем тремя
   строками - его читают, чтобы понять, годное оно или нет, а для этого
   хватает начала. */
#adminGenTotals { margin-bottom: 8px; }
.gen-card { padding: 10px 12px; margin-bottom: 8px; }
/* Свёрнутая карточка: только название и категория. Раскрытая показывает
   описание, отзыв и удаление. Высота едет по фактическому содержимому -
   описания разной длины, и одно число на всех либо режет длинное, либо
   оставляет дыру под коротким. */
.gen-body {
  max-height: 0; overflow: hidden;
  transition: max-height .28s ease, opacity .2s ease;
  opacity: 0;
}
.gen-card.is-open .gen-body { opacity: 1; }

.gen-arrow {
  flex: 0 0 auto; width: 26px; height: 26px; padding: 0;
  border: none; background: none; color: var(--fg-dim);
  font-size: 20px; line-height: 1; cursor: pointer; font-family: inherit;
  transition: transform .28s ease;
}
.gen-card.is-open .gen-arrow { transform: rotate(90deg); }

.gen-head { cursor: pointer; }
.gen-del { margin-top: 10px; }


/* Фильтры в строке с «Назад»: прижаты вправо, где раньше стояло название. */
.gen-tabs { margin: 0; flex: 0 0 auto; gap: 6px; }


/* Две кнопки в раскрытой карточке: возврат и удаление. В строку, а не столбиком -
   они равнозначны, и столбик из двух кнопок читается как список из одной
   важной и одной второстепенной. */
/* Две кнопки в строку. min-width: 0 обязателен: обеим их собственные правила
   задают ширину 100%, и без обнуления минимума они не ужимаются до половины,
   а вылезают за край карточки - именно это и было видно. */
.gen-actions { display: flex; gap: 8px; margin-top: 10px; }
.gen-actions button {
  flex: 1 1 0; min-width: 0; width: auto; margin: 0;
  padding: 11px 8px; font-size: 13.5px; white-space: nowrap;
}
.gen-del { margin-top: 0; }


/* Карточка товара повторяет карточку заявки. Описание тем же тоном, что
   и остальная справка, но с сохранением переносов строк: поставщики пишут
   его в несколько строк, и склеенное в абзац оно теряет смысл. */
#productBody .detail-title { font-size: 17px; font-weight: 600; letter-spacing: -0.2px; margin: 0 0 12px; }
#productBody .product-desc { white-space: pre-line; margin-bottom: 11px; }
#productBody .detail-status { color: var(--fg); font-weight: 600; }


/* Значок «поделиться» в шапке статистики рекламы. Заголовок здесь не h2,
   а строка в панели, поэтому выравнивание задаём ей отдельно - иначе значок
   встаёт по базовой линии текста и висит ниже, чем нужно. */
.pitch-topbar-title[data-share] { display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
.pitch-topbar-title .share-btn.copy-btn {
  width: 22px; height: 22px; flex: 0 0 auto; padding: 0;
  border: none; background: none; box-shadow: none; border-radius: 0;
  opacity: .38;
  /* Значок опущен на два пикселя: он квадратный, а буквы рядом с ним имеют
     выносные элементы вверх и не имеют вниз - по геометрии значок кажется
     приподнятым, хотя по центрированию стоит ровно. */
  display: flex; align-items: center; justify-content: center;
  margin-top: 2px;
}
.pitch-topbar-title .share-btn.copy-btn:active { opacity: .7; }
.pitch-topbar-title .share-btn.copy-btn.is-done { opacity: 1; background: none; }
.pitch-topbar-title .share-btn .copy-ico { width: 14px; height: 14px; }


/* Переключатель разреза на графике рекламы. Стоит на месте подписи «Последние
   24 часа»: подпись сообщала период, кнопки сообщают его же и позволяют
   сменить - одно место вместо двух. */
/* Четыре кнопки в строку с отметкой «онлайн» - места в обрез, поэтому поля
   узкие и шрифт мелкий. Переносить их на второй ряд нельзя: строка над
   графиком должна оставаться одной. */
.chart-range { display: inline-flex; gap: 4px; flex-wrap: nowrap; }
.chart-range-btn {
  padding: 4px 8px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border); background: transparent; color: var(--fg-dim);
  font-family: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
.chart-range-btn.is-on {
  background: var(--pine, #0f9d58); border-color: var(--pine, #0f9d58); color: #fff;
}


/* Профиль поставщика оформлен как карточка заявки: блоки разделены чертой,
   а не сложены плитками с полями. Два экрана с контактами продавца выглядели
   по-разному, и человек каждый раз заново искал, где что. */
.sp-card {
  background: transparent; border: none; border-radius: 0;
  padding: 12px 0; margin: 0;
  border-top: 1px solid var(--border);
}
.sp-head { padding-bottom: 12px; }
.sp-row { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; }
.sp-label { color: var(--fg-dim); font-size: 13px; }
.sp-value { font-size: 13.5px; font-weight: 600; text-align: right; }


/* ── Экран «Заявки» в админке ───────────────────────────────────────────── */
/* Карточка заявки: фото, название, условия, под ними воронка счётчиков.
   Порядок счётчиков слева направо повторяет путь заявки - смотрели, дошло,
   ответили, купили контакт: по нему видно, на каком шаге всё встало. */
.rfq-admin-card { text-align: left; width: 100%; }
.rfq-admin-head { display: flex; align-items: flex-start; gap: 10px; }
.rfq-admin-photo {
  width: 44px; height: 44px; flex: 0 0 auto; border-radius: 10px;
  object-fit: cover; background: var(--card);
}
.rfq-admin-text { flex: 1 1 auto; min-width: 0; }
.rfq-live {
  flex: 0 0 auto; font-size: 10.5px; font-weight: 700; padding: 1px 7px;
  border-radius: 999px; color: #0b6b4f;
  background: color-mix(in srgb, var(--ok, #0f9d58) 16%, transparent);
}
.rfq-counts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.rfq-count { font-size: 12px; color: var(--fg-dim); white-space: nowrap; }

/* Получатели и покупатели - строками «кто : что с ним стало». Список из
   двадцати человек читают глазами сверху вниз, и статус должен стоять
   у правого края, чтобы взгляд шёл по одной линии. */
.rfq-people { display: flex; flex-direction: column; gap: 2px; }
.rfq-person {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: 12.5px; padding: 4px 0; border-bottom: 1px solid var(--border);
}
.rfq-person:last-child { border-bottom: none; }
.rfq-status { color: var(--fg-dim); white-space: nowrap; }
.rfq-status.is-ok { color: var(--ok, #0f9d58); font-weight: 600; }
.rfq-status.is-bad { color: #b02a1e; }

.rfq-offer { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.rfq-offer:last-child { border-bottom: none; }
.rfq-offer-photo { width: 56px; height: 56px; flex: 0 0 auto; border-radius: 10px; object-fit: cover; }
.rfq-offer-text { flex: 1 1 auto; min-width: 0; font-size: 13px; line-height: 1.45; }


/* ── Своя рассылка ───────────────────────────────────────────────────────── */
/* Фотографии плиткой, как в Telegram: человек собирает альбом и должен видеть
   его тем же способом, каким увидит получатель. */
/* Четыре в ряд: десять фотографий укладываются в три ряда вместо пяти, и
   экран перестаёт быть лентой из картинок. Сетка, а не свободная строка -
   плитки должны стоять по столбцам, иначе при перестановке они прыгают
   по-разному в зависимости от ширины соседей. */
.mailing-photos {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 6px; margin: 10px 0;
}
.mailing-photo {
  position: relative; width: 100%; aspect-ratio: 1 / 1;
  animation: mailing-photo-in .26s cubic-bezier(.34, 1.4, .64, 1);
}
@keyframes mailing-photo-in {
  from { opacity: 0; transform: scale(.82); }
  to   { opacity: 1; transform: scale(1); }
}
.mailing-photo img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; display: block; }
.mailing-photo-x {
  position: absolute; top: -4px; right: -4px; width: 18px; height: 18px;
  border: none; border-radius: 50%; background: rgba(0,0,0,.65); color: #fff;
  font-size: 11px; line-height: 1; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}

/* Полоса отправки. Заполняется по факту доставленных, а не по времени:
   полоса, которая едет сама по себе, врёт - а здесь врать нельзя, за ней
   стоят живые люди, получающие сообщение. */
.mailing-progress { margin: 12px 0; }
.mailing-bar {
  height: 8px; border-radius: 999px; background: var(--card); overflow: hidden;
}
.mailing-bar-fill {
  height: 100%; width: 0%; border-radius: 999px;
  background: var(--pine, #0f9d58);
  transition: width .6s ease;
}
.mailing-progress-text {
  margin-top: 6px; font-size: 13px; font-weight: 600; color: var(--fg); text-align: center;
}


/* Номер на плитке: порядок в альбоме видит получатель, значит и отправитель
   должен видеть его здесь, а не проверять на себе. */
.mailing-photo-n {
  position: absolute; left: 3px; bottom: 3px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px; background: rgba(0,0,0,.62); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}
/* Перетаскиваемая плитка приподнимается: без обратной связи непонятно,
   взялась она или палец просто скользит по экрану. */
/* Взятая плитка приподнимается и отбрасывает тень - без этого непонятно,
   взялась она или палец просто скользит по экрану. Возврат помягче взятия:
   резкое схлопывание читается как сбой. */
.mailing-photo.is-dragging {
  transform: scale(1.1) rotate(-2deg); opacity: .9; z-index: 2;
  box-shadow: 0 8px 20px rgba(20, 24, 28, .28);
  animation: none;
}
/* Перемещение плавнее взятия: плитка едет на новое место почти треть секунды,
   и глаз успевает проследить путь. Резкая перестановка читается как подмена -
   человек не понимает, его ли это фотография переехала. */
.mailing-photo {
  transition: transform .32s cubic-bezier(.22, .61, .36, 1),
              opacity .18s ease, box-shadow .22s ease;
  touch-action: none;
}
.mailing-photo img { transition: border-radius .2s ease; }

@media (prefers-reduced-motion: reduce) {
  .mailing-photo { animation: none; transition: none; }
}



/* Кнопка отправки с числом: текст длиннее обычного, поэтому не переносим
   и слегка ужимаем на узких экранах, вместо того чтобы ломать её на две
   строки посреди слова. */
#adminMailingSend { white-space: nowrap; }


/* Перебор по длине: красным и с коротким подрагиванием. Цвет говорит, что
   что-то не так, движение - что это случилось именно сейчас, от последнего
   набранного символа. */
.mailing-len-over { animation: len-shake .3s ease-out; font-weight: 600; }
@keyframes len-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-3px); }
  75%      { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .mailing-len-over { animation: none; }
}


/* Счётчик внутри поля, в правом нижнем углу. Сквозной для касаний: иначе
   он перехватывал бы палец у самого края текста. Поле получает отступ снизу,
   чтобы последняя строка не забегала под число. */
.mailing-text-wrap { position: relative; }
.mailing-text-wrap .feedback-textarea { padding-bottom: 26px; }
.mailing-len {
  position: absolute; right: 12px; bottom: 10px;
  font-size: 11.5px; color: var(--fg-dim); pointer-events: none;
  background: transparent;
}
.mailing-len.error { color: #b02a1e; }

/* Строка действий: значок камеры слева, отправка справа. */
.mailing-actions { display: flex; align-items: stretch; gap: 8px; margin-top: 8px; }
.mailing-actions .submit-btn { flex: 1 1 auto; margin: 0; }
.mailing-photo-btn {
  flex: 0 0 auto; width: 52px; border-radius: 12px;
  border: 1px solid var(--border); background: var(--card);
  font-size: 20px; line-height: 1; cursor: pointer; font-family: inherit;
}
.mailing-photo-btn:disabled { opacity: .4; }


/* Стоп рядом с отправкой, той же высоты. Красный и квадратный: его ищут
   глазами в момент, когда всё идёт не так, и он не должен быть похож
   на соседнюю кнопку. */
.mailing-stop-btn {
  flex: 0 0 auto; width: 52px; border-radius: 12px;
  border: 1px solid color-mix(in srgb, #dc2626 40%, transparent);
  background: color-mix(in srgb, #dc2626 12%, transparent);
  color: #b02a1e; font-size: 18px; line-height: 1;
  cursor: pointer; font-family: inherit;
}
.mailing-stop-btn:active { opacity: .75; }


/* Разосланное сообщение: кнопка гаснет и перестаёт нажиматься. Серый вместо
   зелёного - чтобы палец, летящий по привычке в ту же точку, увидел разницу
   раньше, чем нажмёт. */
#adminMailingSend.is-done {
  background: var(--card); color: var(--fg-dim);
  box-shadow: none; cursor: default;
}
#adminMailingSend.is-done::after { display: none; }


/* Черта перед архивом: он не часть отправки, а взгляд назад. */
.mailing-divider { margin: 16px 0 12px; }

/* Фотографии в архиве мельче - их разглядывают, только чтобы вспомнить,
   какая это была рассылка, а не чтобы выбирать порядок. */
.arch-photos { grid-template-columns: repeat(5, 1fr); gap: 4px; }
.arch-photos .mailing-photo { animation: none; }


/* Пост в архиве - в рамке сообщения, как его видел получатель: фотографии
   сверху, текст под ними. Список цифр без самого поста не даёт вспомнить,
   что именно рассылали. */
.arch-post {
  margin-top: 10px; padding: 10px 12px;
  border-radius: 12px; background: var(--card);
}
.arch-text {
  font-size: 13px; line-height: 1.5; color: var(--fg);
  white-space: pre-line; margin-top: 8px;
}
.arch-post .arch-photos { margin: 0; }
.arch-post .arch-photos:only-child { margin: 0; }


/* ── Заказ рекламы в чатах ───────────────────────────────────────────────── */
/* Календарь плиткой по четыре: две недели укладываются в четыре ряда, и весь
   выбор виден без прокрутки - а прокрутка на выборе дат сбивает: человек
   не видит, что было слева. */
/* Календарь рекламы в чатах пользуется теми же плитками, что бронь баннера
   (.schedule-*): один и тот же выбор суток не должен выглядеть по-разному
   на двух экранах. Свой отступ - единственное, что здесь добавлено. */
#adChatDays { margin: 8px 0; }

.ad-price {
  margin: 12px 0 4px; font-size: 15px; text-align: center; color: var(--fg-dim);
}
.ad-price b { font-size: 19px; color: var(--fg); }

/* Видео в плитках показываем значком: кадр из файла браузер отдаёт не всегда,
   а пустой квадрат читается как незагрузившееся фото. */
/* Ролик в ленте выглядит как фотография: тот же размер, та же обрезка, тот же
   радиус. Лента из фото и видео должна читаться одной лентой, а не двумя
   разными вещами рядом. Нажатия не ловим - иначе iOS раскрывает видео на весь
   экран вместо того, чтобы дать перетащить плитку. */
.ad-video-tile {
  width: 100%; height: 100%; border-radius: 10px; background: var(--card);
  object-fit: cover; display: block; pointer-events: none;
}
/* Заглушка - когда файла ещё нет или он слишком тяжёлый для обратной отдачи. */
.ad-video-stub {
  display: flex; align-items: center; justify-content: center; font-size: 26px;
}
/* Длительность в углу: у играющего ролика нет ни одной подсказки, сколько он
   идёт, а для рекламного поста это первое, что нужно знать. */
.ad-video-dur {
  position: absolute; right: 3px; bottom: 3px;
  height: 16px; padding: 0 5px; border-radius: 999px;
  background: rgba(0, 0, 0, .62); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 16px;
}
/* Строка хода работы вместо подсказки: сжатие идёт столько же, сколько сам
   ролик, и без цветного отличия человек читает её как обычный текст и уходит. */
.hint.is-work { color: var(--accent); font-weight: 600; }


/* Вкладки «Моей рекламы» - той же формы, что разрезы графика. */
#adsMineTabs { margin: 0 0 10px; }


/* ── Карточка одобренного поставщика ─────────────────────────────────────── */
/* Была выписка «поле: значение» построчно - так выглядит база данных, а не
   человек, которого выбирают. Стало: имя и подтверждение сверху, место и
   телефоны строками со значками, категории плитками. */
.sup-card {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 16px; padding: 14px; margin: 10px 0;
}
.sup-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.sup-ava {
  width: 46px; height: 46px; flex: 0 0 auto; border-radius: 50%;
  background: var(--card); color: var(--fg-dim);
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; font-weight: 700;
}
.sup-head-text { min-width: 0; }
.sup-name { font-size: 16px; font-weight: 600; letter-spacing: -0.2px; }
.sup-sub { font-size: 12.5px; color: var(--ok, #0f9d58); font-weight: 600; margin-top: 2px; }

.sup-row {
  display: flex; gap: 8px; align-items: baseline;
  font-size: 13.5px; padding: 4px 0; border-top: 1px solid var(--border);
}
.sup-ico { flex: 0 0 auto; }

/* Категории плитками: их выхватывают глазами по одной, а строка через запятую
   читается целиком, даже когда нужна одна. */
.sup-cats { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.sup-cat {
  font-size: 11.5px; line-height: 1.7; padding: 1px 9px;
  border-radius: 999px; background: var(--card); color: var(--fg-dim);
}


/* Плитка «+» в конце ленты медиа: добавление стоит там же, где перестановка,
   и лента читается как один набор, а не как список плюс кнопка внизу. */
.ad-add-tile {
  border: 1px dashed var(--border); background: var(--card); color: var(--fg-dim);
  border-radius: 10px; font-size: 24px; line-height: 1; cursor: pointer;
  font-family: inherit; padding: 0;
  display: flex; align-items: center; justify-content: center;
  animation: none;
}
.ad-add-tile:active { opacity: .7; }


/* Плитки объявления ведут себя как в рассылке: взятая приподнимается, слегка
   наклоняется и отбрасывает тень, соседи разъезжаются плавно. Отдельное
   правило нужно, потому что общий стиль .mailing-photo действует и здесь,
   а вот скругление и переход у картинки внутри - свои. */
#adChatPhotos .mailing-photo { touch-action: none; }
#adChatPhotos .mailing-photo.is-dragging {
  transform: scale(1.1) rotate(-2deg); opacity: .9; z-index: 3;
  box-shadow: 0 8px 20px rgba(20, 24, 28, .28);
}
#adChatPhotos img { border-radius: 10px; }


/* ── Просмотр прикреплённого медиа ────────────────────────────────────────
   Тёмная подложка и кнопки поверх файла. Курсор не «zoom-out», как у обычного
   увеличения фото: отсюда листают, а не только закрывают. */
.photo-zoom.mv { cursor: default; }

.mv-stage {
  max-width: 100%; max-height: 100%;
  display: flex; align-items: center; justify-content: center;
  padding: 0 52px;   /* место под стрелки: иначе они лежат на самом снимке */
}
.mv-stage img, .mv-stage video {
  max-width: 100%; max-height: 76vh;
  border-radius: 14px; display: block; background: #000;
}

/* Счётчик вверху по центру: на десяти файлах без него не понять, где ты. */
.mv-count {
  position: absolute; top: 16px; left: 0; right: 0; text-align: center;
  color: rgba(255, 255, 255, .74); font-size: 13px; font-weight: 700;
  letter-spacing: .02em; pointer-events: none;
}

.mv-close {
  position: absolute; top: 10px; right: 12px; z-index: 2;
  width: 34px; height: 34px; padding: 0; border: none; border-radius: 50%;
  background: rgba(255, 255, 255, .14); color: #fff; font-size: 15px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; font-family: inherit;
}

/* Стрелки прижаты к краям и высокие: на ходу в них попадают не глядя. */
.mv-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 42px; height: 60px; padding: 0; border: none; border-radius: 12px;
  background: rgba(255, 255, 255, .12); color: #fff;
  font-size: 30px; line-height: 1; font-family: inherit;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.mv-prev { left: 6px; }
.mv-next { right: 6px; }
.mv-arrow:active, .mv-close:active { background: rgba(255, 255, 255, .26); }
.mv-arrow.hidden { display: none; }

/* На узком экране стрелки съедают снимок - уводим их вниз, к краям. */
@media (max-width: 420px) {
  .mv-stage { padding: 0 8px; }
  .mv-arrow { top: auto; bottom: 18px; transform: none; width: 56px; height: 46px; }
}


/* Поднятая плитка объявления. Тень и лёгкая полупрозрачность говорят, что
   она «в руке»; сам сдвиг и увеличение задаёт код - плитка идёт за пальцем,
   и правило в стилях перебивало бы её положение на каждом кадре. */
.mailing-photo.is-lifted {
  z-index: 3;
  opacity: .94;
  animation: none;
  box-shadow: 0 12px 26px rgba(20, 24, 28, .34);
  will-change: transform;
}
.mailing-photo.is-lifted .mailing-photo-x { opacity: 0; }

/* Соседи расступаются заметно, но не медленно: на перестановке в четыре
   колонки путь короткий, и долгая дорога читается как подтормаживание. */
#adChatPhotos .mailing-photo {
  transition: transform .24s cubic-bezier(.22, .61, .36, 1),
              opacity .18s ease, box-shadow .2s ease;
}

@media (prefers-reduced-motion: reduce) {
  #adChatPhotos .mailing-photo { transition: none; }
}


/* Название площадки, которое ведёт в канал.
   Подчёркивание рисуем фоном, а не text-decoration: у фона можно анимировать
   ширину, у подчёркивания - нельзя. Слоя два и лежат они друг на друге:
   пунктир в состоянии покоя и сплошная линия, которая при наведении
   вырастает слева направо, как будто ссылку проводят пальцем. Пунктир
   остаётся на месте - линия не появляется из пустоты, а закрашивает то,
   что уже было.
   Сплошным подчёркиванием в покое здесь нельзя: строка длинная и жирная,
   и линия под ней читается как зачёркивание. */
.place-name.is-link {
  color: var(--link);
  cursor: pointer;
  text-decoration: none;
  padding-bottom: 3px;
  background-repeat: no-repeat;
  background-image:
    repeating-linear-gradient(to right, currentColor 0 2px, transparent 2px 5px),
    linear-gradient(to right, currentColor, currentColor);
  background-position: 0 100%, 0 100%;
  background-size: 100% 1px, 0 2px;
  transition: background-size .3s cubic-bezier(.22, .61, .36, 1),
              filter .18s ease;
}

/* Только там, где указатель действительно есть. На телефоне :hover
   «залипает» после нажатия, и ссылка осталась бы подчёркнутой навсегда. */
@media (hover: hover) {
  .place-name.is-link:hover {
    background-size: 100% 1px, 100% 2px;
    filter: brightness(1.15);
  }
}

/* Отклик на палец. Наведения на телефоне не бывает, поэтому здесь работает
   не :hover, а короткая анимация, которую ставит код по нажатию: линия
   пробегает слева направо, имя на миг светлеет - и гаснет обратно.
   Через :active это делать нельзя: в iOS он на обычных элементах срабатывает
   через раз, и половина нажатий оставалась бы без отклика. */
.place-name.is-hit {
  animation: place-link-hit .42s cubic-bezier(.22, .61, .36, 1);
}

@keyframes place-link-hit {
  0%   { background-size: 100% 1px, 0 2px;    filter: none; }
  45%  { background-size: 100% 1px, 100% 2px; filter: brightness(1.3); }
  100% { background-size: 100% 1px, 0 2px;    filter: none; }
}

@media (prefers-reduced-motion: reduce) {
  .place-name.is-hit { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .place-name.is-link { transition: none; }
}
