/* ═══════════════════════════════════════════════════════════════════════
   АРБОБОТ · ДИЗАЙН-КИТ «СТАЛЬ»
   Единственный источник стилей для Mini App и всех будущих экранов.

   Характер: «шина пилы и утренний туман» — холодный светлый фон, петролевый
   акцент, малые радиусы, плотная сетка, цифры моноширинной точности.
   Это прибор, а не журнал.

   Правила для разработчиков:
   • Цвета — ТОЛЬКО через var(--…). Хекс в компонентах запрещён.
   • Отступы — шаг 4px: используйте var(--sp-1..6), не выдумывайте свои.
   • Радиусы — три и только три: --r-card / --r-btn / --r-chip.
   • Числа (цены, счётчики, №, даты) — всегда с .num или font-variant-numeric.
   • Новые компоненты добавляйте сюда же и показывайте на странице /twa/kit/.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   КАК В ЭТОМ ПРИЛОЖЕНИИ ГРУЗЯТСЯ ДАННЫЕ

   Это не рекомендация, а правило: каждый раздел ходит за данными через
   loadSection() из index.html. Раньше каждый грузился по-своему, мерцание
   лечили по месту — вылечили в одном, вылезло в другом, и так по кругу.

       loadSection('ключ', {
         host, path, render(data, meta), skeleton(host), onError(err),
         force, staleMs
       })

   ПРАВИЛА. Они взяты не из головы: SWR (stale-while-revalidate) из
   HTTP-кэширования, keepPreviousData у TanStack Query и пороги восприятия
   NN/g.

   1. Есть данные — рисуем НЕМЕДЛЕННО, запрос уходит в фоне. Ответ совпал с
      тем, что уже показано, — DOM не трогаем вовсе.
   2. Нет данных совсем — каркас, но не раньше 200 мс. Ответ из локальной
      сети приходит быстрее, и каркас успел бы только мигнуть: до 100 мс
      индикатор не нужен вообще, каркас уместен на 400 мс–3 с.
   3. При смене вкладки прежнее содержимое остаётся, пока не придут новые
      данные. Пустой экран между двумя списками читается как поломка.
   4. Свежие данные (моложе staleMs, по умолчанию 15 с) сеть не дёргают:
      возврат в раздел через секунду — не повод перезапрашивать. Явное
      обновление (оттяжка, действие пользователя) передаёт force: true.
   5. Один и тот же запрос в полёте не дублируется.
   6. Список перерисовывается СВЕРКОЙ (syncRows по ключу и подписи строки), а
      не пересборкой: innerHTML = '' сбрасывает позицию прокрутки, ломает
      загруженные картинки и мигает.
   7. Проявление (fade-in) — ОДИН раз за раздел, на первую отрисовку. Второе
      проявление того же списка и есть то самое мерцание; именно это
      выяснилось замером, когда «мерцание не ушло» после двух попыток.
   8. Ошибку показываем, только если показать больше нечего. Поверх живого
      списка она сообщает «всё пропало», хотя всё на месте.

   ЧЕГО НЕ ДЕЛАТЬ. Не заводить рядом второй путь загрузки «только для этого
   экрана» — так и появляются расхождения. Не звать render() вручную мимо
   loadSection. Не забывать вернуть обещание из своей обёртки: вызов, чей
   результат undefined, роняет .then у вызывающего СИНХРОННО и молча уносит
   всё, что стояло после него.

   ═══ ТАБЫ: ТОЛЬКО ЧЕРЕЗ segPanes ═══════════════════════════════════════
   Табы в приложении делает ОДИН компонент — segPanes(tabsHost, items, panes,
   active, onShow). Своих переключателей больше не заводим: на самодельном
   переходе «Кабинета» подряд вылезло пять дефектов, а в колоколе перехода не
   было вовсе, потому что строка кнопок жила внутри перерисовываемого узла.

   Разметка, которую компонент ожидает найти готовой:

     <div id="…SegHost"></div>            строка кнопок появится здесь
     <div class="seg-panes">
       <div class="list"></div>           панель первого таба
       <div class="list hidden"></div>    панель второго
     </div>

   От двух до трёх пунктов: четыре на узком экране не читаются. Подпись
   переносить нельзя — если не влезает, подпись длинная, а не кнопка узкая.

   Правила, ради которых компонент и существует:
   1. Строка кнопок живёт ВНЕ панелей и переживает перерисовку содержимого.
      Внутри — подложка прыгает в начало, а подсветка на миг отменяет только
      что сделанный выбор.
   2. Панели — разные узлы. Один узел на два таба означает пересборку вместо
      перехода.
   3. Данные грузятся ДО движения, в onShow(key, pane): приходящая панель
      должна въезжать уже с содержимым, а не подставлять каркас на полпути.
   4. Высоту обёртки на время перехода ведёт JS — от уходящей панели к
      приходящей. Подпирать выдуманным min-height нельзя: в конце перехода
      подпорка отваливается и высота прыгает (замер в бою: 2705 → 449 и
      обратно, со сбитой прокруткой).
   5. Класс проявления снимается вместе с классами разъезда. Оба задают
      свойство animation на одном узле, и оставшийся стартует с нуля —
      контент проваливается в opacity 0 и проявляется заново.
   6. Тема, фон и отступы — из токенов; своих цветов у табов нет.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Шрифт: IBM Plex Sans, вариативный, самохостинг ──
   Google Fonts у пользователей в РФ бывает недоступен — не подключать CDN. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url(fonts/plex-cyr.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url(fonts/plex-lat.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+2000-206F, U+20BD;
}

/* ═══ ТОКЕНЫ ═══════════════════════════════════════════════════════════
   Светлая тема — базовая. Тёмная переопределяет ТОЛЬКО токены:
   компоненты стилизуются через переменные и о темах не знают.       */
:root {
  /* поверхности */
  --bg:      #F5F7F8;   /* фон приложения */
  --surface: #FFFFFF;   /* карточки, панели, таббар */
  --soft:    #E9F0EF;   /* приглушённая подложка: чипы, аватары, second-кнопки */
  --line:    #E3E8EB;   /* все границы */

  /* текст */
  --ink:  #1C2226;      /* основной */
  --mute: #5E6B73;      /* вторичный: подписи, мета */

  /* засечки линейки: крупные заметно темнее мелких, но видны обе */
  --ruler-major: #7E8C93;
  --ruler-minor: #AAB6BC;

  /* акцент — петроль. Один на всю систему */
  --accent:      #196A62;
  --accent-deep: #14544E;   /* текст акцентного цвета на --soft */
  /* Подложка и граница для карточек, которые надо выделить из ряда, не
     прибегая к тревожному цвету: подсказки профиля, важные подсказки. */
  --accent-soft: #E4EFED;
  --accent-line: #BFD9D5;

  /* семантика состояния (не смешивать с акцентом) */
  --ok:      #2F9C60;  --ok-deep: #1F6B42;  --ok-soft: #E6F3EB;
  --warn:    #B07818;  --warn-deep: #7A5310; --warn-soft: #F4EDDC;
  --danger:  #B3432E;  --danger-deep: #8C3322; --danger-soft: #F6E7E3;

  /* кнопки */
  --btn-bg:  #196A62;
  --btn-ink: #FFFFFF;

  /* геометрия */
  --r-card: 12px;   /* карточки, панели, поля ввода */
  --r-btn:  10px;   /* кнопки */
  --r-chip: 8px;    /* пилюли, миниатюры, мелочь */

  /* движение: единые премиум-кривые и длительности (тактильный отклик) */
  --ease-out:    cubic-bezier(.22, 1, .36, 1);    /* мягкое торможение (входы) */
  --ease-spring: cubic-bezier(.34, 1.42, .5, 1);  /* лёгкий овершут (нажатия, бейджи) */
  --dur-1: .12s;   /* микро: press */
  --dur-2: .2s;    /* обычные переходы */
  --dur-3: .34s;   /* экраны, шторки */
  --dur-4: .52s;   /* фон и дальние слои: медленнее контента — даёт параллакс */

  /* шаг отступов (4px-сетка) */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px;
  --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;

  /* высоты контролов: три и только три */
  --ctl-main: 50px;  /* главная кнопка экрана */
  --ctl:      40px;  /* обычные кнопки и поля в строке */
  --ctl-sm:   32px;  /* компактные, в шапку */

  /* тень — одна, едва заметная. «Сталь» не парит, а лежит */
  --sh: 0 1px 2px rgba(28,34,38,.06);

  --skeleton: linear-gradient(90deg, var(--line) 25%, var(--soft) 50%, var(--line) 75%);

  --font: 'IBM Plex Sans', -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* шкала шрифтов: 5 ступеней, других размеров не заводить */
  --fs-title: 20px;   /* заголовок экрана */
  --fs-lead:  17px;   /* цена в ленте, крупный факт */
  --fs-body:  14px;   /* основной текст */
  --fs-sub:   12.5px; /* подписи, мета */
  --fs-cap:   11.5px; /* капс-ярлыки, таббар */
  --fs-hero:  30px;   /* цена в карточке — единственный «большой» размер */
}

/* Тёмная «Сталь»: темнеет легче всех — графит с петролевой подсветкой.
   Двойной селектор: JS ставит data-theme (источник правды — тема Telegram),
   а @media ловит СИСТЕМНУЮ тёмную ещё до запуска JS — иначе первый кадр
   светлый и на тёмном клиенте видно мигание светлое→тёмное перед онбордингом. */
:root[data-theme="dark"] {
  --bg:      #12171A;
  --surface: #1A2125;
  --soft:    #1C2B29;
  --line:    #283238;
  --ink:  #E7ECEE;
  --mute: #8FA0A8;
  --ruler-major: #8C9BA3;
  --ruler-minor: #4A585F;
  --accent:      #3BA79A;
  --accent-deep: #63C4B8;
  --accent-soft: #17322F;
  --accent-line: #24504B;
  --ok: #41B274;  --ok-deep: #79D3A6;  --ok-soft: #163024;
  --warn: #C98F2C; --warn-deep: #E4B96A; --warn-soft: #2E2618;
  --danger: #C75B45; --danger-deep: #E79A8A; --danger-soft: #331F1A;
  --btn-bg:  #1E7A70;
  --btn-ink: #F2F7F6;
  --sh: 0 1px 2px rgba(0,0,0,.5);
  --skeleton: linear-gradient(90deg, var(--line) 25%, #303B41 50%, var(--line) 75%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:      #12171A;
    --surface: #1A2125;
    --soft:    #1C2B29;
    --line:    #283238;
    --ink:  #E7ECEE;
    --mute: #8FA0A8;
    --ruler-major: #8C9BA3;
    --ruler-minor: #4A585F;
    --accent:      #3BA79A;
    --accent-deep: #63C4B8;
  --accent-soft: #17322F;
  --accent-line: #24504B;
    --ok: #41B274;  --ok-deep: #79D3A6;  --ok-soft: #163024;
    --warn: #C98F2C; --warn-deep: #E4B96A; --warn-soft: #2E2618;
    --danger: #C75B45; --danger-deep: #E79A8A; --danger-soft: #331F1A;
    --btn-bg:  #1E7A70;
    --btn-ink: #F2F7F6;
    --sh: 0 1px 2px rgba(0,0,0,.5);
    --skeleton: linear-gradient(90deg, var(--line) 25%, #303B41 50%, var(--line) 75%);
  }
}

/* ═══ БАЗА ═════════════════════════════════════════════════════════════ */
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; overflow-x: hidden; }
/* Полосу прокрутки прячем. На телефоне её и так нет, а на ПК она появлялась
   только на длинных вкладках (лента, «Кабинет») и пропадала на короткой
   (профиль) — ширина колонки прыгала при каждом переключении раздела.
   Прячем и у html, и у body: реально прокручивается ИМЕННО body (при
   height:100% + overflow-x:hidden вторая ось становится auto, и контейнером
   прокрутки оказывается он, а не документ — замерено: полоса 15 px у body).
   Саму прокрутку это не трогает: колесо, тач и клавиатура работают как прежде. */
html, body { scrollbar-width: none; }
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font-family: var(--font); font-size: var(--fs-body); line-height: 1.45;
  -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
img, video { display: block; max-width: 100%; }
.hidden { display: none !important; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* числа: табличные цифры везде, где есть счёт */
.num { font-variant-numeric: tabular-nums; }

.app { max-width: 480px; margin-inline: auto; min-height: 100%; display: flex; flex-direction: column; }

/* ═══ ШАПКА ЭКРАНА ═════════════════════════════════════════════════════
   <header class="hdr"><div class="hdr-row"><h1>…</h1></div>
   <div class="ruler"></div></header>                                  */
.hdr { padding: 14px var(--sp-4) 0; }
.hdr-row { display: flex; align-items: center; gap: var(--sp-2); }
.hdr h1 { margin: 0; font-size: var(--fs-title); font-weight: 700; letter-spacing: -.01em; }
.hdr-count { font-size: 13px; color: var(--mute); font-variant-numeric: tabular-nums; align-self: baseline; }
.hdr-spacer { margin-left: auto; }

/* Линейка — фирменный разделитель «Стали». Только под шапкой, не в контенте.
   Засечки вынесены в отдельную ленту внутри: её можно возить transform-ом при
   смене экранов, не трогая раскладку и не перекрашивая фон. */
.ruler { height: 10px; margin: 10px 2px 0; overflow: hidden; }
.ruler-tape {
  display: block; height: 100%; width: 300%; margin-left: -100%;
  /* Мелкие засечки бледнее крупных ЦВЕТОМ, а не общей прозрачностью. Раньше на
     всю линейку висел opacity .55, и в светлой теме мелкие (--line #E3E8EB на
     фоне #F5F7F8) пропадали полностью. */
  background:
    repeating-linear-gradient(90deg, var(--ruler-minor) 0 1px, transparent 1px 8px),
    repeating-linear-gradient(90deg, var(--ruler-major) 0 1px, transparent 1px 40px);
  background-size: 100% 5px, 100% 10px;
  background-position: 0 100%, 0 100%;
  background-repeat: repeat-x;
}

main {
  flex: 1;
  padding: var(--sp-3) var(--sp-4) 84px;
  /* отдельной строкой: WebView без env() отбросит только её, поля останутся */
  padding-bottom: calc(84px + env(safe-area-inset-bottom));
}
.list { display: flex; flex-direction: column; gap: 10px; }

/* ═══ КНОПКИ ═══════════════════════════════════════════════════════════
   .mainbtn   — главное действие экрана, одна на экран
   .btn-soft  — второстепенная (подложка --soft)
   .btn-small — компактная, в шапку/в строку
   .btn-danger-soft — деструктивная второстепенная                     */
.mainbtn {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: var(--ctl-main);
  background: var(--btn-bg); color: var(--btn-ink);
  border-radius: var(--r-btn); text-align: center;
  font-weight: 700; font-size: 14.5px; letter-spacing: .06em;
  padding: 0 10px; transition: transform .12s ease, opacity .12s ease;
}
.mainbtn:active { transform: scale(.98); }
.mainbtn[disabled] { opacity: .55; pointer-events: none; }
.btn-sub { text-align: center; font-size: 12px; color: var(--mute); }
/* кнопка + подпись под ней держатся вместе, шаг между ними меньше общего */
.d-action { display: flex; flex-direction: column; gap: var(--sp-2); }

/* Второстепенные кнопки — одна высота --ctl, различаются только цветом роли */
.btn-soft, .btn-danger-soft {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--ctl); padding: 0 20px;
  font-weight: 600; font-size: 13.5px; border-radius: var(--r-btn);
}
.btn-soft { background: var(--soft); color: var(--accent-deep); }
.btn-danger-soft { background: var(--danger-soft); color: var(--danger-deep); }
.btn-small {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--ctl-sm); padding: 0 14px;
  background: var(--btn-bg); color: var(--btn-ink);
  font-weight: 600; font-size: 13px; border-radius: var(--r-btn);
}
.retry { margin-top: 14px; } /* модификатор .btn-soft внутри .state-box */

/* ═══ ПИЛЮЛЯ СТАТУСА ═══════════════════════════════════════════════════
   <span class="st">Опубликован</span>  (+ .work / .gray / .warn)      */
.st {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; padding: 4px 10px;
  border-radius: var(--r-chip);
  color: var(--ok-deep); background: var(--ok-soft);
}
.st::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }
.st.gray { color: var(--mute); background: var(--soft); }
.st.gray::before { background: var(--mute); }
.st.work { color: var(--accent-deep); background: var(--soft); }
.st.work::before { background: var(--accent); }
.st.warn { color: var(--warn-deep); background: var(--warn-soft); }
.st.warn::before { background: var(--warn); }

/* ═══ КАРТОЧКА ЗАКАЗА В ЛЕНТЕ ══════════════════════════════════════════ */
.oc {
  display: block; width: 100%; text-align: left;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--sp-3) 14px; box-shadow: var(--sh);
  transition: transform .12s ease, border-color .12s ease;
}
.oc:active { transform: scale(.985); border-color: var(--accent); }
.oc-top { display: flex; align-items: center; gap: var(--sp-2); font-size: 12px; color: var(--mute); font-variant-numeric: tabular-nums; min-width: 0; }
/* Номер и чип статуса не ужимаются: «№ 26» ломалось пополам, а подпись чипа
   переносилась на вторую строку — подложка раздувалась вдвое и выдавливала
   всё остальное. Обоим нужно ровно столько места, сколько занимает текст. */
.oc-top .num { flex: none; white-space: nowrap; }
.oc-top .oc-resp-badge, .oc-top .badge-own { flex: none; white-space: nowrap; }
/* Гео — своей строкой во всю ширину, до двух строк. Это главное, по чему
   выбирают заказ: район и направление важнее, чем экономия одной строки. */
.oc-geo {
  margin-top: 5px; font-size: 12px; color: var(--mute); line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.oc-when { margin-left: auto; flex: none; }
.oc-body { display: flex; gap: 10px; margin-top: 6px; }
.oc-text {
  margin: 0; flex: 1; font-size: 13.5px; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere;
}
.thumb {
  width: 64px; height: 64px; flex: none; border-radius: var(--r-chip);
  background: var(--soft); overflow: hidden; position: relative;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.thumb .more {
  position: absolute; right: 3px; bottom: 3px;
  font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums;
  background: rgba(18,23,26,.72); color: #fff; border-radius: 6px; padding: 1px 5px;
}
.oc-price-row { margin-top: var(--sp-2); display: flex; align-items: baseline; gap: 8px; }
.oc-price { font-size: var(--fs-lead); font-weight: 700; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
/* Комиссия — чип справа от цены: приглушённый, чтобы не спорить с суммой. */
.oc-comm {
  margin-left: auto; flex: none; align-self: center;
  font-size: 11.5px; font-weight: 600; color: var(--mute);
  background: var(--soft); border-radius: var(--r-chip); padding: 3px 8px;
  font-variant-numeric: tabular-nums;
}
.oc-meta { margin-top: 3px; font-size: var(--fs-sub); color: var(--mute); font-variant-numeric: tabular-nums; }

/* ═══ НАКЛАДНОЙ ЭКРАН (карточка, форма) ════════════════════════════════
   <section class="screen"><div class="d-wrap">…</div></section>
   Показ: .screen.on; закрытие — Telegram BackButton                   */
.screen {
  position: fixed; inset: 0; z-index: 20; background: var(--bg);
  transform: translateX(100%); transition: transform var(--dur-3) var(--ease-out);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding-top: 14px;
}
/* Полосу прокрутки прячем и здесь. Правило для html/body накладной экран не
   покрывает: у него СВОЙ контейнер прокрутки (overflow-y: auto), и на ПК он
   рисовал собственную полосу 15 px — замерено на уведомлениях с длинным
   списком, при том что у body полоса уже была нулевой. Ширина колонки от этого
   прыгала на каждом открытии экрана. Саму прокрутку не трогаем. */
.screen { scrollbar-width: none; }
.screen::-webkit-scrollbar { width: 0; height: 0; }
.screen.on { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .screen { transition: none; }
  .sk i { animation: none; }
}
/* Горизонтальные поля живут на обёртке, а не на .screen: так они не зависят
   от судьбы сложных деклараций и одинаковы на любом WebView */
/* Контент экрана — вертикальный стек с равным шагом. Спейсинг ОДИН, на
   контейнере: добавленный элемент получает отступ сам, ничто не «слипнется».
   Правило кита: внутри .d-wrap НЕ добавлять element-margin сверху/снизу —
   расстояние задаёт gap. Логически слитные пары (цена+инфо, кнопка+подпись)
   оборачиваются в .d-group, где внутренний шаг меньше. */
.d-wrap {
  max-width: 480px; margin-inline: auto;
  display: flex; flex-direction: column; gap: var(--sp-4);
  padding: 0 var(--sp-4) var(--sp-5);
  padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom));
}
.d-group { display: flex; flex-direction: column; gap: var(--sp-1); }

.d-top { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sub); }
.d-num { color: var(--mute); margin-left: auto; font-variant-numeric: tabular-nums; }
.d-price-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.d-price { font-size: var(--fs-hero); font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.d-price.txt { font-size: 20px; }
/* комиссия — не серый хвост, а маленький чип: читается как отдельный факт */
.d-fee {
  font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--accent-deep); background: var(--soft);
  border-radius: var(--r-chip); padding: 3px 9px; align-self: center;
}
/* Расшифровка чипа комиссии. Само слово «комиссия» двусмысленно: деньги идут
   не площадке, а заказчику — арбористу, который передал свой заказ. Без этой
   строки исполнитель читает процент как поборы сервиса. */
.d-fee-note { font-size: 12px; color: var(--mute); margin-top: 4px; line-height: 1.4; }
.d-info { font-size: var(--fs-sub); color: var(--mute); line-height: 1.5; overflow-wrap: anywhere; }

/* Факты заказа — карточка со строками «иконка + подпись + значение».
   Регион и дата вместо dot-строки: сканируется взглядом, выглядит прибором. */
.facts-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  box-shadow: var(--sh); overflow: hidden;
}
.fact {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
}
.fact + .fact { border-top: 1px solid var(--line); }
.fact-ic {
  width: 34px; height: 34px; flex: none; border-radius: var(--r-chip);
  background: var(--soft); display: grid; place-items: center; color: var(--accent);
}
.fact-ic svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.9; }
.fact-body { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.fact-label { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--mute); }
.fact-value { font-size: 14.5px; color: var(--ink); overflow-wrap: anywhere; }

/* Описание задачи с тонкой подписью-ярлыком — как пункт опросника */
.d-text-block { display: flex; flex-direction: column; gap: 6px; }
.d-text-label { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--mute); }
.d-text { font-size: 14.5px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }

/* ═══ ГАЛЕРЕЯ ══════════════════════════════════════════════════════════ */
.gallery-wrap { position: relative; margin-inline: calc(-1 * var(--sp-4)); }
.gallery {
  display: flex; gap: var(--sp-2); overflow-x: auto;
  /* горизонтальный вылет к краям экрана; вертикальный отступ даёт gap .d-wrap */
  padding: 0 var(--sp-4);
  scroll-snap-type: x mandatory; scrollbar-width: none;
  scroll-behavior: smooth;
}
.gallery::-webkit-scrollbar { display: none; }
.g-item { cursor: zoom-in; }

/* Стрелки листания — нужны на ПК (свайпа нет). На тач работают тоже, но их
   прячем, если устройство листает пальцем и указателя нет. */
/* Стрелки лежат поверх фото — та же схема «читаемо на любом фоне»:
   тёмная полупрозрачная подложка, белая иконка, светлая обводка. */
.gal-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(18,22,26,.42);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  border: 1px solid rgba(255,255,255,.26); box-shadow: 0 2px 8px rgba(0,0,0,.35);
  color: #fff; transition: opacity .15s ease, background .15s ease;
}
.gal-nav:active { background: rgba(18,22,26,.62); }
.gal-nav.prev { left: calc(var(--sp-4) - 6px); }
.gal-nav.next { right: calc(var(--sp-4) - 6px); }
.gal-nav svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.2; }
.gal-nav[disabled] { opacity: 0; pointer-events: none; }
@media (hover: none) and (pointer: coarse) {
  /* палец умеет свайпать — стрелки не навязываем, но оставляем при hover-мышке */
  .gal-nav { display: none; }
}

/* ═══ ПОЛНОЭКРАННЫЙ ПРОСМОТР ФОТО ══════════════════════════════════════
   Открытие по тапу на фото; закрытие — фон, крестик, системная «назад». */
.lightbox {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(8, 11, 13, .96);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
  touch-action: none;
}
.lightbox.on { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .lightbox { transition: none; } }
.lb-img {
  max-width: 100vw; max-height: 100vh; max-height: 100dvh;
  object-fit: contain; user-select: none; -webkit-user-drag: none;
}
/* Полупрозрачные, но читаемые на ЛЮБОМ фоне: тёмная подложка + белая иконка
   + светлая обводка. На светлом фото виден тёмный круг, на тёмном — обводка. */
.lb-close, .lb-nav {
  background: rgba(18,22,26,.44);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: #fff; border: 1px solid rgba(255,255,255,.28);
  box-shadow: 0 2px 10px rgba(0,0,0,.4);
}
.lb-close:active, .lb-nav:active { background: rgba(18,22,26,.66); }
.lb-close {
  position: fixed; top: 10px; top: max(10px, env(safe-area-inset-top));
  right: 12px; z-index: 62;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
}
.lb-close svg { width: 20px; height: 20px; stroke: currentColor; stroke-width: 2.4; fill: none; }
.lb-nav {
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 62;
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
}
.lb-nav.prev { left: 10px; }
.lb-nav.next { right: 10px; }
.lb-nav svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 2.4; fill: none; }
.lb-nav[hidden], .lb-close[hidden] { display: none; }
/* Одно фото — на всю ширину карточки, без сноса в скролл-галерею (item 6). */
.gallery.one .g-item { width: 100%; max-width: 100%; scroll-snap-align: none; }

/* Лента миниатюр в полноэкранном просмотрщике (Telegram-стиль, item 6). */
.lb-thumbs {
  position: fixed; left: 0; right: 0; z-index: 62;
  bottom: 8px; bottom: max(8px, env(safe-area-inset-bottom));
  display: flex; gap: 6px; padding: 0 12px; overflow-x: auto; justify-content: center;
}
.lb-thumb {
  flex: none; width: 46px; height: 46px; border-radius: 8px; overflow: hidden; padding: 0;
  border: 2px solid transparent; background: rgba(255, 255, 255, .12);
  opacity: .5; cursor: pointer; transition: opacity .15s ease;
}
.lb-thumb.on { opacity: 1; border-color: #fff; }
.lb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (prefers-reduced-motion: reduce) { .lb-thumb { transition: none; } }

.lb-count {
  position: fixed; bottom: 62px; bottom: calc(62px + env(safe-area-inset-bottom));
  left: 50%; transform: translateX(-50%); z-index: 62;
  font-size: 13px; font-variant-numeric: tabular-nums;
  color: #fff; background: rgba(0,0,0,.4); border-radius: 999px; padding: 4px 12px;
}
.g-item {
  flex: none; width: 78%; max-width: 340px; aspect-ratio: 4 / 3;
  border-radius: var(--r-card); background: var(--soft);
  overflow: hidden; scroll-snap-align: center; position: relative;
}
.g-item img, .g-item video { width: 100%; height: 100%; object-fit: cover; }
.g-item .play { position: absolute; inset: 0; display: grid; place-items: center; }
.g-item .play::before {
  content: ''; width: 46px; height: 46px; border-radius: 50%;
  background: var(--surface); box-shadow: var(--sh);
}
.g-item .play span {
  position: absolute; width: 0; height: 0;
  border-left: 14px solid var(--accent);
  border-top: 9px solid transparent; border-bottom: 9px solid transparent;
  margin-left: 4px;
}

/* ═══ БЛОК АВТОРА / АВАТАР ═════════════════════════════════════════════ */
/* border-top — визуальный разделитель; вертикальный отступ даёт gap .d-wrap,
   padding-top оставляем небольшим, чтобы линия не липла к аватару */
.donor { display: flex; align-items: center; gap: 10px; padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.ava {
  width: 38px; height: 38px; flex: none; border-radius: 50%;
  background: var(--soft); color: var(--accent-deep);
  display: grid; place-items: center; font-weight: 700; font-size: 15px;
  overflow: hidden;
}
.ava img { width: 100%; height: 100%; object-fit: cover; }
/* Инициал и картинка — в одной ячейке грида, картинка сверху. Без этого буква
   уехала бы во вторую строку и раздула кружок. */
.ava > .ava-ini, .ava > img { grid-area: 1 / 1; }
.ava-ini { line-height: 1; user-select: none; }
.ava.big { width: 56px; height: 56px; font-size: 22px; }
.ava.loading .ph-spin { width: 20px; height: 20px; }
/* смена аватара в профиле */
.ava-edit { display: flex; align-items: center; gap: 14px; }
.ava-edit .ava.big { width: 64px; height: 64px; font-size: 24px; }
.ava-edit-side { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; min-width: 0; }
.ava-edit-side .btn-soft { padding: 0 14px; }
.d-name { font-size: 14.5px; font-weight: 600; }
.d-sub { font-size: var(--fs-sub); color: var(--mute); font-variant-numeric: tabular-nums; }

/* ═══ ФОРМЫ ════════════════════════════════════════════════════════════
   <label class="field"><span class="f-label">…</span>
     <textarea class="input">…</textarea>
     <span class="f-hint">подсказка <b class="counter">0/3000</b></span>
   </label>                                                             */
/* Внутри .d-wrap шаг задаёт gap контейнера — своего margin у поля нет */
.field { display: block; }
/* Подпись поля может нести иконку: в длинной форме она даёт зацепку для глаза,
   и поля перестают быть одинаковой лентой заголовков. Монохром, тот же --mute:
   иконка помогает найти поле, а не спорит с содержимым за внимание. */
.f-label {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-cap); font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--mute);
  margin-bottom: 6px;
}
.f-label svg {
  width: 14px; height: 14px; flex: none;
  stroke: currentColor; fill: none; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.input {
  width: 100%; font: inherit; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 12px 14px; box-shadow: var(--sh);
  transition: border-color .12s ease;
}
.input:focus { outline: none; border-color: var(--accent); }
textarea.input { min-height: 120px; resize: vertical; line-height: 1.5; }
.input.err { border-color: var(--danger); }
.f-hint { display: flex; justify-content: space-between; gap: var(--sp-2); font-size: 12px; color: var(--mute); margin-top: 5px; }
.f-hint .counter { font-weight: 600; font-variant-numeric: tabular-nums; }
.f-hint .counter.over { color: var(--danger); }
.f-err { font-size: 12.5px; color: var(--danger-deep); margin-top: 5px; }

/* прикрепление фото: сетка миниатюр + кнопка добавления */
.photos-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2); margin-top: var(--sp-2); }
/* Долгий тап по плитке — это перестановка, а не системное меню «сохранить
   изображение / открыть в новой вкладке». Меню тут бессмысленно (адрес
   подписан и живёт час) и крадёт жест: пока оно всплывает, палец уже ведёт.
   Гасим и вызов меню, и выделение, и перетаскивание картинки браузером. */
.ph-item {
  position: relative; aspect-ratio: 1; border-radius: var(--r-chip);
  overflow: hidden; background: var(--soft);
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
  -webkit-user-drag: none;
}
/* canvas — миниатюра, нарисованная из уже разобранного кадра: браузеру не
   нужно декодировать картинку ради плитки, а значит и очередь декодера не
   встаёт перед подготовкой следующего снимка. */
.ph-item img, .ph-item canvas {
  display: block; width: 100%; height: 100%; object-fit: cover;
  -webkit-user-drag: none; pointer-events: none;   /* жест ловит плитка, не картинка */
}

/* ── Экран «Порядок фотографий» ─────────────────────────────────────────
   Отдельный экран, а не режим поверх правки. Панель действий стоит В ПОТОКЕ
   под сеткой: прошлая версия фиксировала её над сеткой, и при десяти снимках
   она закрывала нижний ряд.

   touch-action: none — на плитках ЗАРАНЕЕ. Браузер читает это правило в
   момент начала касания; навесить его по ходу жеста нельзя, и на этом
   сломалась самая первая версия: плитка сдвигалась на пару пикселей и
   возвращалась, потому что вебвью считал жест прокруткой. */
.po-hint { font-size: 13px; line-height: 1.45; color: var(--mute); margin: 0 0 var(--sp-2); }
.po-grid { position: relative; }
.po-item {
  touch-action: none; cursor: grab;
  transition: transform var(--dur-2) var(--ease-out);
}
/* Подхваченная плитка вырывается из потока и следует за пальцем 1:1.
   position: absolute внутри сетки — иначе перестановка соседей сдвигала бы
   точку отсчёта, и кадр прыгал бы под пальцем. */
.po-item.po-drag {
  position: absolute; z-index: 3; cursor: grabbing;
  transition: none; pointer-events: none;
  transform: scale(1.06) rotate(-1.5deg);
  box-shadow: 0 14px 32px rgba(18, 23, 26, .32);
}
/* Номер кадра: без него непонятно, что порядок здесь вообще что-то значит. */
.po-num {
  position: absolute; left: 5px; top: 5px;
  min-width: 20px; height: 20px; padding: 0 5px;
  display: grid; place-items: center;
  border-radius: 10px; background: rgba(18, 23, 26, .72); color: #fff;
  font-size: 11.5px; font-weight: 700;
}
.po-item.po-first .po-num { background: var(--accent); }
.po-item.po-first::after {
  content: 'обложка';
  position: absolute; left: 5px; bottom: 5px;
  font-size: 10px; font-weight: 700; letter-spacing: .02em;
  padding: 2px 6px; border-radius: 5px;
  background: var(--accent); color: #fff;
}
/* В потоке под сеткой, а не поверх неё. */
.po-bar { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-3); }
.ph-order { margin-top: var(--sp-1); }
@media (prefers-reduced-motion: reduce) {
  .po-item { transition: none; }
  .po-item.po-drag { transform: none; }
}
.ph-del {
  position: absolute; top: 3px; right: 3px; width: 22px; height: 22px;
  border-radius: 50%; background: rgba(18,23,26,.72); color: #fff;
  display: grid; place-items: center; font-size: 14px; line-height: 1;
}
/* размер сжатого фото — прямо на миниатюре, чтобы было видно, сколько весит */
.ph-size {
  position: absolute; left: 3px; bottom: 3px;
  font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums;
  background: rgba(18,23,26,.72); color: #fff; border-radius: 5px; padding: 1px 5px;
}
.photos-total { font-size: 12px; color: var(--mute); font-variant-numeric: tabular-nums; }
.photos-total.over { color: var(--danger-deep); font-weight: 600; }
.ph-add {
  aspect-ratio: 1; border: 1.5px dashed var(--line); border-radius: var(--r-chip);
  display: grid; place-items: center; color: var(--mute); font-size: 22px;
  background: var(--surface);
}
.ph-add:active { border-color: var(--accent); color: var(--accent); }
/* плитка «готовим фото»: спиннер, пока идёт сжатие — сразу видно, что пошло дело */
.ph-item.pending { display: grid; place-items: center; }
/* Кадр не подготовился. Раньше вместо этого молча брали оригинал — и человек
   узнавал о беде уже на публикации, из ошибки про интернет. */
/* Кадр уже разобран и виден, но ещё летит на сервер. Показываем сам снимок
   приглушённым с тонкой полосой поверх: пустая крутилка вместо выбранной
   фотографии заставляла гадать, то ли грузится. */
.ph-item.adding canvas { opacity: .55; }
.ph-wait {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: var(--soft); overflow: hidden;
}
.ph-wait::before {
  content: ""; position: absolute; inset: 0; width: 40%;
  background: var(--accent); border-radius: 999px;
  animation: phWait 1.1s ease-in-out infinite;
}
/* Едем трансформом, а не отступом: margin-left пересчитывает раскладку каждый
   кадр, а полоса крутится бесконечно всё время загрузки. Проценты у translateX
   считаются от своей ширины (40% контейнера), отсюда -100% и 250%. */
@keyframes phWait { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
@media (prefers-reduced-motion: reduce) {
  .ph-wait::before { animation: none; width: 100%; opacity: .6; }
}

.ph-item.failed { background: var(--warn-soft); }
.ph-again {
  position: absolute; inset: 0; display: grid; place-items: center; gap: 4px;
  align-content: center; color: var(--warn-deep); font-size: 11px; font-weight: 600;
}
.ph-again svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; }
.ph-again:active { background: rgba(0, 0, 0, .05); }
.ph-spin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2.5px solid var(--line); border-top-color: var(--accent);
  animation: pubSpin .8s linear infinite;
}
.ph-spin.sm { width: 15px; height: 15px; border-width: 2px; }
/* прогресс обработки выбранных фото */
.phprog { margin-top: 10px; }
.phprog-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--mute); margin-bottom: 6px; }
.phprog-bar { height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.phprog-bar i { display: block; height: 100%; background: var(--accent); border-radius: 2px; transition: width .2s ease; }
@media (prefers-reduced-motion: reduce) { .ph-spin { animation: none; } }

/* ═══ ПАНЕЛИ ПРОФИЛЯ / СТАТИСТИКА ══════════════════════════════════════ */
.panel-cap {
  font-size: var(--fs-cap); letter-spacing: .12em; text-transform: uppercase;
  color: var(--mute); margin: 20px 0 var(--sp-2); font-weight: 600;
}
.p-card {
  display: flex; align-items: center; gap: var(--sp-3);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 14px; box-shadow: var(--sh);
}
.tk {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 14px; box-shadow: var(--sh);
  font-size: 13.5px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere;
}
/* ── Репутация и счётчики профиля ──────────────────────────────────────
   Было четыре одинаковых плитки, среди которых «12 / 0» — главный сигнал
   о человеке — читался как ещё один счётчик. Репутация вынесена наверх и
   названа словами, остальное ужато в чипы: нули не занимают экран. */
/* Венок с числом «спасибо». Прогресс-бар долей хороших оценок не прижился:
   читался как индикатор загрузки. Признание должно выглядеть признанием. */
.rep {
  margin-top: var(--sp-2);
  display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 12px 14px; box-shadow: var(--sh);
}
.rep-badge { position: relative; width: 64px; height: 64px; flex: none; display: grid; place-items: center; }
.rep-badge .rep-wreath { position: absolute; inset: 0; width: 100%; height: 100%; }
.rep-badge .num {
  position: relative; font-size: 21px; font-weight: 800; line-height: 1;
  letter-spacing: -.02em; color: var(--accent-deep); font-variant-numeric: tabular-nums;
}
.rep-badge .num.sm { font-size: 14px; }
.w-stem { stroke: var(--line); stroke-width: 2; fill: none; stroke-linecap: round; }
.w-leaf { fill: var(--soft); stroke: var(--accent); stroke-width: 1.7; }
/* пустой венок — приглушён: не награда, а место для будущей */
.rep.none .w-leaf { stroke: var(--line); }
.rep.none .num { color: var(--mute); }
.rep-body { min-width: 0; }
.rep-body b { display: block; font-size: 15px; margin-bottom: 3px; }
.rep-sub {
  font-size: 13px; color: var(--mute); line-height: 1.45;
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
}
.rep-sub svg {
  width: 13px; height: 13px; flex: none; stroke: var(--mute); fill: none;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
/* Чипы у имени: лист = спасибо (акцент), вымпел = замечание (нейтрально).
   Те же знаки, что в профиле, — язык репутации один на всё приложение. */
.rep-chip {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: 2px;
  margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  background: var(--soft); color: var(--accent-deep);
  font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.rep-chip.bad {
  background: transparent; border: 1px solid var(--line);
  color: var(--mute); font-weight: 600; margin-left: 6px;
}
.rep-chip svg {
  width: 12px; height: 12px; flex: none; stroke: var(--accent); fill: none;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}
.rep-chip.bad svg { stroke: var(--mute); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-stat {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--soft); border-radius: 999px; padding: 8px 14px;
  font-size: 13px; color: var(--mute);
}
.chip-stat b {
  font-size: 15px; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.chip-stat svg {
  width: 15px; height: 15px; flex: none; color: var(--accent);
  stroke: currentColor; fill: none; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.tk-quiet { color: var(--mute); font-size: 13px; }
/* Архив «Моих объявлений»: история отодвинута вниз и приглушена — читается,
   но не спорит за внимание с живыми объявлениями. */
.arch-cap { margin-top: 26px; }
.mo-arch { box-shadow: none; background: transparent; }
.mo-arch .mo-text, .mo-arch .mo-geo { color: var(--mute); }


/* ═══ ТАББАР ═══════════════════════════════════════════════════════════ */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
  background: var(--surface); border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
/* Матовое стекло: контент проступает размытым под баром (премиум-глубина).
   Только там, где backdrop-filter поддержан — иначе остаётся плотный surface. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .tabbar {
    background: color-mix(in srgb, var(--surface) 78%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(180%);
            backdrop-filter: blur(18px) saturate(180%);
  }
}
.tabbar-in { max-width: 480px; margin-inline: auto; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
.tab {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 9px 0 7px; font-size: 11px; color: var(--mute); font-weight: 500;
  transition: color var(--dur-2) var(--ease-out), transform var(--dur-1) var(--ease-spring);
  -webkit-tap-highlight-color: transparent;
}
.tab svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.8;
  transition: transform var(--dur-2) var(--ease-spring); }
.tab.on { color: var(--accent); font-weight: 600; }
.tab.on svg { transform: translateY(-1px) scale(1.06); }   /* активная иконка чуть «всплывает» */
.tab:active { transform: scale(.92); }

/* Экран «Сказать»: сначала объясняем, зачем писать, потом просим написать. */
.say-intro { text-align: center; padding: 4px 0 2px; }
.say-h { font-size: 18px; font-weight: 600; margin: 6px 0 6px; color: var(--ink); }
.say-p { font-size: 13.5px; line-height: 1.5; color: var(--mute); margin: 0; }

/* Подпись мастерской под экраном «Сказать». Задача — не бросаться в глаза, но
   и не притворяться, что её нет: отделена волоском, набрана серым, и только
   имя чуть плотнее. Знак оживает под нажатием — на этом всё внимание к себе
   и заканчивается. */
.credit { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.credit-in {
  display: grid; justify-items: center; gap: 5px; width: 100%;
  padding: 6px 0 2px; color: var(--mute); text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-2) var(--ease-out);
}
.credit-mark {
  width: 40px; height: 24px; fill: none; stroke: currentColor;
  stroke-width: 1.3; stroke-linecap: round; opacity: .5;
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-spring);
}
.credit-mark circle { fill: var(--bg); }
.credit-line { font-size: 12px; letter-spacing: .01em; }
/* Ссылка — само имя мастерской: подчёркнуто и в акценте, как всякая ссылка.
   Что подпись нажимается, раньше узнать было неоткуда — серый текст среди
   других серых подписей. */
.credit-name {
  font-weight: 600; color: var(--accent-deep);
  border-bottom: 1px solid currentColor; padding-bottom: 1px;
}
.credit-sub { font-size: 11px; opacity: .75; }
/* Стрелка наружу: уходим из приложения, а не жмём что-то на этом экране. */
.credit-go {
  width: 12px; height: 12px; margin-left: 5px; vertical-align: -1px;
  fill: none; stroke: var(--accent-deep); stroke-width: 2.2; opacity: .85;
  /* Два коротких кивка при появлении экрана — и тишина: подпись зовёт, но
     не мигает без конца под руками у человека, который пришёл писать письмо. */
  animation: creditNudge 2.4s var(--ease-out) .7s 2;
}
@keyframes creditNudge {
  0%, 14%, 100% { transform: none; }
  6% { transform: translate(2px, -2px); }
}
.credit-in:active { color: var(--accent-deep); }
.credit-in:active .credit-mark { opacity: 1; transform: scale(1.06); }
.credit-in:active .credit-go { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .credit-in, .credit-mark { transition: none; }
  .credit-in:active .credit-mark { transform: none; }
  .credit-go { animation: none; }
}

/* бейдж «Ваше» на карточке — своё объявление, откликаться нельзя */
.badge-own {
  flex: none; font-size: 11px; font-weight: 600; letter-spacing: .02em;
  color: var(--accent-deep); background: var(--soft);
  border-radius: var(--r-chip); padding: 2px 8px;
}
/* заметка вместо кнопки отклика на своём объявлении */
.own-note {
  display: flex; align-items: center; gap: 10px;
  background: var(--soft); border-radius: var(--r-card);
  padding: 14px; color: var(--accent-deep); font-size: 13.5px; line-height: 1.4;
}
.own-note svg { width: 22px; height: 22px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; }
.own-note.ok { background: var(--ok-soft); color: var(--ok-deep); }
.own-note.ok svg { stroke-width: 2.4; }

/* Действия владельца на странице своего объявления. Кнопки те же, что в
   «Кабинете» (.mo-act), но ряд свой: там он лежит внутри карточки с её
   полями, здесь — прямо в потоке экрана. */
.own-acts { display: flex; flex-wrap: wrap; gap: 8px; }

/* ═══ СОСТОЯНИЯ ════════════════════════════════════════════════════════ */
.state-box { text-align: center; padding: 48px 20px; color: var(--mute); }
.state-box .big { font-size: 15px; font-weight: 600; color: var(--ink); margin-bottom: var(--sp-1); }
.state-box .sub { font-size: 13px; }

/* Премиальный пустой экран: крупная минималистичная иллюстрация, заголовок,
   поддерживающий текст, одно действие. Не «пусто», а приглашение. */
.empty {
  min-height: 60vh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  padding: 40px 28px; gap: var(--sp-3);
}
.empty-art { width: 132px; height: 132px; margin-bottom: var(--sp-2); }
.empty h2 { font-size: 19px; font-weight: 700; letter-spacing: -.01em; }
.empty p { color: var(--mute); font-size: 14px; line-height: 1.5; max-width: 30ch; }
.empty .mainbtn { max-width: 300px; margin-top: var(--sp-2); }
.empty .empty-hint { font-size: 12.5px; color: var(--text-faint, var(--mute)); }

/* Экран входа: сплошная подложка поверх всего приложения (лента, профиль,
   таббар остаются под ним). Держит верх стека, пока бот не запущен.
   Отдельный класс от .gate (экран «нет Telegram»), чтобы каскады не смешались. */
.authgate {
  position: fixed; inset: 0; z-index: 55;
  background: var(--bg); overflow-y: auto;
  display: flex; flex-direction: column;
}
.authgate .empty { margin: auto; min-height: auto; }
.gate-spin { display: grid; place-items: center; min-height: 40vh; }
.gate-spin .spin {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--accent);
  animation: pubSpin .8s linear infinite;
}

/* ═══ НИЖНИЙ ЛИСТ (выбор: продолжить черновик / создать новый) ══════════ */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 58;
  background: rgba(0, 0, 0, .44);
  display: flex; align-items: flex-end; justify-content: center;
  opacity: 0; transition: opacity .2s ease;
}
.sheet-backdrop.on { opacity: 1; }
.sheet {
  width: min(440px, 100%);
  background: var(--surface);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, .22);
  padding-left: 20px; padding-right: 20px; padding-top: 10px;
  padding-bottom: 20px;
  padding-bottom: calc(20px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: var(--sp-2);
  transform: translateY(12px); transition: transform .2s ease;
}
.sheet-backdrop.on .sheet { transform: none; }
.sheet-grip {
  width: 40px; height: 4px; border-radius: 2px;
  background: var(--line); align-self: center; margin-bottom: 4px;
}
.sheet-title { font-size: 18px; font-weight: 700; letter-spacing: -.01em; }
.sheet-sub { color: var(--mute); font-size: 13.5px; line-height: 1.45; }
/* flex: none обязателен: шторка — колонка, и при всплывшей клавиатуре высоты
   не хватает — главная кнопка сплющивалась вместо того, чтобы держать размер. */
.sheet .mainbtn { margin-top: 4px; flex: none; }
.sheet-alt { align-self: stretch; text-align: center; }
/* Лист «нужен опыт». Раньше тут был тост: он гас раньше, чем человек успевал
   прочитать, а открытый следом профиль выглядел сбоем. Спрашиваем на месте —
   заказчик всё равно видит об исполнителе только имя и эти строки. */
.sheet-art { width: 84px; height: 84px; align-self: center; flex: none; }
.exp-sheet .sheet-title, .exp-sheet .sheet-sub { text-align: center; }
.exp-sheet .sheet-sub { max-width: 34ch; margin: 0 auto; }
/* С поднятой клавиатурой картинка съедает место, нужное полю */
@media (max-height: 620px) { .sheet-art { display: none; } }
.draft-sum {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--bg); padding: 4px 14px;
}
.draft-row {
  display: flex; gap: 12px; align-items: baseline;
  padding: 8px 0; border-bottom: 1px solid var(--line);
}
.draft-row:last-child { border-bottom: none; }
.draft-row span { color: var(--mute); font-size: 12.5px; flex: 0 0 82px; }
.draft-row b { font-weight: 600; font-size: 13.5px; min-width: 0; overflow-wrap: anywhere; }
@media (prefers-reduced-motion: reduce) {
  .sheet-backdrop, .sheet { transition: none; }
}

/* Картинки ленты/галереи мягко проявляются по факту загрузки (класс .loaded
   ставит fetchImg после decode) — вместо резкого «щелчка». */
.thumb img, .g-item img { opacity: 0; transition: opacity .25s ease; }
.thumb img.loaded, .g-item img.loaded { opacity: 1; }

/* Пока байты не пришли, картинки в кадре нет вовсе: <img> без src в части
   вебвью рисуется иконкой «битое медиа», и человек видит поломку там, где
   просто идёт загрузка. Прячем сам элемент и показываем, что кадр занят. */
img[data-media]:not(.loaded) { visibility: hidden; }
.img-loading { position: relative; }
.img-loading::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: var(--soft);
  background-image: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.35) 50%, transparent 100%);
  background-size: 200% 100%;
  animation: shimmer 1.3s linear infinite;
}
.img-loading::after {
  content: ''; position: absolute; z-index: 1;
  top: 50%; left: 50%; width: 20px; height: 20px; margin: -10px 0 0 -10px;
  border-radius: 50%; border: 2px solid var(--line); border-top-color: var(--accent);
  animation: pubSpin .8s linear infinite;
}
/* Аватар маленький — крутилка в нём выглядит мусором, хватит подложки. */
.ava.img-loading::after { display: none; }
@media (prefers-reduced-motion: reduce) {
  .img-loading::before, .img-loading::after { animation: none; }
}
@media (prefers-reduced-motion: reduce) { .thumb img, .g-item img { transition: none; } }

/* ═══ PULL-TO-REFRESH ══════════════════════════════════════════════════
   Пилюли «Обновляем…» здесь больше нет: она вылезала при любом фоновом
   фетче и спорила с ростком за внимание. Фон обновляется молча (SWR).   */
/* Pull-to-refresh. Было дежурное колечко-спиннер; теперь по мере оттяжки
   прорисовывается росток — стебель, потом листья, — и это честная шкала:
   дорисовался значит отпускай. Крутящаяся анимация с листвой включается
   ТОЛЬКО если ответ идёт дольше секунды: на быстром обновлении мелькание
   раздражает сильнее, чем помогает. */
.ptr {
  position: fixed; left: 50%; top: -46px; transform: translateX(-50%);
  z-index: 55; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface); box-shadow: 0 2px 12px rgba(0, 0, 0, .16);
  transition: transform .2s ease;
}
.ptr-art {
  width: 22px; height: 22px;
  /* Не --line: на белой капсуле он почти сливался, и растущий росток был
     виден только когда дособирался и зеленел. Полростка — тоже картинка. */
  stroke: var(--mute); fill: none; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
  transition: stroke .18s linear, transform .22s var(--ease-out);
}
/* Кольцо со стрелкой вокруг капсулы. Без него жест читался как «что-то
   растёт», а не как «идёт обновление»: стрелка — единственный общепонятный
   знак этого действия. Замыкается вместе с оттяжкой, крутится в ожидании. */
.ptr-ring {
  position: absolute; left: 50%; top: 50%; width: 60px; height: 60px;
  margin: -30px 0 0 -30px; pointer-events: none;
}
.ptr-ring path {
  fill: none; stroke: var(--line); stroke-width: 1.3;
  stroke-linecap: round; stroke-linejoin: round;
  transition: stroke .18s linear;
}
.ptr-ring .tip { opacity: 0; transition: opacity .14s linear; }
.ptr.ready .ptr-ring path, .ptr.busy .ptr-ring path { stroke: var(--accent); }
/* Вращение ведёт JS одним счётчиком угла — и под пальцем, и в ожидании.
   CSS-анимация стартовала бы с нуля и давала рывок на отпускании. */
.ptr.done .ptr-ring { opacity: 0; transition: opacity .18s linear; }
.ptr.ready .ptr-art { stroke: var(--accent); transform: scale(1.06); }
/* Дыхание, пока ждём медленный ответ */
.ptr.busy .ptr-art { stroke: var(--accent); animation: ptBreathe 1.1s var(--ease-out) infinite; }
@keyframes ptBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.14); }
}
/* Готово: короткий хлопок вместо ещё одного оборота спиннера */
.ptr.done .ptr-art { stroke: var(--accent); animation: ptPop .34s var(--ease-out) 1; }
@keyframes ptPop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.24); }
  100% { transform: scale(1); }
}
.ptr-leaves { position: absolute; inset: -24px; pointer-events: none; }
.ptr-leaves i {
  position: absolute; left: 50%; top: 50%; width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px; background: var(--accent);
  border-radius: 70% 0 70% 0; opacity: 0;
}
/* В ожидании листва больше не кружит: на пятачке в 38 px крутящееся кольцо и
   бесконечный разлёт листьев спорили друг с другом. Листва осталась наградой
   за завершённое обновление. */
.ptr.done .ptr-leaves i { animation: ptLeaf .5s var(--ease-out) 1; }
@keyframes ptLeaf {
  0%   { opacity: 0; transform: translate(0, 0) rotate(0) scale(.5); }
  22%  { opacity: .9; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(210deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .ptr, .ptr-art, .ptr-ring path, .ptr-ring .tip { transition: none; }
  /* Листва и дыхание — чистое украшение: при reduce их просто нет */
  .ptr.busy .ptr-art, .ptr.done .ptr-art, .ptr-leaves i { animation: none; }
  .ptr-leaves { display: none; }
  /* Кольцо не крутим (вращение отключено в JS), но стрелку показываем: смысл
     «идёт обновление» — не украшение, без неё состояние ожидания молчит. */
  .ptr.busy .ptr-ring .tip { opacity: 1; }
}

/* ═══ КОЛОКОЛ УВЕДОМЛЕНИЙ (шапка) + БЕЙДЖ ══════════════════════════════
   Колокол в шапке каждого таба; бейдж — число непрочитанных, гаснет при
   открытии центра. Красная точка обведена фоном, чтобы читалась на любой шапке. */
.bell {
  position: relative; flex: none; width: 40px; height: 40px;
  display: grid; place-items: center; border: 0; background: transparent;
  color: var(--ink); cursor: pointer; border-radius: 50%;
}
.bell:active { background: var(--soft); }
.bell svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; }
.bell-badge {
  position: absolute; top: 3px; right: 3px; min-width: 16px; height: 16px;
  padding: 0 4px; border-radius: 8px; background: var(--danger);
  color: #fff; font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg);
}

/* ═══ ИНДИКАТОРЫ НЕПРОСМОТРЕННОГО «КАБИНЕТА» (бар · сегмент · карточка) ══ */
.tab-ic { position: relative; display: inline-flex; }
.tab-badge {
  position: absolute; top: -4px; right: -9px; min-width: 15px; height: 15px;
  padding: 0 3px; border-radius: 8px; background: var(--danger); color: #fff;
  font-size: 9.5px; font-weight: 700; line-height: 15px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg);
}
.seg-badge {
  display: inline-block; min-width: 16px; height: 16px; margin-left: 6px; padding: 0 4px;
  border-radius: 8px; background: var(--danger); color: #fff; vertical-align: middle;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.card-badge {
  flex: none; min-width: 18px; height: 18px; margin-left: 6px; padding: 0 5px;
  border-radius: 9px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
  font-variant-numeric: tabular-nums;
}

/* ═══ СТРОКА УВЕДОМЛЕНИЯ ═══════════════════════════════════════════════
   Аватар-иконка по типу + текст + время; непрочитанное подсвечено плашкой и
   точкой (паттерн monday.com/Instagram). Тап ведёт в объявление. */
.notif {
  display: flex; align-items: flex-start; gap: 12px; width: 100%;
  padding: 12px; border: 0; background: transparent; text-align: left;
  border-radius: var(--r-card); cursor: pointer;
}
/* ── Вести «из мастерской» ────────────────────────────────────────────────
   Второй поток колокола: то, что мы говорим людям сами. Карточка, а не строка
   списка — весть читают, а не пробегают глазами в поисках своего дела.

   На колоколе вести показываются точкой, а не числом: число там означает
   «есть дела», и новость о функции, попав в этот счёт, заставила бы значок
   врать. Точка заметна, но действия не требует. */
.bell.has-news::after {
  content: ''; position: absolute; top: 8px; right: 9px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 0 2px var(--surface);
}
/* ── «Мои дела»: напоминания ──────────────────────────────────────────────
   Не событие, а задача: живёт, пока условие верно, и исчезает само, когда
   сделано. Поэтому и верстается иначе, чем строка хроники.

   Что взято из разобранных референсов:
   · Air NZ — чип-контекст сверху и время справа, а кнопка появляется ТОЛЬКО
     там, где есть действие. Один макет, а плотность разная.
   · Turo — заголовок сформулирован как задача, под ним строка «почему сейчас».
   · Craft — срок показан ЦВЕТОМ подзаголовка, а не отдельной плашкой: плашка
     спорила бы с кнопкой за внимание.
   · Mint — то, что про тебя самого (профиль), отличается тоном иконки и не
     привязано к объявлению.

   Отсюда два вида: обычная карточка — дело по объявлению, .rem-self — про
   человека. Название объявления живёт в чипе, а не внутри предложения: в
   строке-предложении оно тонуло, и глазу не за что было зацепиться. */
.rem {
  background: var(--soft); border-radius: var(--r-card); padding: 14px;
  display: flex; flex-direction: column; gap: 6px;
}
.rem + .rem { margin-top: var(--sp-2); }
.rem-top { display: flex; align-items: center; gap: 8px; }
.rem-ic { flex: none; width: 18px; height: 18px; color: var(--accent-deep); }
.rem-ic svg { display: block; width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.75; }
.rem-chip {
  flex: 1 1 auto; min-width: 0; font-size: var(--fs-sub); color: var(--mute);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* В объявление ведёт ВСЯ карточка, кроме кнопок. Целиться в строку названия —
   лишняя работа для пальца: человек кладёт его куда попало, и это нормально.
   Отклик на нажатие даём всей карточкой, чтобы намерение подтвердилось. */
.rem-open { cursor: pointer; transition: transform var(--dur-1) var(--ease-spring); }
.rem-open:active { transform: scale(.99); }
@media (prefers-reduced-motion: reduce) {
  .rem-open { transition: none; }
  .rem-open:active { transform: none; }
}

/* Куда привела кнопка уведомления. Прокрутка сама по себе не отвечает на
   вопрос «а что здесь моё?» — особенно на длинном экране, где блоков много.
   Подсветка живёт полторы секунды и гаснет: это указание, а не состояние. */
.focus-flash { animation: focusFlash 1.8s var(--ease-out) 1; border-radius: var(--r-card); }
@keyframes focusFlash {
  0%, 70% { box-shadow: 0 0 0 3px var(--accent-line); }
  100%    { box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) { .focus-flash { animation: none; } }

/* Нумерованные шаги в шторке: инструкцию читают, переключаясь в другое
   приложение, поэтому шаги, а не абзац. */
.steps { margin: 0 0 var(--sp-3); padding-left: 22px; display: grid; gap: 6px; }
.steps li { font-size: 14px; line-height: 1.45; color: var(--ink); }
.rem-when { flex: none; font-size: var(--fs-sub); color: var(--mute); }
.rem-title { font-weight: 700; font-size: 14.5px; line-height: 1.3; }
.rem-body { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--mute); }
/* Цветом идёт ФАКТ, а не весь абзац. Сперва красили всё «почему сейчас»
   целиком — три строки цветного текста спорили с заголовком и читались
   тяжелее, чем нужно. В референсе (Craft) цветом только сама просрочка, а
   совет остаётся обычным; так и правильно: подсвечивать нужно то, что
   случилось, а не то, что делать. */
.rem-lead { color: var(--ink); }
.rem-lead.warn { color: var(--warn-deep); }
.rem-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: 2px; }
/* Вторая кнопка на карточке — светлая, а не --soft: подложка карточки уже
   --soft, и такая кнопка сливалась с ней в простой текст. Проверено глазом:
   «Снять с публикации» читалось как подпись, а не как действие. */
.rem-acts .mo-act.soft { background: var(--surface); color: var(--accent-deep); }
/* Опыт и @username — наверху и заметнее прочего. Без них человек упрётся в
   блокировку в самый неподходящий момент: уже выбрал заказ и жмёт «Откликнуться».

   Первая версия выделяла их рейкой слева — приёмом, который лепят на всё
   подряд, и выглядело это дёшево. В разобранных карточках такого нет ни у
   кого: Duolingo и Instagram выделяют подсказку профиля ПОДЛОЖКОЙ, а значок
   дают крупным и в круге. Мелкий штрих 18 px, висящий на пустой строке,
   выглядел случайным — потому и «кривым».

   Здесь так же: акцентная подложка вместо рейки, значок в круге 34 px, а
   вокруг него раз в несколько секунд расходится тихий круг. Цвет НЕ
   тревожный: это не беда, а незакрытая мелочь. */
.rem.rem-self {
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
}
.rem.rem-self .rem-title { color: var(--accent-deep); }
.rem.rem-self .rem-top { margin-bottom: 2px; }
.rem.rem-self .rem-ic {
  position: relative; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface); color: var(--accent);
}
.rem.rem-self .rem-ic svg { width: 19px; height: 19px; }
/* Символ @ рисуем БУКВОЙ, а не контуром: собственный путь читался чем угодно,
   только не «собакой». Знак, который у людей на клавиатуре, подделывать нечем. */
.rem-at { font-size: 19px; font-weight: 600; line-height: 1; }
/* Круг лежит ПОД значком: ::before — первый по порядку блок, а содержимое
   рисуется после него, поэтому значок не перекрывается. */
.rem.rem-self .rem-ic::before {
  content: ''; position: absolute; inset: -4px; border-radius: 50%;
  background: var(--accent); opacity: 0;
  animation: remPing 3.6s var(--ease-out) infinite;
}
/* Раз в 3,6 с — один расход и долгая пауза. Постоянная пульсация читалась бы
   как сбой; редкий круг замечают краем глаза и возвращаются к нему сами. */
@keyframes remPing {
  0%   { transform: scale(.55); opacity: .26; }
  55%  { transform: scale(1);   opacity: 0; }
  100% { transform: scale(1);   opacity: 0; }
}

.nseg { margin-bottom: var(--sp-1); }
.news-card {
  background: var(--soft); border-radius: var(--r-card); padding: 14px;
  display: flex; flex-direction: column; gap: 6px;
}
.news-card.unread { background: var(--accent-soft, var(--soft)); }
.news-top { display: flex; align-items: center; gap: 8px; }
.news-title { font-weight: 700; font-size: 14.5px; flex: 1 1 auto; min-width: 0; }
.news-body { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--ink); }
.news-when { font-size: 12px; color: var(--mute); }

/* Презентация: один экран за раз, крупно и без спешки. Точки внизу показывают,
   сколько ещё осталось, — иначе человек не знает, стоит ли начинать. */
.deck-slide { text-align: center; padding: 24px 8px 8px; }
/* Показ на весь экран важнее подписи: кадр объясняет быстрее текста. Высоту
   ограничиваем, чтобы заголовок и кнопка не уехали за край на низких экранах. */
.deck-media {
  display: block; width: 100%; max-height: 42vh; object-fit: cover;
  border-radius: var(--r-card); margin-bottom: var(--sp-2); background: var(--soft);
}
.deck-emoji { font-size: 44px; line-height: 1; margin-bottom: var(--sp-2); }
.deck-title { font-size: 19px; margin: 0 0 var(--sp-1); }
.deck-text { font-size: 14.5px; line-height: 1.5; color: var(--mute); margin: 0; }
.deck-dots { display: flex; gap: 6px; justify-content: center; padding: var(--sp-2) 0; }
.deck-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--line);
  transition: background var(--dur-1) var(--ease-out), width var(--dur-1) var(--ease-out);
}
.deck-dot.on { background: var(--accent); width: 18px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) { .deck-dot { transition: none; } }

.notif.unread { background: var(--soft); }
.notif:active { background: var(--line); }
.notif-ic {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center; background: var(--surface); color: var(--accent);
}
.notif.unread .notif-ic { background: var(--bg); }
.notif-ic svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
.notif.t-chosen .notif-ic { color: var(--ok-deep); }
.notif.t-rejected .notif-ic { color: var(--mute); }
.notif-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.notif-text { font-size: 14px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
.notif-when { font-size: 12px; color: var(--mute); }
.notif-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--danger); margin-top: 6px; }

/* ═══ ПЕРЕКЛЮЧАТЕЛЬ РАЗДЕЛОВ (.seg) ════════════════════════════════════
   Пилюля-контейнер, активный пункт — приподнятая плашка (паттерн Wise/N26).
   Один аксессор на несколько списков: переключение без ухода с экрана.

   КАК ПОЛЬЗОВАТЬСЯ. Разметку собирает segTabs() в index.html, руками её
   писать не нужно. От двух до трёх пунктов: четыре на узком экране уже не
   читаются, для такого нужен другой приём (прокручиваемая лента вкладок).

       segTabs(host, [{key, label, badge}], активный, function (key) {…})

   Позиция подложки считается из двух переменных, а не прибита к числу
   пунктов: --seg-n сколько всего, --seg-i какой активен. Прежняя версия
   знала только про два таба (width: calc(50% - 4.5px) и класс at-1), и
   третий пункт сломал бы её молча — подложка встала бы не на место.

   Подпись НЕ переносится: перенос ломает высоту пилюли и подложка
   разъезжается с текстом. Длинное имя ужимается многоточием — это честнее,
   чем прыгающая высота. Значит и подписи стоит держать короткими.

   Подложка едет по НАЖАТИЮ, не по приходу данных: переключатель отвечает за
   «я услышал», а не за «я загрузил». */
.seg {
  position: relative;
  display: flex; gap: 3px; margin: 2px 0 12px; padding: 3px;
  background: var(--soft); border-radius: var(--r-btn);
  --seg-n: 2; --seg-i: 0;
}
.seg-ind {
  position: absolute; top: 3px; bottom: 3px; left: 3px;
  width: calc((100% - 6px - (var(--seg-n) - 1) * 3px) / var(--seg-n));
  background: var(--surface); border-radius: calc(var(--r-btn) - 3px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .1); z-index: 0;
  transform: translateX(calc(var(--seg-i) * (100% + 3px)));
  transition: transform var(--dur-2) var(--ease-out);
}
.seg-btn {
  position: relative; z-index: 1;
  flex: 1 1 0; min-width: 0; border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: 14px; font-weight: 600; color: var(--mute);
  padding: 8px 10px; border-radius: calc(var(--r-btn) - 3px);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color var(--dur-2) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.seg-btn.on { color: var(--ink); }   /* фон даёт скользящая .seg-ind */
.seg-btn > span.lbl { overflow: hidden; text-overflow: ellipsis; }
/* Три пункта — текста меньше помещается, поэтому кегль на ступень ниже. */
.seg[style*="--seg-n: 3"] .seg-btn, .seg.seg-3 .seg-btn { font-size: 13px; padding: 8px 6px; }
@media (prefers-reduced-motion: reduce) {
  .seg-btn { transition: none; }
  .seg-ind { transition: none; }
}

/* ═══ КАРТОЧКА «МОИ ЗАКАЗЫ» ════════════════════════════════════════════ */
.mo-card {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); overflow: hidden;
}
.mo-info {
  display: block; width: 100%; text-align: left;
  padding: 12px 14px; background: transparent; color: inherit;
}
.mo-info:active { background: var(--soft); }
.mo-top { display: flex; align-items: center; gap: var(--sp-2); }
.mo-top .num { font-size: var(--fs-sub); color: var(--mute); }
.mo-top .oc-when { font-size: var(--fs-sub); color: var(--mute); }
.mo-geo { margin-top: 8px; font-size: 13px; color: var(--mute); }
.mo-text { margin-top: 4px; font-size: 14px; line-height: 1.4; color: var(--ink); }
.mo-actions {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 0 14px 12px;
}
.mo-act {
  flex: 1 1 auto; min-width: 44%;
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--ctl); padding: 0 12px;
  font-weight: 600; font-size: 13px; border-radius: var(--r-btn);
  border: 1px solid transparent;
}
.mo-act:active { transform: scale(.98); }
.mo-act[disabled] { opacity: .55; pointer-events: none; }
.mo-act.primary { background: var(--btn-bg); color: var(--btn-ink); }
.mo-act.soft { background: var(--soft); color: var(--accent-deep); }
.mo-act.danger { background: var(--danger-soft); color: var(--danger-deep); }

/* ═══ ОТКЛИКИ (карточка исполнителя у своего объявления) ═══════════════ */
.resp-card {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); padding: 14px;
}
.resp-card + .resp-card { margin-top: 10px; }
.resp-head { display: flex; align-items: center; gap: 10px; }
.resp-id { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.resp-id .d-name { font-weight: 600; }
/* Цена, названная исполнителем: под «по договорённости» это главное, по чему
   заказчик сравнивает отклики, — поэтому строка своя, а не хвост меты. */
.resp-offer { display: flex; align-items: center; gap: 7px; margin-top: 10px; }
.resp-offer svg { width: 17px; height: 17px; flex: none; fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.resp-offer b { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ink); }
.resp-offer span { font-size: 12px; color: var(--mute); }
/* Поля вилки «от — до» в шторке отклика */
.offer-row { display: flex; gap: 10px; }
.offer-row .field { flex: 1; min-width: 0; }

.resp-exp { margin-top: 10px; font-size: 13.5px; line-height: 1.45; color: var(--ink); overflow-wrap: anywhere; }
.resp-actions { display: flex; gap: 8px; margin-top: 12px; }
.resp-actions .mo-act { flex: 1; }
/* flex: 1 — только внутри РЯДА кнопок. В .d-action контейнер колоночный, и
   flex-basis: 0 применялся к высоте: кнопка «Написать заказчику» схлопывалась
   до высоты строки, игнорируя --ctl. Правило, написанное для строки, утекало
   в колонку — классика. */
.resp-contact { text-decoration: none; }
.resp-actions .resp-contact { flex: 1; }
.resp-empty { color: var(--mute); font-size: 13.5px; line-height: 1.5; }
/* Рейтинг рядом с именем — см. .rep-chip (лист/вымпел) в блоке репутации. */
/* Число откликов на своём объявлении — акцентом: сразу видно, где активность. */
.mo-resp { color: var(--accent-deep); font-weight: 700; }

/* ═══ НАСТРОЙКИ: строка + переключатель (iOS-стиль) ════════════════════ */
.set-row {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 0; border-bottom: 1px solid var(--line);
}
.set-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.set-title { font-size: 15px; font-weight: 600; color: var(--ink); }
.set-sub { font-size: 12.5px; color: var(--mute); line-height: 1.4; }
.switch { position: relative; flex: none; width: 46px; height: 28px; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch .track {
  position: absolute; inset: 0; border-radius: 999px; background: var(--line);
  transition: background .18s ease;
}
.switch .track::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
  transition: transform .18s ease;
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track::after { transform: translateX(18px); }
@media (prefers-reduced-motion: reduce) { .switch .track, .switch .track::after { transition: none; } }

/* ═══ СТОИМОСТЬ: число + переключатель «по договорённости» ════════════ */
/* Свободный текст в этом поле давал в ленте «25 000 ₽» рядом со «звоните»,
   и сравнить объявления было нечем. Осталось два состояния: сумма или
   договорная цена — вторая включается кнопкой, а не набирается руками. */
.price-box { position: relative; }
.price-box .input { padding-right: 36px; }
.price-cur {
  position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
  color: var(--mute); font-weight: 600; pointer-events: none;
  transition: opacity var(--dur-2) var(--ease-out);
}
.input.is-any { color: var(--mute); }
.input.is-any ~ .price-cur { opacity: 0; }
.price-any {
  margin-top: 8px; border: 1px solid var(--line); background: transparent;
  color: var(--mute); border-radius: 999px; padding: 7px 14px;
  font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  transition: background var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.price-any:active { transform: scale(.97); }
.price-any.on { background: var(--soft); border-color: var(--accent); color: var(--accent-deep); }
@media (prefers-reduced-motion: reduce) {
  .price-any, .price-cur { transition: none; }
  .price-any:active { transform: none; }
}

/* ═══ КОМИССИЯ: поле-кнопка + барабан (wheel picker) ══════════════════ */
.comm-field {
  text-align: left; cursor: pointer; display: flex; align-items: center;
  font-weight: 600; color: var(--ink);
}
.comm-field::after {
  content: ''; margin-left: auto; flex: none; width: 9px; height: 9px;
  border-right: 2px solid var(--mute); border-bottom: 2px solid var(--mute);
  transform: rotate(45deg) translate(-2px, -2px);
}
.comm-sheet { text-align: center; }
.comm-sheet .sheet-sub { font-size: 12.5px; color: var(--mute); margin: 0 0 10px; line-height: 1.4; }
.wheel {
  position: relative; height: 220px; margin: 4px 0 16px;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
}
.wheel-track {
  height: 100%; overflow-y: scroll; scroll-snap-type: y mandatory;
  padding: 88px 0;               /* (220-44)/2 — чтобы крайние значения центрировались */
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  /* Перспектива — в per-item transform (perspective(...) rotateX), а не свойством
     на этом скролл-контейнере: так WebView не «сплющивает» объём при прокрутке. */
}
.wheel-track::-webkit-scrollbar { display: none; }
.wheel-item {
  height: 44px; scroll-snap-align: center;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; font-weight: 700; color: var(--mute);
  font-variant-numeric: tabular-nums; transition: color .12s ease;
  backface-visibility: hidden; will-change: transform, opacity;
}
.wheel-item.on { color: var(--accent-deep); }
/* Без 3D (reduced-motion) — плоский акцент центрального значения масштабом. */
@media (prefers-reduced-motion: reduce) { .wheel-item.on { transform: scale(1.1); } }
.wheel-unit { font-size: 14px; font-weight: 600; margin-left: 2px; opacity: .7; }
.wheel-band {
  position: absolute; left: 6px; right: 6px; top: 50%; height: 44px;
  transform: translateY(-50%); border-radius: 10px; z-index: -1;
  background: var(--soft); border-top: 2px solid var(--line); border-bottom: 2px solid var(--line);
}
@media (prefers-reduced-motion: reduce) { .wheel-item { transition: none; } }

/* ═══ ПОЛЕ «КОГДА» ═════════════════════════════════════════════════════
   Нативный input с типом date выглядит по-разному в каждом клиенте, на части
   Android-вебвью открывается криво, а период им вообще не задать. Поэтому поле
   — кнопка (как у комиссии), а выбор идёт в своей шторке: календарь на день или
   период, быстрые варианты и свободный текст. Хранится по-прежнему строка. */
/* Стрелка та же, что у комиссии, — оба поля-кнопки должны читаться одинаково. */
.date-field {
  text-align: left; cursor: pointer; display: flex; align-items: center;
  font-weight: 600; color: var(--ink); gap: var(--sp-2);
}
.date-field::after {
  content: ''; margin-left: auto; flex: none; width: 9px; height: 9px;
  border-right: 2px solid var(--mute); border-bottom: 2px solid var(--mute);
  transform: rotate(45deg) translate(-2px, -2px);
}
/* Модификатор именно свой: класс .empty занят экранами пустых состояний
   (min-height 60vh, колонка по центру) — кнопка от него раздувалась на пол-экрана. */
.date-field.is-placeholder { color: var(--mute); font-weight: 400; }

/* Быстрые варианты — самый частый ответ в один тап, без календаря вообще. */
.dq { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 6px; }
.dq-chip {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  border-radius: var(--r-chip); padding: 7px 11px; font: inherit; font-size: 13px;
  font-weight: 600; cursor: pointer;
  transition: transform var(--dur-1) var(--ease-spring), background var(--dur-2) var(--ease-out);
}
.dq-chip:active { transform: scale(.96); }
.dq-chip.on { background: var(--soft); border-color: var(--accent); color: var(--accent-deep); }

.cal { user-select: none; }
.cal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.cal-title { font-size: 15px; font-weight: 700; text-transform: capitalize; }
.cal-nav {
  width: 34px; height: 34px; border: 0; background: var(--soft); color: var(--ink);
  border-radius: var(--r-chip); font: inherit; font-size: 17px; line-height: 1; cursor: pointer;
  transition: transform var(--dur-1) var(--ease-spring);
}
.cal-nav:active { transform: scale(.92); }
.cal-nav[disabled] { opacity: .35; cursor: default; }
.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal-dow { margin-top: 8px; font-size: 11.5px; color: var(--mute); text-align: center; }
.cal-grid { margin-top: 2px; row-gap: 2px; }
.cal-cell { position: relative; }
.cal-day {
  width: 100%; aspect-ratio: 1 / 1; min-height: 38px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ink);
  font: inherit; font-size: 14px; font-variant-numeric: tabular-nums;
  border-radius: var(--r-chip); cursor: pointer;
  transition: transform var(--dur-1) var(--ease-spring), background var(--dur-2) var(--ease-out);
}
.cal-day:active { transform: scale(.9); }
.cal-day[disabled] { color: var(--mute); opacity: .38; cursor: default; }
.cal-day.today { box-shadow: inset 0 0 0 1px var(--accent); font-weight: 700; }
/* Заливка периода лежит ПОД кнопкой: иначе скруглённые концы «съедали» полосу. */
.cal-cell.in-range::before {
  content: ''; position: absolute; inset: 0; background: var(--soft); z-index: 0;
}
.cal-cell.range-start::before { border-radius: var(--r-chip) 0 0 var(--r-chip); }
.cal-cell.range-end::before   { border-radius: 0 var(--r-chip) var(--r-chip) 0; }
.cal-day { position: relative; z-index: 1; }
.cal-day.sel { background: var(--btn-bg); color: var(--btn-ink); font-weight: 700; }
.cal-hint { font-size: 12px; color: var(--mute); margin: 6px 0 0; }
.date-sheet .field { margin-top: 10px; }
.date-sheet .sheet-sub { margin: 0 0 4px; }
/* Календарь делает шторку высокой: на коротком экране (SE, 640px) «Готово»
   уезжало за край. Прокручивается ТЕЛО шторки, а кнопка остаётся приколотой —
   главное действие всегда под пальцем. Отдельной строкой dvh: старый WebView
   отбросит только её и останется на vh. */
.date-sheet { max-height: 92vh; max-height: 92dvh; }
.ds-body {
  display: flex; flex-direction: column; gap: var(--sp-2);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  min-height: 0;                       /* иначе flex-элемент не даст себя сжать */
}
/* Пара действий в один ряд. Главное растягивается, второстепенное держит свою
   ширину, и оба выравниваются по высоте: у .mainbtn она --ctl-main (50px), у
   .btn-soft — --ctl, и рядом это читалось как ошибка вёрстки. Фиксированную
   высоту здесь снимаем — ряд задаёт её сам. */
.btn-row { display: flex; gap: var(--sp-2); align-items: stretch; flex: none; }
.btn-row > .mainbtn { flex: 1; margin-top: 0; }
.btn-row > .btn-soft, .btn-row > .btn-danger-soft { flex: 0 0 auto; height: auto; }
@media (prefers-reduced-motion: reduce) {
  .dq-chip, .cal-nav, .cal-day { transition: none; }
}

/* Блок завершения работы / отзыва (перенос флоу из бота в TWA). */
.complete-box { margin-top: 12px; padding: 12px; background: var(--soft); border-radius: var(--r-card); }
.cb-title { font-size: 13.5px; font-weight: 600; color: var(--ink); margin: 0 0 8px; }
.cb-note { width: 100%; min-height: 46px; margin-bottom: 8px; resize: vertical; }
.cb-fail { width: 100%; margin-top: 8px; color: var(--danger-deep); }

/* Уже откликнулся — карточка в ленте приглушена, статус меткой (item 11). */
.oc-responded { opacity: .55; }
.oc-resp-badge {
  font-size: 11px; font-weight: 700; color: var(--accent-deep);
  background: var(--soft); border-radius: var(--r-chip); padding: 2px 8px;
}
/* Аватар-картинка (автор объявления, откликнувшиеся) заполняет кружок (item 8). */
.ava img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }
.ava.sm { width: 38px; height: 38px; font-size: 15px; }
/* Время отклика в шапке карточки отклика (item 15). */
.resp-head .resp-when { margin-left: auto; align-self: flex-start; flex: none; font-size: 12px; color: var(--mute); }

.sk { border-radius: var(--r-card); border: 1px solid var(--line); background: var(--surface); padding: var(--sp-3) 14px; }
.sk i {
  display: block; height: 12px; border-radius: 6px;
  background: var(--skeleton); background-size: 200% 100%;
  animation: shimmer 1.2s linear infinite;
}
.sk i + i { margin-top: var(--sp-2); }
.sk .w60 { width: 60%; } .sk .w90 { width: 90%; } .sk .w40 { width: 40%; height: 16px; }
@keyframes shimmer { to { background-position: -200% 0; } }

/* ═══ СКЕЛЕТОНЫ ════════════════════════════════════════════════════════
   Правило: скелетон повторяет ФОРМУ конкретного контента — те же контейнеры
   (.oc / .p-card / .rep / .mo-card / .d-wrap), внутри shimmer-блоки .skl на
   месте текста, картинок, аватара. Размеры совпадают → смена скелетон→контент
   без сдвигов (reflow). Shimmer мягкий (1.6с), контент проявляется .fade-in.
   Не «универсальный прямоугольник», а каркас именно этого экрана. */
.skl {
  display: block; border-radius: 6px;
  background: var(--skeleton); background-size: 200% 100%;
  animation: shimmer 1.6s linear infinite;
}
.skl-line { height: 12px; }
.skl-line.lg { height: 20px; border-radius: 7px; }
.skl-chip { height: 22px; width: 92px; border-radius: var(--r-chip); }
.skl-thumb { width: 64px; height: 64px; flex: none; border-radius: var(--r-chip); }
.skl-ava { width: 56px; height: 56px; flex: none; border-radius: 50%; }
.skl-ava.sm { width: 38px; height: 38px; }
.skl-btn { height: var(--ctl); border-radius: var(--r-btn); }
.skl-btn.main { height: var(--ctl-main); }
.skl-col { display: flex; flex-direction: column; gap: 8px; min-width: 0; flex: 1; }
.fade-in { animation: fadeIn .22s ease both; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .skl { animation: none; }
  .fade-in { animation: none; }
}

/* чип-таймер в шапке: показывает, сколько осталось до снятия ограничения */
.ban-chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--ctl-sm); padding: 0 10px;
  font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--warn-deep); background: var(--warn-soft);
  border-radius: var(--r-chip);
}
.ban-chip::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--warn); }

/* Прогресс-тост публикации: висит сверху, показывает ход и эффектный финиш.
   Пользователь уходит из формы сразу, а тут видит, что происходит. */
/* Сложные тосты с процессом висят ВНИЗУ, над таббаром: сверху они закрывали
   шапку — заголовок, счётчик и колокол пропадали ровно тогда, когда человек
   ждёт результата и хочет видеть, где он находится. */
/* Прогресс — такой же житель общей колонки, как и сообщения, а не отдельная
   фиксированная плашка поверх них. order: 1 держит его в самом низу: он живёт
   долго, и сообщения приходят и уходят НАД ним, не сдвигая его с места.
   Скрытый — выключен из потока (display: none), иначе оставлял бы после себя
   пустой gap в колонке. */
.pub-toast {
  display: none; order: 1;
  position: relative; width: 100%;
  transform: translateY(16px);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  box-shadow: 0 12px 32px -12px rgba(0,0,0,.45);
  padding: 12px 14px; opacity: 0; pointer-events: none;
  transition: opacity .2s ease, transform .25s cubic-bezier(.2,.9,.28,1);
}
.pub-toast.shown { display: block; }
/* pointer-events возвращаем вместе с видимостью. Пока плашка скрыта, она не
   должна перехватывать нажатия по ленте под собой, — но показанная обязана их
   принимать. Без этой строки крестик и «Отменить» были нарисованы, а палец
   проходил сквозь них насквозь: нажатие не давало даже отклика. */
.pub-toast.on { opacity: 1; transform: none; pointer-events: auto; }
.pub-row { display: flex; align-items: center; gap: 10px; }
.pub-ic {
  width: 30px; height: 30px; flex: none; border-radius: 50%;
  display: grid; place-items: center; position: relative;
  background: var(--soft); color: var(--accent);
}
.pub-ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.4; }
/* крутящийся индикатор во время работы */
.pub-ic .spin {
  position: absolute; inset: 2px; border-radius: 50%;
  border: 2.5px solid var(--line); border-top-color: var(--accent);
  animation: pubSpin .8s linear infinite;
}
@keyframes pubSpin { to { transform: rotate(360deg); } }
/* Значок выбирает состояние, а не ручное переключение display в трёх местах
   кода. Из-за него «Загрузка отменена» и «Не опубликовалось» показывали пустой
   цветной кружок: галочку прятали, а взамен не показывали ничего. */
.pub-ic .pg { display: none; }
.pub-toast.done .pg-done,
.pub-toast.fail .pg-fail,
.pub-toast.stopped .pg-stop { display: block; }
.pub-toast.done .pub-ic .spin,
.pub-toast.fail .pub-ic .spin,
.pub-toast.stopped .pub-ic .spin { display: none; }
.pub-toast.done .pub-ic { background: var(--ok-soft); color: var(--ok-deep); }
.pub-toast.fail .pub-ic { background: var(--danger-soft); color: var(--danger-deep); }
/* Отменил сам — это не ошибка: тон нейтральный, без красного. */
.pub-toast.stopped .pub-ic { background: var(--soft); color: var(--mute); }
.pub-toast.stopped .pub-bar { visibility: hidden; }
.pub-body { min-width: 0; flex: 1; }
.pub-title { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.pub-sub { font-size: 12px; color: var(--mute); margin-top: 1px; }
.pub-bar { height: 4px; border-radius: 999px; background: var(--soft); overflow: hidden; margin-top: 10px; }
.pub-bar i { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--accent); transition: width .3s ease; }
.pub-toast.done .pub-bar i { background: var(--ok); width: 100% !important; }
/* «неопределённая» фаза — обработка на сервере */
.pub-toast.indet .pub-bar i { width: 40% !important; animation: pubIndet 1.1s ease-in-out infinite; }
/* Тот же приём, что у phWait: трансформ вместо отступа, проценты — от своей ширины. */
@keyframes pubIndet { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
.pub-close { flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; color: var(--mute); }
.pub-close svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 2.4; fill: none; }
/* Крестик виден всегда и всегда значит одно: убрать плашку с глаз. Прежде он
   прятался в середине работы, а в состоянии «можно прервать» ещё и менял смысл
   на отмену — человек жал «закрыть», а прерывал загрузку и видел новый текст
   вместо исчезновения. Отмена теперь на отдельной подписанной кнопке. */
.pub-cancel {
  flex: none; font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--danger-deep); background: none; padding: 6px 2px;
  white-space: nowrap; -webkit-tap-highlight-color: transparent;
}
.pub-cancel:active { opacity: .6; }
/* частицы финиша */
.pub-particles { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.pub-particles i {
  position: absolute; top: 50%; left: 21px; width: 7px; height: 7px; border-radius: 2px;
  opacity: 0; will-change: transform, opacity;
}
.pub-toast.done .pub-particles i { animation: pubBurst .7s ease-out forwards; }
@keyframes pubBurst {
  0% { opacity: 1; transform: translate(0,0) scale(1) rotate(0); }
  100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(.3) rotate(180deg); }
}
@media (prefers-reduced-motion: reduce) {
  .pub-ic .spin { animation: none; }
  .pub-toast.indet .pub-bar i { animation: none; width: 60% !important; }
  .pub-toast.done .pub-particles i { animation: none; opacity: 0; }
}

/* шаги в пустом экране — нумерованный список инструкции */
.empty-steps {
  text-align: left; margin: 4px 0 6px; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 10px; max-width: 30ch;
  counter-reset: step;
}
.empty-steps li {
  position: relative; padding-left: 34px; font-size: 13.5px; line-height: 1.4; color: var(--mute);
  counter-increment: step;
}
.empty-steps li b { color: var(--ink); font-weight: 600; }
.empty-steps li::before {
  content: counter(step); position: absolute; left: 0; top: -1px;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--soft); color: var(--accent-deep);
  font-size: 12px; font-weight: 700; display: grid; place-items: center;
}

/* toast — короткое подтверждение действия */
/* ── сообщения ───────────────────────────────────────────────────────────
   Стопка над нижним меню. Плашка светлая, как весь интерфейс: тип показывает
   значок, а не заливка — красный прямоугольник во всю ширину для «фото не
   загрузилось» слишком громкий. Полоса снизу тает и показывает остаток. */
/* Единый вьюпорт всех плашек: и сообщений, и прогресса отправки. Пока их было
   два, они перекрывали друг друга — прогресс с z-index 70 просто накрывал
   сообщение собой. Колонка с gap делает наложение невозможным по построению.
   Отступ снизу считает JS по реальной высоте нижнего меню: захардкоженные
   84 px врали при другом масштабе шрифта, вырезе экрана и любом изменении бара. */
.toasts {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: 96px;                               /* запас, пока JS не измерил */
  bottom: calc(var(--toast-bottom, 84px) + 12px);
  z-index: 70; width: min(440px, calc(100vw - 24px));
  display: flex; flex-direction: column; gap: 8px;
  pointer-events: none;
  /* Очередь не должна упираться в шапку: лишнее уедет за край, но вёрстку
     страницы это не сдвинет. */
  max-height: calc(100vh - 140px);
}
.toast {
  position: relative; overflow: hidden;
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-card);
  box-shadow: var(--sh);
  font-size: 13.5px; font-weight: 500; line-height: 1.35;
  padding: 11px 12px 12px 13px;
  opacity: 0; transform: translateY(10px) scale(.97);
  transition: opacity .2s var(--ease-out), transform .22s var(--ease-out);
  pointer-events: auto;
}
.toast.on { opacity: 1; transform: none; }
.t-ic { display: grid; place-items: center; width: 22px; height: 22px; }
.t-ic svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.t-msg { min-width: 0; }
.t-x {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; color: var(--mute);
}
.t-x svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }
.t-x:active { background: var(--soft); }
/* остаток времени: полоса тает слева направо, под пальцем замирает */
.t-life {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: currentColor; opacity: .3;
  transform-origin: left center;
  animation: toastLife linear forwards;
}
@keyframes toastLife { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* Цвет несёт значок и полоса; текст остаётся обычным — так плашка не кричит. */
.toast.t-ok .t-ic, .toast.t-ok .t-life { color: var(--ok-deep); }
.toast.t-err .t-ic, .toast.t-err .t-life { color: var(--danger-deep); }
.toast.t-warn .t-ic, .toast.t-warn .t-life { color: var(--warn-deep); }
.toast.t-info .t-ic, .toast.t-info .t-life { color: var(--accent-deep); }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity .12s linear; transform: none; }
}

/* заглушка вне Telegram */
.gate { text-align: center; padding: 80px 24px; }
.gate .mark { font-size: 40px; margin-bottom: var(--sp-3); }
.gate h2 { font-size: 17px; margin: 0 0 6px; }
.gate p { color: var(--mute); font-size: 13.5px; margin: 0; }

.preview-badge {
  position: fixed; top: var(--sp-2); right: var(--sp-2); z-index: 50;
  font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  background: var(--soft); color: var(--accent-deep); border: 1px solid var(--line);
  border-radius: 6px; padding: 3px 8px;
}

/* ═══ ПРЕМИУМ-МОУШН ════════════════════════════════════════════════════
   Единый слой тактильной обратной связи: пружинные нажатия, «поп» бейджей,
   подъём скелетонов. Всё — только transform/opacity (без сдвигов раскладки) и
   полностью снимается prefers-reduced-motion ниже. */

/* Нажатия: короткий проваливающийся отклик на всех интерактивных элементах. */
.btn-soft, .btn-danger-soft, .mo-act, .resp-contact, .oc-act, .switch,
.date-field, .comm-field, .lb-thumb {
  transition: transform var(--dur-1) var(--ease-spring),
              background var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.btn-soft:active, .btn-danger-soft:active, .mo-act:active,
.resp-contact:active, .oc-act:active { transform: scale(.96); }
/* Поля-кнопки во всю ширину: .96 на такой площади выглядит скачком, хватит .985 */
.comm-field:active, .date-field:active { transform: scale(.985); }
.mainbtn { transition: transform var(--dur-1) var(--ease-spring), opacity var(--dur-1) var(--ease-out); }
.mainbtn:active { transform: scale(.97); }
/* Карточки: премиум-кривая и «вдавливание» с уменьшением тени при нажатии. */
.oc { transition: transform var(--dur-2) var(--ease-out), border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out); }
.oc:active { transform: scale(.985); box-shadow: 0 1px 2px rgba(0,0,0,.05); }

/* Бейджи ПРОЯВЛЯЮТСЯ при появлении, а не «попают» (переход из display:none
   перезапускает анимацию). Раньше здесь был старт со scale(0) и кадр 60% со
   scale(1.18) поверх пружины — а пружина накладывается на каждый отрезок кадров
   отдельно, поэтому значок реально раздувался до 1.25, потом проваливался под
   единицу до 0.99. Два колебания на элементе размером с цифру — дребезг.
   Теперь один отрезок и ненулевой старт: ничто не появляется из ничего.
   Отскок тут ≈0.58% и на глаз не виден, и это НАМЕРЕННО: --ease-spring даёт
   ~5.7% от размаха, а размах всего .9→1. Значок мелькает в ленте постоянно,
   заметный отскок на таком частом элементе — шум. Не «чинить» расширением
   размаха до scale(.8). */
@keyframes badgePop {
  from { opacity: 0; transform: scale(.9); }
  to   { opacity: 1; transform: none; }
}
.bell-badge, .tab-badge, .seg-badge {
  animation: badgePop var(--dur-2) var(--ease-spring) both;
}
/* Счётчик на карточке — особый случай: карточка пересобирается целиком при
   каждом фоновом обновлении, поэтому анимация на базовом классе играла бы на
   любом тихом освежении. Модификатор ставит JS и только когда число выросло.
   Остальные три значка так не умеют ломаться: их узлы живут постоянно и
   показываются снятием .hidden, а повторное снятие анимацию не перезапускает. */
.card-badge.badge-pop {
  animation: badgePop var(--dur-2) var(--ease-spring) both;
}

/* Скелетоны всплывают со ступенчатой задержкой — «оживание» списка. */
@keyframes skRise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.sk { animation: skRise var(--dur-3) var(--ease-out) both; }
.sk:nth-child(2) { animation-delay: .05s; }
.sk:nth-child(3) { animation-delay: .1s; }
.sk:nth-child(4) { animation-delay: .15s; }
.sk:nth-child(5) { animation-delay: .2s; }
.sk:nth-child(n+6) { animation-delay: .25s; }

/* ── Смена трёх основных экранов ─────────────────────────────────────────
   Уходящий и приходящий едут в ПРОТИВОПОЛОЖНЫЕ стороны — это читается как
   замещение, а не как перерисовка на месте. На время перехода оба вынимаются
   из потока (иначе встали бы друг под другом), поэтому .app держит высоту.

   Три слоя двигаются с разной амплитудой: экран — полный ход, шапка — меньше,
   лента засечек — ещё меньше. Отставание превращает проезд картинки в сдвиг
   механической конструкции: ближние слои обгоняют дальние. */
.panes { position: relative; flex: 1; }
.panes.swapping { min-height: 70vh; }
.pane-anim { position: absolute; left: 0; right: 0; top: 0; }
/* Сегменты «Кабинета» переключаются тем же приёмом, но опора у них другая, и
   на этом был баг. У разделов обёртка тянется флексом (flex: 1) — снятие
   панелей с потока её не трогает. У сегментов обёртка живёт ПО СОДЕРЖИМОМУ,
   и, когда оба списка уходят из потока, опереться ей не на что.

   Раньше её подпирали выдуманным min-height: 56vh. В конце свопа подпорка
   отваливалась одновременно с возвратом панели в поток, и высота разом
   становилась высотой содержимого — замер дал прыжок 95 px ровно в момент
   уборки. Первый прыжок, в начале свопа, прикрыт разъездом панелей; второй
   не прикрыт ничем и читался как «весь таб моргнул один раз».

   Теперь высоту на время свопа ведёт JS числом: от высоты уходящей к высоте
   приходящей, за то же время. Догадка про 56vh больше не нужна, а обрезка
   держит уходящий список внутри обёртки, пока та едет к новой высоте. */
.seg-panes { position: relative; }
.seg-panes.swapping { overflow: hidden; }

@keyframes paneInRight  { from { transform: translateX(26%);  opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes paneInLeft   { from { transform: translateX(-26%); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes paneOutLeft  { from { transform: none; opacity: 1 } to { transform: translateX(-26%); opacity: 0 } }
@keyframes paneOutRight { from { transform: none; opacity: 1 } to { transform: translateX(26%);  opacity: 0 } }
.pane-in-right  { animation: paneInRight  var(--dur-3) var(--ease-out) both; }
.pane-in-left   { animation: paneInLeft   var(--dur-3) var(--ease-out) both; }
.pane-out-left  { animation: paneOutLeft  var(--dur-3) var(--ease-out) both; }
.pane-out-right { animation: paneOutRight var(--dur-3) var(--ease-out) both; }

/* Заголовок: старый быстро растворяется вверх, новый приходит снизу. Оба в
   одной ячейке грида — соседние элементы шапки от смены не дёргаются. */
.hdr-title { display: grid; }
.hdr-title > span { grid-area: 1 / 1; }
@keyframes titleIn  { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }
@keyframes titleOut { from { opacity: 1; transform: none } to { opacity: 0; transform: translateY(-10px) } }
.ht-in  { animation: titleIn  var(--dur-3) var(--ease-out) both; }
.ht-out { animation: titleOut .16s var(--ease-out) both; pointer-events: none; }

/* Замена действий в шапке — морфом одной оболочки, а не подменой двух кнопок.
   Ширина и подложка едут между формами, содержимое перекрещивается: глаз читает
   один орган управления, меняющий форму. Ширину «широкого» состояния меряем в
   рантайме (текст кнопки может смениться) и кладём в --hm-w. */
.hdr-morph {
  position: relative; height: 34px; flex: none; overflow: hidden;
  border-radius: var(--r-btn);
  transition: width var(--dur-3) var(--ease-out), background var(--dur-3) var(--ease-out);
}
/* Широкое состояние — целевое действие, поэтому акцентная заливка, как у любой
   главной кнопки. Узкое — вспомогательная иконка, фон не нужен. */
.hdr-morph.is-wide   { width: var(--hm-w, 96px); background: var(--btn-bg); }
.hdr-morph.is-narrow { width: 34px; background: transparent; }
.hdr-morph > * {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  /* nowrap обязателен: пока оболочка узкая, текст переносился на вторую строку
     и «выпрыгивал» снизу, а на широком состоянии возвращался в центр. */
  white-space: nowrap;
  transition: opacity .14s var(--ease-out);
}
.hdr-morph > .off { opacity: 0; pointer-events: none; }
/* Фон и радиус даёт оболочка — кнопка внутри только текст и цвет на нём. */
.hdr-morph .btn-small { background: transparent; border-radius: 0; padding: 0; height: 100%; }

/* Счётчик и чип бана появляются и уходят с началом и концом, а не мгновенно. */
@keyframes hdrActIn  { from { opacity: 0; transform: scale(.86) } to { opacity: 1; transform: none } }
@keyframes hdrActOut { from { opacity: 1; transform: none } to { opacity: 0; transform: scale(.86) } }
.ha-in  { animation: hdrActIn  var(--dur-2) var(--ease-spring) both; }
.ha-out { animation: hdrActOut .14s var(--ease-out) both; pointer-events: none; }

/* Лента засечек прокручивается на свой шаг под неподвижной шапкой: у каждого
   экрана своё место на линейке, поэтому переход читается как движение вдоль
   одной длинной шкалы, а не как перерисовка разделителя. */
.ruler-tape {
  transform: translateX(var(--tape-x, 0px));
  /* Медленнее контента (--dur-4 против --dur-3): лента ещё доезжает, когда экран
     уже встал. Это и даёт лёгкий параллакс — дальний слой всегда отстаёт. */
  transition: transform var(--dur-4) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .btn-soft, .btn-danger-soft, .mo-act, .resp-contact, .oc-act, .mainbtn, .oc,
  .tab, .tab svg { transition: none; }
  /* .card-badge.badge-pop перечислен отдельно не для красоты: медиазапрос не
     добавляет специфичности, и одного .card-badge (0,1,0) не хватает, чтобы
     погасить модификатор (0,2,0) — значок продолжал бы прыгать при reduce. */
  .rem.rem-self .rem-ic::before { animation: none; opacity: 0; }
  .bell-badge, .tab-badge, .seg-badge, .card-badge, .card-badge.badge-pop,
  .sk { animation: none; }
  .pane-in-right, .pane-in-left, .pane-out-left, .pane-out-right,
  .ht-in, .ht-out, .ha-in, .ha-out { animation: none; }
  .hdr-morph, .hdr-morph > * { transition: none; }
  .ruler-tape { transition: none; }
}
