/*
 * Своё поверх Pico — только то, чего в бесклассовой сборке нет.
 *
 * Правило простое: если Pico стилизует голую семантику (form, label, input,
 * button, table, nav) сам, здесь про это не пишем. Иначе обещание «замена
 * библиотеки стоит одного <link>» перестаёт быть правдой уже к третьему экрану.
 *
 * Файл подключается после pico.classless.min.css, поэтому при равной
 * специфичности выигрывает он — этим и переопределяются переменные темы.
 *
 * Состав: акцентная палитра поверх темы Pico, типографика заголовков, шапка
 * и навигация, состояния поллинга, пустое состояние и нижняя панель действий.
 * Каждый блок появился вместе со своим потребителем — правил «на будущее»
 * здесь нет.
 */

:root {
  /* Ширина колонки контента. Pico задаёт её через свои брейкпоинты, но у нас
     один столбец текста и форм, а не сетка: держим единое значение в одном
     месте, чтобы экраны не разъезжались по ширине. */
  --dpp-content-width: 34rem;
}

/* --- Акцент: brand кита MVP 0.3 (E31) -------------------------------------
 *
 * Значение задаёт дизайн-кит (Figma handoff 137:2, color/bg/brand); прежняя
 * «патина меди» была близкой по духу и уступила киту. Нейтрали остаются
 * за Pico, переопределяется ровно роль «основной цвет». Тёмного варианта нет:
 * тема закреплена светлой (см. блок токенов кита).
 *
 * Селекторы повторяют пиковские дословно, и это обязательно: свои переменные
 * темы Pico объявляет не в голом `:root`, а в `:root:not([data-theme=dark])`
 * и `:root:not([data-theme])` — по специфичности они перебили бы простой
 * `:root`, и акцент вернулся бы к синему (в светлой теме это и случилось
 * с первой версией файла).
 */
:root:not([data-theme="dark"]),
[data-theme="light"] {
  --pico-primary: #2e6b57;
  --pico-primary-background: #2e6b57;
  --pico-primary-border: #2e6b57;
  --pico-primary-hover: #245546;
  --pico-primary-hover-background: #245546;
  --pico-primary-hover-border: #245546;
  --pico-primary-underline: rgba(46, 107, 87, 0.5);
  --pico-primary-hover-underline: #245546;
  --pico-primary-focus: rgba(46, 107, 87, 0.375);
  --pico-primary-inverse: #fff;
}

/* --- Типографика ---------------------------------------------------------
 *
 * Кит просит Inter, но вебфонт не вендорится (E31, решение 4): лишние
 * килобайты там, где связь может быть плохой (подвал, котельная). Inter
 * стоит первым и берётся, где установлен; фолбэк системный.
 */
:root {
  --pico-font-family:
    Inter, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;
}

/* Паспорт — это серийные номера, даты и показания счётчиков. Цифры обязаны
   стоять в колонку, иначе списки и таблицы «пляшут» на каждой строке. */
body {
  font-variant-numeric: tabular-nums;
}

/* Заголовки чуть плотнее и легче дефолтных 700/normal: крупный кегль с
   обычным трекингом на 390 px рассыпается на отдельные слова. */
h1,
h2,
h3 {
  --pico-font-weight: 650;
  letter-spacing: -0.015em;
}

h1 {
  --pico-font-size: var(--dpp-font-heading);
  --pico-line-height: var(--dpp-line-heading);
}

@media (min-width: 768px) {
  h1 {
    --pico-font-size: 2.125rem;
    /* Кегль здесь вне шкалы кита (она про телефон), поэтому и высота строки
       возвращается к пиковской относительной: абсолютная из токена душила бы
       выросший заголовок. */
    --pico-line-height: 1.125;
  }
}

/* --- Колонка и шапка ------------------------------------------------------
 *
 * Одна колонка на всех ширинах. Pico на большом экране растягивает main до
 * 1450 px — для сплошного текста и форм это нечитаемая длина строки.
 */
body > header,
body > main {
  max-width: var(--dpp-content-width);
}

/* Шапка узкая: она подпись, а не палуба. Перенос — страховка на случай
   длинного адреса почты, чтобы полоса не выехала за экран. */
body > header {
  padding-top: calc(var(--pico-block-spacing-vertical) / 2);
  padding-bottom: 0;
}

body > header nav {
  flex-wrap: wrap;
}

/* Высоту нажатия в 48 px даёт сама ссылка, а не отступы li: отступами шапка
   выросла бы вдвое, а подпись — не палуба. */
body > header nav li {
  padding-block: 0;
}

/* margin-block: Pico вытягивает ссылку из строки отрицательным полем под свои
   же отступы li; отступы обнулены выше, а поле осталось — и ссылка вставала
   на 8 px выше коробки шапки, заезжая в main. */
body > header nav :is(a, button) {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
  margin-block: 0;
}

/* Метка приложения набрана как шильдик на оборудовании — то самое, что этот
   продукт и оцифровывает: моноширинный капс вразрядку, мелко и тихо. */
.mark {
  color: var(--pico-contrast);
  font-family: var(--pico-font-family-monospace);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

@media (min-width: 576px) {
  .mark {
    font-size: 0.75rem;
    letter-spacing: 0.14em;
  }
}

/* Адрес вошедшего — справка, а не действие: тихий цвет и мелкий кегль.
   Обрезка до 14 знаков не сообщала ничего, а целиком адрес доставался только
   подсказкой по наведению — ни с клавиатуры, ни пальцем. Поэтому узкий экран
   справку прячет, а широкий печатает адрес целиком. Адрес — неразрывная строка,
   поэтому длинному разрешён перенос где угодно: иначе он выдавит шапку вбок. */
.account {
  display: none;
  overflow-wrap: anywhere;
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

@media (min-width: 576px) {
  .account {
    display: inline;
  }
}

/* Кнопка-ссылка: действие в шапке (выход) не спорит по весу с действием на
   странице. Pico бесклассовый красит любую button как основную. */
.link-button {
  --pico-background-color: transparent;
  --pico-border-color: transparent;
  --pico-color: var(--pico-primary);
  --pico-underline: var(--pico-primary-underline);
  display: inline-flex;
  align-items: center;
  padding-block: 0.875rem;
  padding-inline: 0;
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
  text-decoration: underline 1px var(--pico-underline);
  text-underline-offset: 0.2em;
}

.link-button:is(:hover, :active, :focus) {
  --pico-background-color: transparent;
  --pico-border-color: transparent;
  --pico-color: var(--pico-primary-hover);
}

/* Сообщение об ошибке рядом с полем. Само поле Pico красит по aria-invalid,
   а подпись под ним оставляет обычным текстом — её и красим здесь.

   Отрицательный отступ сверху подтягивает подпись к своему полю: между ними
   не должно быть больше воздуха, чем до следующего поля, иначе сообщение
   читается как относящееся к нижнему. */
.error {
  margin-top: calc(var(--pico-spacing) * -0.5);
  color: var(--pico-del-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* --- Сообщение уровня формы ----------------------------------------------
 *
 * Отказ, не привязанный ни к какому полю: троттлинг и родня. Полосой слева,
 * а не заливкой — заливка на этом каркасе конкурировала бы по весу с кнопкой
 * действия, а сообщение обязано читаться и не кричать.
 */
.notice {
  margin-bottom: var(--pico-spacing);
  padding: calc(var(--pico-spacing) * 0.5) 0
    calc(var(--pico-spacing) * 0.5) var(--pico-spacing);
  border-left: 3px solid var(--pico-muted-border-color);
  font-size: var(--dpp-font-body);
  line-height: var(--dpp-line-body);
}

.notice-error {
  border-left-color: var(--pico-del-color);
}

/* --- Кнопка в ожидании ответа ---------------------------------------------
 *
 * htmx вешает класс htmx-request на элемент, который шлёт запрос (у нас —
 * на саму форму), на время запроса. Селекторы поэтому по вложенности: подписи
 * кнопки меняются местами, где бы она внутри формы ни лежала. Без видимой
 * реакции фоновая отправка выглядит зависанием, и человек жмёт второй раз.
 *
 * Кнопка на форме растянута во всю ширину колонки (Pico), поэтому подмена
 * подписи её не дёргает и удерживать её ширину незачем. Исключение — формы
 * поиска: у них класс ожидания уезжает на узел выдачи (hx-indicator), и
 * кнопка подписи не меняет вовсе — см. .asset-search-bar.
 *
 * У формы с несколькими submit-кнопками нажатую помечает data-busy (app.js):
 * без пометки «Секунду…» включалось бы на всех разом.
 */
.button-busy {
  display: none;
}

.htmx-request .button-label {
  display: none;
}

.htmx-request .button-busy {
  display: inline;
}

.htmx-request:has([data-busy]) button:not([data-busy]) .button-label {
  display: inline;
}

.htmx-request:has([data-busy]) button:not([data-busy]) .button-busy {
  display: none;
}

/* Пропуск навигации: первое, что встречает клавиатура и скринридер.
   Видим только в фокусе — иначе занимал бы место у всех остальных. */
.skip-link {
  position: absolute;
  top: 0;
  left: -9999px;
  z-index: 10;
  padding: 0.5rem 1rem;
  background: var(--pico-background-color);
}

.skip-link:focus {
  left: 0;
}

/* --- Прогресс фоновой работы ----------------------------------------------
 *
 * Элемент опрашивает сервер и подменяет сам себя, пока работа идёт (00 §7.1).
 * Восемь секунд ожидания нужно чем-то занять, и занимает их список стадий:
 * без него «идёт» и «зависло» выглядят одинаково.
 *
 * Карточка обрамлена рамкой, а не заливкой: она встаёт посреди страницы, и
 * заливка спорила бы по весу с кнопкой действия.
 */
.progress {
  margin-bottom: var(--pico-block-spacing-vertical);
  padding: var(--pico-spacing);
  border: 1px solid var(--pico-muted-border-color);
  border-radius: var(--pico-border-radius);
}

.progress-title {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  font-weight: 600;
}

/* Живой регион: одна строка «что идёт сейчас», спрятанная от глаз. Глазами
   то же самое видно по списку стадий, а скринридеру нужна именно короткая
   строка — регион объявляется целиком на каждое изменение внутри него, и
   заголовок со списком зачитывались бы заново полтора раза в секунду
   (см. views.progressStatusText).
 *
 * Прячется клипом, а не display:none или visibility:hidden: спрятанный этими
 * двумя способами регион не объявляется вовсе. */
.progress-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Итоговая карточка помечена цветом акцента: работа закончилась, и это
   единственное событие, ради которого человек смотрел на экран.
 *
 * Селектор смотрит на родителя не от хорошей жизни: рамку рисует внешняя
 * .progress, а классом «готово» помечено подменяемое содержимое внутри неё —
 * рамка пережить подмену обязана (см. views.Progress), поэтому сменить ей
 * класс с сервера нечем. :has поддержан всеми текущими браузерами; там, где
 * его нет, теряется только цвет рамки. */
.progress:has(.progress-done) {
  border-color: var(--pico-primary-border);
}

.stages {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Стадия: маркер слева, подпись справа. Ширина маркера фиксированная, иначе
   строки съезжают по горизонтали при каждой смене состояния. */
.stage {
  position: relative;
  margin-bottom: calc(var(--pico-spacing) * 0.25);
  padding-left: 1.5rem;
  font-size: var(--dpp-font-body);
  line-height: var(--dpp-line-body);
}

.stage::before {
  position: absolute;
  left: 0;
  width: 1.5rem;
  content: "·";
}

/* Ещё не начатая стадия приглушена: список показывает, что впереди, но
   внимание удерживает текущая строка. */
.stage-pending {
  color: var(--pico-muted-color);
}

.stage-active {
  font-weight: 600;
}

.stage-active::before {
  color: var(--pico-primary);
  content: "→";
}

.stage-done::before {
  color: var(--pico-primary);
  content: "✓";
}

/* Сорвавшаяся стадия — тем же красным, что и ошибка поля: у отказа в
   приложении один цвет, где бы он ни случился. */
.stage-failed {
  color: var(--pico-del-color);
}

.stage-failed::before {
  color: var(--pico-del-color);
  content: "×";
}

/* --- Пустое состояние -----------------------------------------------------
 *
 * Первое, что видит вошедший, пока объектов нет. Рамка пунктиром, а не
 * заливкой: место обозначено как пустое и ждущее заполнения — заливка читалась
 * бы как содержимое, которого нет.
 */
.empty {
  margin-bottom: var(--pico-block-spacing-vertical);
}

.empty > p:last-child {
  margin-bottom: 0;
}

/* Табличка без гравировки — тот же шильдик, что метка в шапке, но пустой.
   Единственное место экрана, где голос продукта звучит громко; остальное
   тихо, как и договаривались с каркасом. */
.plate {
  margin-bottom: var(--pico-block-spacing-vertical);
  padding: var(--pico-block-spacing-vertical) var(--pico-spacing);
  border: 1px dashed var(--pico-muted-border-color);
  border-radius: var(--pico-border-radius);
  color: var(--pico-muted-color);
  font-family: var(--pico-font-family-monospace);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-align: center;
  text-transform: uppercase;
}

@media (min-width: 576px) {
  .plate {
    font-size: 0.75rem;
    letter-spacing: 0.14em;
  }
}

/* --- Нижняя панель действий (00 §3) ---------------------------------------
 *
 * Главное действие экрана прибито к низу: телефон держат одной рукой, и
 * большой палец достаёт до нижнего края экрана, а не до верхнего.
 *
 * sticky, а не fixed, и это существенно. Fixed-панель висит поверх страницы
 * всегда: на телефоне её перекрывает всплывающая клавиатура, а содержимое под
 * ней приходится компенсировать отступом снизу, который потом забывают
 * поставить на новом экране. Sticky прилипает к низу окна, пока страница
 * длиннее экрана, и встаёт в поток, когда прокрутка кончилась, — компенсировать
 * нечего.
 *
 * Полоса под панелью — фон и граница: без них текст, проезжающий под кнопкой,
 * читается вперемешку с ней.
 */
/* Панель прибита к низу, то есть плывёт над содержимым, пока не доехала до
   своего места в потоке. Само по себе это и есть замысел (00 §3), но браузер
   об этом не знает: он считает элемент показанным, если тот попал в экран, —
   и переводя фокус по Tab, оставляет поле под панелью. Человек нажимает Tab,
   печатает и не видит куда: прогон 18.08.2026 поймал это на форме добавления
   места (docs/testing/history/2026-08-18-e21.md).

   scroll-padding объявляет нижнюю полосу непригодной для показа, и браузер
   доводит туда прокрутку сам — при переходе по Tab, по якорю и при вызове
   scrollIntoView. Ставится только там, где панель есть: :has держит правило
   ровно на тех страницах, где оно нужно, а не на всех подряд.

   Число — верхняя оценка высоты панели (подпись, поле выбора файла и две
   полосы кита по 52 px с зазорами и отступами): точную высоту CSS не
   измеряет, а запас вниз безопасен — лишняя прокрутка на пару строк заметно
   лучше, чем поле под панелью. Переменной, а не литералом: то же число нужно
   правилу про две полосы разом внизу файла, и разъехались бы они молча.
   Меняете состав панели или высоту .btn — правьте здесь и проверьте UI-13(б)
   и шаг с Tab в UI-11. */
:root {
  --dpp-action-bar-reserve: 16rem;
}

html:has(.action-bar) {
  scroll-padding-block-end: var(--dpp-action-bar-reserve);
}

.action-bar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  padding-top: calc(var(--pico-spacing) * 0.75);
  /* Safe area — полоса жеста на телефонах без кнопки «домой»: без неё кнопка
     оказывается ровно под ней. */
  padding-bottom: calc(var(--pico-spacing) * 0.75 + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--pico-muted-border-color);
  background: var(--pico-background-color);
}

/* Кнопка во всю ширину колонки; высоту, паддинг и выравнивание даёт .btn —
   в панели стоят кнопки кита. Своего display здесь нет: он весь у .btn,
   вместе с перебивкой инлайнового Pico у a[role=button]. */
.action-bar :is(a[role="button"], button) {
  width: 100%;
  margin-bottom: 0;
}

/* --- Галерея фотографий (E04) ---------------------------------------------
 *
 * Сетка, а не список: снимков десятки, и колонка в один столбец превращает
 * экран объекта в бесконечную ленту. Ширина колонки задана минимумом, а не
 * их числом, — на 390 px помещаются две, на планшете четыре, и ни одной
 * контрольной точки для этого не нужно.
 */
.media-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--pico-spacing);
  margin-bottom: var(--pico-block-spacing-vertical);
}

/* Карточка живёт в сетке, поэтому собственные отступы Pico ей мешают:
   снизу образуется двойной зазор. */
.media-card {
  margin-bottom: 0;
  padding: calc(var(--pico-spacing) * 0.5);
}

/* Превью занимает всю ширину карточки и не растягивается: снимки приходят
   и вертикальные, и горизонтальные. Место под кадр зарезервировано соотношением:
   габаритов у тега нет, а `loading="lazy"` доставляет кадр после отрисовки —
   без соотношения карточка растёт с нуля и двигает всё, что ниже. Обрезки
   здесь нет (`contain`): это снимок-доказательство, а не значок. */
.media-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  background: var(--pico-code-background-color);
  border-radius: var(--pico-border-radius);
}

.media-card :is(p, .notice) {
  margin-bottom: 0;
}

/* Кадр в карточке подтверждения. Высота ограничена, а не ширина: с ним
   сверяют поля формы, и снимок во весь экран уводил бы саму форму под сгиб
   на телефоне — то есть прятал бы то, ради чего кадр и показан. Мелкий шрифт
   шильдика в превью всё равно не читается, поэтому картинка обёрнута ссылкой
   на снимок целиком. Место держит обёртка: соотношение и предел высоты обязаны
   стоять на одном элементе, иначе они спорят. Картинке взамен дана высота
   в сто процентов — с `height: auto` она схлопнется до прихода кадра,
   и скачок над формой, с которой сверяют поля, останется. Обрезки нет
   (`contain`): по этому кадру читают шильдик. */
.confirm-photo {
  display: block;
  aspect-ratio: 4 / 3;
  max-height: 45vh;
  margin-bottom: var(--pico-spacing);
}

.confirm-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: var(--pico-border-radius);
  background: var(--pico-code-background-color);
}

/* Свёртка «Дополнить, если знаете» на подтверждении (E22). Заголовок её —
   строка перехода, а не кнопка кита: внутри форма продолжается теми же полями.
   Зона нажатия 48 px правилом, а не кеглем: у summary одна короткая строка,
   и пальцем в неё на телефоне не попадают. Отступ снизу равняет свёртку
   с полем над ней — Pico отступа у details внутри формы не даёт. */
.confirm-more {
  margin-bottom: var(--pico-spacing);
}

.confirm-more > summary {
  display: flex;
  align-items: center;
  min-height: 3rem;
  font-weight: 600;
}

.media-title {
  font-size: var(--dpp-font-body);
  line-height: var(--dpp-line-body);
}

/* Экран просмотра снимка (E36). Соотношения у рамки нет — в отличие от превью
   и кадра подтверждения: там картинка стоит рядом с другим содержимым и обязана
   держать чужую раскладку, а здесь она и есть экран. Фиксированные 4/3 зажимали
   бы портретный снимок — а шильдик снимают стоя — в серые поля по бокам.

   Место под кадр держит min-height: габаритов у тега нет, и без резерва
   страница прыгает в момент прихода кадра. Потолок высоты подобран на 390 × 844
   так, чтобы состояние и переход к соседям попадали на первый экран: кадр
   во всю высоту оставил бы человека без единого указания, что дальше.

   align-items — не умолчание: stretch тянул горизонтальный снимок до
   min-height, и 16:9 на 390 px выходил 358×253 вместо 358×201 (прогон 03.09.2026). */
.photo-view {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 30vh;
  margin: 0 0 var(--pico-spacing);
  background: var(--pico-code-background-color);
  border-radius: var(--pico-border-radius);
}

.photo-view img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 45vh;
  border-radius: var(--pico-border-radius);
}

/* Соседи разведены по краям: у крайнего снимка ссылка одна, и прижатая
   к своему краю она сама говорит, в какую сторону список кончился. */
.photo-steps {
  display: flex;
  justify-content: space-between;
  gap: var(--pico-spacing);
  margin-bottom: var(--pico-block-spacing-vertical);
}

/* Зона нажатия — 48 px: перелистывание это единственный способ дойти
   до соседнего снимка, а общее правило держит высоту только у ссылки-абзаца
   (main p > a:only-child), под которое соседи в <nav> не попадают
   (прогон 31.08.2026 — 24 px при пороге 48). */
.photo-steps a {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
}

/* Одинокое «Позже» прижимается вправо, одинокое «Раньше» остаётся слева:
   у крайнего снимка сторона ссылки и есть указание, куда список продолжается.
   Отбирает её rel, а не порядок в разметке: :only-child верен для обеих. */
.photo-steps a:only-child[rel="prev"] {
  margin-left: auto;
}

/* Приглашение лежит внутри галереи и прячется, как только в ней появляется
   первая карточка: приехать она может подменой htmx, и условие в разметке
   про такое появление ничего не знает (см. views.MediaScreen).

   Ширина — во всю сетку: в одной колонке текст сжимался бы в столбик. */
.media-empty {
  grid-column: 1 / -1;
  color: var(--pico-muted-color);
}

.media-gallery:has(.media-card) .media-empty {
  display: none;
}

/* Оба пути создания («Снять шильдик» и «Завести вручную») стоят в одной
   панели и занимают всю ширину. Зазор между ними задан явно: правило панели
   обнуляет margin-bottom у кнопок, и без него ссылка слипалась бы
   со «Снять шильдик» в одну полосу на 44 + 44 px. */
.action-bar a[role="button"] {
  margin-top: calc(var(--pico-spacing) * 0.5);
}

/* Поиск и фильтры оборудования (E21, E40): строка «поле + кнопка» снаружи,
 * три select'а свёрнуты в «Уточнить».
 *
 * Детей у сетки двое — бар и <details>, — и колонок ей не нужно:
 * раскладывает пары своя сетка внутри .asset-filter-fields. Свёрнутые
 * фильтры оставляют первому экрану список, ради которого сюда и пришли.
 *
 * Пары «подпись + поле» приходят обёрнутыми в div (см. assetSearchForm):
 * плоские элементы сетка разложила бы по ячейкам порознь, оторвав подписи
 * от полей.
 */
.asset-search {
  display: grid;
  gap: var(--dpp-space-2);
  margin-bottom: var(--pico-spacing);
}

/* Бар поиска: подпись над полем, кнопка в одной строке с полем и по его
   нижнему краю. Кнопка нужна пути без скрипта — с htmx запрос уходит
   по вводу, — поэтому она контурная и в ширину подписи, а не полосой. */
.asset-search-bar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--dpp-space-2);
  align-items: end;
}

.asset-search-bar > button {
  width: auto;
  height: 3.5rem; /* высота поля кита (см. input … { height: 3.5rem }) */
  margin-bottom: 0;
}

/* Узел выдачи на время запроса: класс ожидания htmx приходит сюда по
   hx-indicator, а не на форму, — иначе подпись кнопки менялась бы на каждую
   букву. Приглушение вместо спиннера: список остаётся читаемым. */
#asset-list-rows.htmx-request,
#search-results.htmx-request {
  opacity: 0.6;
  transition: opacity 150ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  #asset-list-rows.htmx-request,
  #search-results.htmx-request {
    transition: none;
  }
}

/* Три select'а внутри «Уточнить»: колонки считает браузер по минимальной
   ширине, как в галерее, — на телефоне столбик, на широком две-три. */
.asset-filter-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: calc(var(--pico-spacing) * 0.5);
  align-items: end;
}

/* «Уточнить» — компактная подпись в ширину слова с шевроном Pico рядом,
   а не полоса на всю ширину: под баром она читалась бы вторым полем.
   inline-flex, а не inline: у inline min-height не работает, и зона
   нажатия ниже 44 px. Раскрытый <details> у Pico отбивает summary целым
   pico-spacing — здесь достаточно шага кита. */
.asset-filters {
  margin-bottom: 0;
}

.asset-filters > summary {
  display: inline-flex;
  align-items: center;
  width: max-content;
  min-height: 2.75rem;
  font-weight: 600;
}

.asset-filters > summary::after {
  float: none;
  margin-inline-start: var(--dpp-space-1);
}

.asset-filters[open] > summary {
  margin-bottom: var(--dpp-space-2);
}

.asset-search :is(input, select) {
  margin-bottom: 0;
}

/* «Недавно открывали» над списком оборудования (E40, заход 40.3): тот же
   размер подзаголовка, что у групп поиска, — заголовок экрана здесь один. */
.asset-recent {
  margin-bottom: var(--dpp-space-4);
}

.asset-recent > h2 {
  margin-bottom: var(--dpp-space-2);
  font-size: 1rem;
}

/* Заголовок группы выдачи поиска (E40): четыре списка подряд без него
   читаются одним. Размер — как у подзаголовка блока, не как у экрана. */
.search-group {
  margin-bottom: var(--dpp-space-2);
  font-size: 1rem;
}

.search-group:not(:first-child) {
  margin-top: var(--dpp-space-4);
}

/* Строка-статус выдачи (E40): чип запроса, подпись порядка, слова
   действующего фильтра и сброс — одной строкой над списком, внутри узла
   строк. Сброс прижат к правому краю: это выход из сузившегося состояния,
   а не действие наравне с «Найти». На 390 px строка переносится. */
.search-status {
  display: flex;
  flex-wrap: wrap;
  gap: var(--dpp-space-1) var(--dpp-space-2);
  align-items: center;
  margin-bottom: var(--dpp-space-2);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

.search-status > a {
  margin-inline-start: auto;
}

.search-chip {
  padding: var(--dpp-space-1) var(--dpp-space-3);
  border-radius: var(--dpp-radius-lg);
  background: var(--dpp-bg-subtle);
  font-weight: 600;
}

/* Выходы из пустой выдачи поиска (E40) — столбик переходов без маркеров:
   маркеры списка читались бы перечислением того, чего не нашлось. Зону нажатия
   ссылкам даёт общее правило одиночной ссылки строки. */
.empty-outcome {
  padding-left: 0;
  margin-bottom: var(--pico-spacing);
  list-style: none;
}

/* Одиночная ссылка, занявшая строку целиком, — это переход, а не слово внутри
   фразы: сброс фильтра, «К объекту», «Пропустить всё», место в дереве. Зона
   нажатия у неё 48 px, и высоту даёт padding, а не кегль. Ссылка внутри
   предложения под правило не попадает — у той в абзаце есть соседи, и
   растянутая, она рвала бы строку текста. Мета строки списка исключена по той
   же причине, и вдобавок звенья у неё бывают пустыми: без исключения правило
   включалось бы через строку. */
main p > a:only-child,
main p:not(.list-meta) > small:only-child > a:only-child,
.empty-outcome li > a:only-child,
.list-card h3 a {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
}

/* --- Записки «важно знать» (E27) ------------------------------------------ */

/* Выбор фотографии — сетка превью, а не столбик строк: снимки различают
   глазами, и двадцать подписей «Фото от 18.08.2026» друг под другом
   не отвечают на «как выглядит нужный вентиль». Колонки считает браузер
   по минимальной ширине, как в галерее объекта: на 390 px помещаются две. */
.note-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: calc(var(--pico-spacing) * 0.5);
  align-items: start;
}

/* Легенда и сообщение группы идут во всю ширину сетки: в одной колонке
   они сжались бы в столбик по слову. */
.note-photos :is(legend, .notice) {
  grid-column: 1 / -1;
}

/* Вариант целиком — зона нажатия: палец попадает и в превью, и в подпись,
   а не только в кружок радио (UI-чеклист, 390 px). */
.note-photos label {
  display: block;
  min-height: 3rem;
  padding: calc(var(--pico-spacing) * 0.25);
  margin-bottom: 0;
}

/* Превью варианта — квадрат с обрезкой: ячейки сетки иначе разъезжаются
   по высоте от ориентации снимка, а место под них резервируется до загрузки. */
.note-photos img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--pico-border-radius);
}

/* Подпись под превью мельче основного текста: это дата загрузки, способ
   отличить два похожих снимка, а не содержание варианта. */
.note-photos span {
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* --- Теги оборудования (E08) ----------------------------------------------
 *
 * Высоту строки задаёт правило, а не содержимое: у варианта с чекбоксом нет
 * превью, которое держит зону нажатия у радиогруппы фотографий, и на телефоне
 * в него не попадают пальцем (прогон 23.08.2026 — 24 px при пороге 48).
 * Поле «свой тег» под ту же гребёнку не попадает: у него подпись над input'ом,
 * и растянутая до 48 px она оторвалась бы от своего поля.
 */
.asset-tags label:has(input[type="checkbox"]) {
  display: flex;
  align-items: center;
  min-height: 3rem;
  margin-bottom: 0;
}

/* --- Кит MVP 0.3 (E31) ----------------------------------------------------
 *
 * Токены дизайн-кита (Figma handoff 137:2), значения дословные. Тёмной
 * палитры у кита нет, и подбирать её здесь — расходиться с эталоном: тема
 * приложения закреплена светлой в <html data-theme="light">.
 */
:root {
  --dpp-brand: #2e6b57;
  --dpp-bg-success: #e3f1eb;
  --dpp-bg-info: #e5f1fa;
  --dpp-bg-warning: #fff2c8;
  --dpp-bg-danger: #fce8e8;
  --dpp-bg-subtle: #f1f3f5;
  --dpp-text-primary: #1f2429;
  --dpp-text-secondary: #6b737a;
  --dpp-text-on-brand: #fff;
  --dpp-text-danger: #a63b3b;
  --dpp-border: #e3e7ea;
  --dpp-border-brand: #2e6b57;
  --dpp-border-danger: #a63b3b;
  --dpp-radius-sm: 8px;
  --dpp-radius-md: 12px;
  --dpp-radius-lg: 16px;

  /* Шаг отступов кита. Числа не выводятся друг из друга (4·8·12·16·20·24·32 —
     не степени и не кратные одному шагу), поэтому перечислены, а не считаются
     от базы. */
  --dpp-space-1: 4px;
  --dpp-space-2: 8px;
  --dpp-space-3: 12px;
  --dpp-space-4: 16px;
  --dpp-space-5: 20px;
  --dpp-space-6: 24px;
  --dpp-space-7: 32px;

  /* Тип-шкала кита: кегль и высота строки ходят парой, порознь их берут
     ошибочно (UX-контракт задаёт именно пары — body 16/24, helper 14/20). */
  --dpp-font-caption: 0.875rem;
  --dpp-line-caption: 1.25rem;
  --dpp-font-body: 1rem;
  --dpp-line-body: 1.5rem;
  --dpp-font-title: 1.125rem;
  --dpp-line-title: 1.5rem;
  --dpp-font-heading: 1.75rem;
  --dpp-line-heading: 2rem;
}

/* Нейтрали текста берутся из токенов кита, а не остаются пиковскими: иначе
   палитра кита объявлена, но не применена. Селекторы повторяют пиковские по
   причине из шапки файла. */
:root:not([data-theme="dark"]),
[data-theme="light"] {
  --pico-color: var(--dpp-text-primary);
  --pico-muted-color: var(--dpp-text-secondary);
}

/* Надзаголовок экрана — мелкий капс кита («18 ФОТО СОХРАНЕНЫ»). */
.eyebrow {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  color: var(--dpp-brand);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Живой регион очереди: сводка в два-три слова, видимая только скринридеру —
   развёрнутый текст рядом печатает .status-block. role на самом блоке заставил
   бы перечитывать его заголовок и тело целиком на каждый файл. */
.status-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Статус загрузки и обработки (кит DPP/Status) --------------------------
 *
 * Копирайт состояния фиксирован контрактом кита: что происходит, что уже
 * сохранено, можно ли уйти, нужно ли действие. Кодов ошибок в нём не бывает.
 */
.status-block {
  margin-bottom: var(--pico-spacing);
  padding: calc(var(--pico-spacing) * 1.25);
  border-radius: var(--dpp-radius-lg);
  background: var(--dpp-bg-info);
}

.status-saved {
  background: var(--dpp-bg-success);
}

.status-offline {
  background: var(--dpp-bg-warning);
}

.status-error {
  background: var(--dpp-bg-danger);
}

.status-title {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  font-size: var(--dpp-font-title);
  font-weight: 600;
  line-height: var(--dpp-line-title);
}

.status-block p {
  margin-bottom: 0;
}

.status-title + p {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
}

.status-block .status-action {
  font-weight: 600;
  color: var(--dpp-brand);
}

.status-error .status-action {
  color: var(--dpp-text-danger);
}

/* --- Карточка памяти (кит DPP/Memory Card) ---------------------------------
 *
 * Карточка полезна до заполнения: пустые поля — не долг, Attention — только
 * для неопределённости, которая блокирует пользу (описание компонента).
 */
.memory-card {
  margin-bottom: var(--pico-spacing);
  padding: calc(var(--pico-spacing) * 1.25);
  border: 1px solid var(--dpp-border);
  border-radius: var(--dpp-radius-lg);
  /* База нейтральна, а success висит на .memory-ready: карточка без состояния
     иначе выглядела бы разобранной — ровно тем, от чего состояние названо
     словом. */
  background: transparent;
}

.memory-ready {
  background: var(--dpp-bg-success);
}

.memory-incomplete {
  background: var(--dpp-bg-subtle);
}

.memory-attention {
  background: var(--dpp-bg-warning);
}

.memory-card .eyebrow {
  margin-bottom: calc(var(--pico-spacing) * 0.75);
}

.memory-card h3 {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  font-size: var(--dpp-font-title);
  line-height: var(--dpp-line-title);
}

.memory-card p {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  color: var(--pico-muted-color);
}

.memory-card a {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
  font-weight: 600;
}

/* --- Хаб «Добавить» (E32) -------------------------------------------------
 *
 * Кнопками, а не карточками: вход в хаб — выбор действия, а подпись под
 * кнопкой объясняет, что за ней, не перетягивая вес заголовка на себя.
 * Не списком: у pico `ul li` задан list-style: square прямо на li, и
 * унаследованный с ul list-style: none его не перебивает.
 */
.add-hub {
  margin-bottom: var(--pico-spacing);
}

.add-hub-entry {
  margin-bottom: var(--dpp-space-4);
}

.add-hub-hint {
  margin: var(--dpp-space-2) 0 0;
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* --- Профиль (E32) --------------------------------------------------------
 *
 * Объекты списком, а не карточками экрана объектов: здесь важна роль, а не
 * адрес, и карточка перетянула бы вес на себя.
 */
.profile-properties {
  margin-bottom: var(--pico-spacing);
  padding-left: 0;
}

/* Маркер снимается на li: pico задаёт list-style самому пункту, и с <ul>
   правило не перебивается. */
.profile-properties li {
  margin-bottom: var(--dpp-space-2);
  list-style: none;
}

.profile-properties a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--dpp-space-3);
  /* Зона нажатия не меньше 48 px: экран открывают большим пальцем на 390 px. */
  min-height: 3.5rem;
  padding: var(--dpp-space-2) var(--dpp-space-4);
  border: 1px solid var(--dpp-border);
  border-radius: var(--dpp-radius-md);
  font-weight: 600;
  text-decoration: none;
}

.profile-role {
  color: var(--pico-muted-color);
  font-weight: 400;
}

.profile-exits {
  display: grid;
  gap: var(--dpp-space-3);
}

.profile-note {
  margin-bottom: 0;
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* --- Кнопка (кит DPP/Button) ----------------------------------------------
 *
 * Классом, а не голой семантикой: Pico красит все кнопки одинаково, а кит
 * различает главное действие и обычное. Роль назначает экран
 * (views.ButtonStyle), а не компонент, — иначе «один primary на экран»
 * нарушается сложением блоков, каждый из которых прав по отдельности.
 *
 * Вторичная — контур, а не другой оттенок заливки: роль и состояние не
 * различаются одним цветом (UX-контракт кита).
 */
.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 3.25rem;
  padding: var(--dpp-space-2) var(--dpp-space-5);
  border-radius: var(--dpp-radius-md);
  font-size: var(--dpp-font-body);
  font-weight: 600;
  line-height: var(--dpp-line-body);
}

/* Pico вешает display: inline-block на a[role=button] — селектор с атрибутом
   выше класса по специфичности, и flex у .btn до ссылки-кнопки не доходит:
   подпись встаёт в левый верхний угол вместо центра. */
a[role="button"].btn {
  display: flex;
}

.btn-primary {
  border-color: var(--dpp-brand);
  background: var(--dpp-brand);
  color: var(--dpp-text-on-brand);
}

.btn-secondary {
  border-color: var(--dpp-border-brand);
  background: transparent;
  color: var(--dpp-brand);
}

/* Нажатие — подсветкой, а не сдвигом: палец закрывает кнопку целиком. */
.btn:active {
  opacity: 0.82;
}

/* Выключенная — заливка subtle и приглушённый текст, а не одна прозрачность
   Pico: hx-disabled-elt держит disabled всё время запроса, и .5 читается как
   «кнопка сломалась». */
.btn[disabled],
.btn[aria-disabled="true"] {
  border-color: var(--dpp-bg-subtle);
  background: var(--dpp-bg-subtle);
  color: var(--dpp-text-secondary);
  opacity: 0.68;
}

/* Главное действие в потоке страницы: во всю ширину колонки. Pico ужимает
   a[role=button] по содержимому, и кнопка «Задать вопрос» вставала в 146 px
   при 350 у эталона (прогон 25.08.2026). Высота и заливка — у .btn: второго
   описания кнопки кита в файле не бывает. */
.primary-action {
  width: 100%;
  margin-bottom: var(--pico-spacing);
}

/* --- Поле (кит DPP/Field) --------------------------------------------------
 *
 * Голой семантикой, а не классом: поле рисуется одним компонентом
 * (views.Field), и второй способ разметить его разъехался бы с первым.
 * Переопределяется геометрия кита и состояния; цвет фона, стрелка select'а
 * и подсветка aria-invalid остаются за Pico.
 */
input:not([type="checkbox"], [type="radio"], [type="file"]),
select,
textarea {
  border: 1px solid var(--dpp-border);
  border-radius: var(--dpp-radius-md);
  font-size: var(--dpp-font-body);
  line-height: var(--dpp-line-body);
}

/* Высота задана числом, а не собрана из паддингов: у select'а Pico её не
   считает вовсе, и стоящие рядом поля разъезжались на пару пикселей. */
input:not([type="checkbox"], [type="radio"], [type="file"]),
select {
  height: 3.5rem;
}

input:not([type="checkbox"], [type="radio"], [type="file"]),
textarea {
  padding-inline: var(--dpp-space-4);
}

/* Справа у select'а паддинг пиковский: под ним стоит стрелка. Селектор
   повторяет пиковский дословно: голый select проигрывает ему специфичностью,
   и токен переставал бы что-либо значить молча. */
select:not([multiple], [size]) {
  padding-inline-start: var(--dpp-space-4);
}

textarea {
  min-height: 3.5rem;
  padding-block: var(--dpp-space-4);
}

/* Фокус и отказ — рамка в два пикселя (кит). Второй пиксель рисуется
   внутренней тенью, а не border-width: выросшая рамка сдвигала бы текст
   в поле на каждый заход курсора. */
:is(input:not([type="checkbox"], [type="radio"], [type="file"]), select, textarea):focus {
  border-color: var(--dpp-border-brand);
  box-shadow: inset 0 0 0 1px var(--dpp-border-brand);
}

/* Отказ перебивает фокус: поле с ошибкой остаётся красным и под курсором,
   иначе состояние пропадает ровно там, где его правят. Фокусная пара
   выписана отдельной строкой не для красоты — без неё правило проигрывает
   правилу выше по специфичности, и порядок в файле ничего не решает. */
:is(input, select, textarea)[aria-invalid="true"],
:is(input, select, textarea)[aria-invalid="true"]:focus {
  border-color: var(--dpp-border-danger);
  box-shadow: inset 0 0 0 1px var(--dpp-border-danger);
}

/* Выключенное — заливкой subtle и гашением до .62: одна пиковская
   прозрачность .5 читается как сломанное поле, а не как «сюда пока нельзя».
   Селекторы повторяют пиковские дословно — переменной темы тут не хватит:
   свою Pico объявляет в `:root:not([data-theme=dark])`, и голый `:root`
   её не перебивает (та же ловушка, что у акцента в шапке файла). */
input:not([type="submit"], [type="button"], [type="reset"])[disabled],
select[disabled],
textarea[disabled] {
  /* background-color, а не сокращение: сокращение сбрасывает background-image,
     а им Pico рисует стрелку select'а — выключенный превратился бы в серый
     прямоугольник без признака выпадающего списка. */
  background-color: var(--dpp-bg-subtle);
  opacity: 0.62;
}

/* Подпись поля — 16 semibold (кит); legend здесь же, потому что заголовок
   группы полей — та же подпись. Обёртки чекбоксов и радио исключены:
   там подпись — сам вариант выбора, а не заголовок поля. Селектор легенды
   двухчастный: свой вес Pico вешает на `fieldset legend`, и голый `legend`
   его не перебивает. */
fieldset legend,
label:not(:has(input, select, textarea)) {
  font-size: var(--dpp-font-body);
  font-weight: 600;
  line-height: var(--dpp-line-body);
}

/* Постоянная подсказка под полем (helper кита, 14/20). Отступ отрицательный
   по той же причине, что у .error: она принадлежит верхнему полю. */
.field-helper {
  margin-top: calc(var(--pico-spacing) * -0.5);
  color: var(--dpp-text-secondary);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* Сообщение проверки под подсказкой уже подтянуто ею — второй раз не тянем. */
.field-helper + .error {
  margin-top: 0;
}

/* Подсказка «уже можно спросить» на экране первого результата — жёлтая
   поверхность кита, чтобы не спорить с зелёным статусом сохранности. */
.hint-card {
  margin-bottom: var(--pico-spacing);
  padding: var(--pico-spacing);
  border-radius: var(--dpp-radius-lg);
  background: var(--dpp-bg-warning);
}

.hint-card p {
  margin-bottom: 0;
}

.hint-card .hint-title {
  margin-bottom: calc(var(--pico-spacing) * 0.25);
  font-weight: 600;
}

/* Очередь дел на экране результата: «Нужно решить · 3» с переходом. Поверхность
   нейтральна намеренно — обе группы равноправны, и цвет тревоги на «Можно
   улучшить» превратил бы необязательное в долг. */
.result-group {
  margin-bottom: var(--pico-spacing);
  padding: var(--pico-spacing);
  border: 1px solid var(--dpp-border);
  border-radius: var(--dpp-radius-lg);
}

.result-group p {
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  color: var(--pico-muted-color);
}

.result-group .result-group-title {
  margin-bottom: calc(var(--pico-spacing) * 0.25);
  font-size: var(--dpp-font-title);
  line-height: var(--dpp-line-title);
}

.result-group a {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
  font-weight: 600;
}

/* --- Нижняя навигация (кит DPP/Bottom Navigation) --------------------------
 *
 * Четыре постоянных раздела объекта; «Спросить» и «Добавить» из панели
 * не прячутся (контракт кита). Sticky по той же причине, что .action-bar:
 * fixed перекрывала бы клавиатура, а компенсирующий отступ забывают.
 */
.bottom-nav {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  gap: var(--dpp-space-1);
  /* Ширина явная, а не от растяжения: body с панелью — flex-колонка, а
     margin-inline: auto у flex-элемента растяжение отменяет, и панель ужималась
     до текста табов (прогон 25.08.2026). */
  width: 100%;
  max-width: var(--dpp-content-width);
  margin-inline: auto;
  padding: var(--dpp-space-2) var(--dpp-space-3);
  padding-bottom: calc(var(--dpp-space-2) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--dpp-border);
  background: var(--pico-background-color);
}

.bottom-nav a {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  justify-content: center;
  min-height: 3.5rem;
  border-radius: var(--dpp-radius-md);
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
  text-decoration: none;
}

.bottom-nav a[aria-current="page"] {
  background: var(--dpp-bg-success);
  color: var(--dpp-brand);
  font-weight: 600;
}

/* Высота панели навигации: таб 3.5rem плюс отступы, граница и safe area.
   Точную высоту CSS не измеряет, поэтому число объявлено один раз — им же
   считаются подъём .action-bar над панелью и нижние scroll-padding. */
:root {
  --dpp-bottom-nav-height: calc(
    3.5rem + var(--dpp-space-2) * 2 + 1px + env(safe-area-inset-bottom, 0px)
  );
}

html:has(.bottom-nav) {
  scroll-padding-block-end: calc(var(--dpp-bottom-nav-height) + 2rem);
}

/* Обе полосы на одной странице (страница объекта): запасы складываются.
   Специфичность у соседних html:has одинаковая — без этого правила спор
   решает порядок в файле, и панель действий остаётся с запасом навигации. */
html:has(.action-bar):has(.bottom-nav) {
  scroll-padding-block-end: calc(
    var(--dpp-action-bar-reserve) + var(--dpp-bottom-nav-height)
  );
}

/* Панель действий поднимается над навигацией: обе прибиты к низу с одним
   z-index, и более поздняя в DOM навигация иначе кроет главное действие
   экрана. Safe area учтена в высоте навигации, второй раз не нужна. */
body:has(.bottom-nav) .action-bar {
  bottom: var(--dpp-bottom-nav-height);
  padding-bottom: calc(var(--pico-spacing) * 0.75);
}

/* Панель прижата к низу окна и на короткой странице: sticky сам по себе держит
   её в потоке, и на экране в две карточки она вставала посреди пустоты вместо
   нижнего края (прогон 25.08.2026; у эталона это «Flexible space»). Колонку
   растягивает body, а margin-top: auto съедает остаток. Ширину шапке и main
   правило не трогает — она задана max-width, а не растяжением. */
body:has(.bottom-nav) {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

body:has(.bottom-nav) .bottom-nav {
  margin-top: auto;
}

/* --- Пачка фотографий (E31) ------------------------------------------------
 *
 * Сетка превью выбранной пачки; рисует её скрипт из object URL, без скрипта
 * блок пуст и скрыт — правило то же, что у .media-empty.
 */
.upload-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
  gap: calc(var(--pico-spacing) * 0.5);
  margin-bottom: var(--pico-spacing);
}

.upload-grid:empty {
  display: none;
}

.upload-grid img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--dpp-radius-md);
}

/* --- Ответ дома (E16) ------------------------------------------------------
 *
 * Ответ стоит над формой, а не под ней: на телефоне поле вопроса вместе
 * с клавиатурой занимает нижнюю половину экрана, и ответ под ним человек
 * не увидел бы вовсе. Предупреждение guardrail — первый блок ответа.
 */
.ask-answer {
  margin-bottom: calc(var(--pico-spacing) * 1.5);
}

/* Заливка ответа живёт на своём блоке: .ask-answer несёт и предупреждение
   guardrail, и честный отказ, и тон успеха покрасил бы их вместе с ответом. */
.ask-answer-text {
  margin-bottom: var(--pico-spacing);
  padding: var(--dpp-space-4);
  border-radius: var(--dpp-radius-lg);
  background: var(--dpp-bg-success);
}

.ask-answer-text p {
  margin-bottom: 0;
}

.ask-warning ul {
  margin-bottom: var(--pico-spacing);
}

/* Честный отказ зовёт наполнить память дома. Входы стоят колонкой: на 390 px
   две кнопки в ряд режут подписи. */
.ask-help {
  display: grid;
  gap: var(--dpp-space-3);
  margin-bottom: var(--pico-spacing);
  padding: var(--dpp-space-4);
  border-radius: var(--dpp-radius-lg);
  background: var(--dpp-bg-subtle);
}

.ask-help h2,
.ask-help ul {
  margin-bottom: 0;
}

/* Номер источника виден, поэтому ни маркер, ни отступ под него не снимаются:
   list-style: none на списке и display: flex на строке гасят номер порознь. */
.ask-sources {
  margin-bottom: 0;
}

.ask-sources li {
  margin-bottom: var(--dpp-space-2);
}

.ask-sources li > span:first-child {
  display: block;
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

/* Зона нажатия ссылки-источника — не меньше 48 px: список идёт плотно,
   и соседние строки иначе ловят палец. */
.ask-sources a {
  display: inline-flex;
  align-items: center;
  min-height: 3rem;
  font-weight: 600;
}

/* --- Шапка экрана и входы (E33) --------------------------------------------
 *
 * .entry-card стоит после .btn намеренно: специфичность у них одинаковая,
 * и раскладку primary-входа решает порядок правил — .btn держит содержимое
 * строкой по центру, а внутри входа два span стопкой.
 */
.screen-header {
  margin-bottom: var(--dpp-space-6);
}

/* margin-top: Pico отбивает заголовок от соседнего абзаца на 3rem, а в шапке
   экрана перед h1 всегда стоит надзаголовок или ссылка вверх — отбивка
   превращалась в пустую полосу над заголовком каждой страницы. */
.screen-header h1 {
  margin-top: 0;
  margin-bottom: var(--dpp-space-2);
}

.screen-back {
  margin-bottom: var(--dpp-space-2);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
}

.screen-lead {
  margin-bottom: var(--dpp-space-2);
}

.screen-note {
  margin-bottom: 0;
  color: var(--pico-muted-color);
  font-size: var(--dpp-font-caption);
  line-height: var(--dpp-line-caption);
  /* Заметкой стоит и почта профиля: длинный адрес без переноса растягивает
     экран на 390 px. */
  overflow-wrap: anywhere;
}

/* Шапка без заголовка стоит вплотную к чужому <h1>: у оборудования счётчик
   остаётся в заголовке раздела, и шапке остаётся один надзаголовок. */
.screen-header:not(:has(h1)) {
  margin-bottom: 0;
}

.entry-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--dpp-space-1);
  /* Зона нажатия не меньше 48 px: экран открывают большим пальцем на 390 px. */
  min-height: 4rem;
  margin-bottom: var(--dpp-space-3);
  padding: var(--dpp-space-3) var(--dpp-space-4);
  border: 1px solid transparent;
  border-radius: var(--dpp-radius-md);
  text-decoration: none;
}

.entry-title {
  font-size: var(--dpp-font-body);
  font-weight: 600;
  line-height: var(--dpp-line-body);
}

/* Вес задан явно: у primary-входа подпись лежит внутри .btn и без этого
   наследует его 600, теряя контраст с заголовком. Цвет — основная нейтраль
   по той же причине, что у .list-meta: на заливках .entry-plain (4.33:1)
   и .entry-attention (4.31:1) приглушённая до 4.5:1 не дотягивает. */
.entry-meta {
  color: var(--dpp-text-primary);
  font-size: var(--dpp-font-caption);
  font-weight: 400;
  line-height: var(--dpp-line-caption);
}

.entry-plain {
  border-color: var(--dpp-border);
  background: var(--dpp-bg-subtle);
}

.entry-attention {
  border-color: var(--dpp-border);
  background: var(--dpp-bg-warning);
}

/* Заливку primary даёт .btn-primary; своя здесь только подпись — приглушённая
   нейтраль на брендовой заливке нечитаема. */
.entry-primary .entry-meta {
  color: var(--dpp-text-on-brand);
}

/* Форма поиска стоит в стопке входов (E40) и держит их ритм: своя отбивка
   снизу как у .entry-card. Поле и кнопка — тот же бар, что у экранов поиска
   (.asset-search-bar): кнопка в строку с полем, не полосой во всю ширину —
   так она читалась бы вторым главным действием Главной. */
.home-search {
  margin-bottom: var(--dpp-space-3);
}

.home-search input {
  margin-bottom: 0;
}

/* --- Строка списка разделов (кит 76:2, E37) ---------------------------------
 *
 * Строчный flex, из которого выросло правило списка оборудования: у всех пяти
 * разделов читают название и мету, а обложка здесь опознавательный знак —
 * на 390 px две колонки из текста дали бы столбик по слову в строке.
 */
.list-cards {
  padding-left: 0;
}

/* Маркер снимается на li: pico задаёт list-style самому пункту, и с <ul>
   правило не перебивается. Комбинатор дочерний — вложенные списки строки
   (чипы тегов оборудования) маркеры оставляют себе; дерево мест под правило
   попадает через свой же `.list-cards`. */
.list-cards > li {
  list-style: none;
}

/* Вложенный список отступ возвращает себе: снятый выше padding браузера
   рисовал бы дерево мест плоским — «Котельная» вровень с «Домом». */
.list-cards .list-cards {
  padding-left: var(--dpp-space-4);
}

.list-card {
  display: flex;
  gap: var(--pico-spacing);
  align-items: flex-start;
  margin-bottom: var(--pico-spacing);
  padding: calc(var(--pico-spacing) * 0.5);
  /* Поверхность строки — токеном, а не пиковской карточкой: заливка кита
     одна на все пять разделов. */
  background: var(--dpp-bg-subtle);
}

/* Обложка ужата до значка и не растягивается: превью приходит в 640 px
   по длинной стороне, и во всю ширину строки оно оттеснило бы название
   под сгиб. Доля ширины, а не пиксели, — на узком экране значок сжимается
   вместе с ним. Размер задан обёртке, а не картинке: обложка бывает ссылкой,
   и flex-basis на вложенном img считался бы от неё, а не от строки. */
.list-thumb {
  flex: 0 0 25%;
  max-width: 6rem;
}

/* Место под кадр держит квадрат: габаритов у тега нет, и без соотношения
   строка растёт с приходом ленивого превью. Обрезка законна — это
   опознавательный знак, а не кадр-доказательство. */
.list-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--pico-border-radius);
}

/* Текст забирает остаток строки; min-width обнулён, иначе длинное название
   распирает колонку и выдавливает обложку за край. */
.list-card-text {
  flex: 1;
  min-width: 0;
}

/* Абзацы внутри строки идут вплотную: это подписи одной карточки,
   а не отдельные мысли. */
.list-card :is(h3, p) {
  margin-bottom: calc(var(--pico-spacing) * 0.25);
}

.list-card :is(h3, p):last-child {
  margin-bottom: 0;
}

/* Название идёт вплотную к надзаголовку: пиковское `p ~ h3` дарит заголовку
   после абзаца верхний отступ абзаца — в строке списка это тот же шов между
   подписями одной карточки, что снят правилом выше. Строке без надзаголовка
   правило ничего не меняет: у заголовков margin-top и так ноль. */
.list-card h3 {
  margin-top: 0;
  font-size: var(--dpp-font-title);
  line-height: var(--dpp-line-title);
}

/* Мета читается основной нейтралью, а не приглушённой: на заливке строки
   (--dpp-bg-subtle) --dpp-text-secondary даёт 4.33:1 и до 4.5:1 не дотягивает,
   а мета здесь — вид, даты и место, а не украшение. Подчинённость заголовку
   несут кегль <small> и вес против 18/600. */
.list-meta {
  color: var(--dpp-text-primary);
}

/* Тело строки списка — то, что человек написал фразами и переводами строк
   (описание работы, текст записки: по две тысячи символов). Без pre-line
   браузер схлопывает их в одну сплошную строку, и записанное «поменяли
   фильтр / подтянули клапан» читается уже не тем, чем записано. pre-line,
   а не pre: длинные строки обязаны переноситься по ширине экрана. */
.list-body {
  white-space: pre-line;
}

/* Главное действие строки — во всю ширину: это ровно то, ради чего в раздел
   пришли. Ширину сегодня даёт и `display: flex` у `.btn` (ссылка-кнопка
   становится блочной), но правило стоит явно: смена раскладки кнопки не
   обязана менять ширину действия. Ссылки-кнопки, нарисованные самим разделом,
   под правило не попадают и ужимаются своим классом (.list-card-edit). */
.list-card-action a[role="button"] {
  width: 100%;
}

/* Форма в подвале строки отступа не добавляет: строку закрывает сама карточка. */
.list-card form {
  margin-bottom: 0;
}

/* Зазор перед «Убрать» отделяет её от «Отмены»: у той зона нажатия в 48 px,
   удаление стоит вплотную снизу, и промах по нижнему краю зоны убирал бы
   место вместо выхода из правки. */
.list-card-remove {
  margin-top: var(--pico-spacing);
}

/* Подвал строки: действия одним рядом, «Убрать» уходит вправо. Столбик
   полноширинных кнопок съедал экран, а удаление в нём стояло вплотную
   к соседнему действию. Зазор ряда — тот же, что и вертикальный, поэтому
   собственные отступы панелей внутри снимаются: два зазора подряд рвут ряд
   на глаз. `:not(:only-child)` оставляет одинокую «Убрать» слева — прижатая
   вправо, она читалась бы главным действием строки. */
.list-card-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pico-spacing);
  margin-top: var(--pico-spacing);
}

.list-card-actions > * {
  margin: 0;
}

.list-card-actions .list-card-remove:not(:only-child) {
  margin-left: auto;
}

/* Формы внутри ряда сами раскладывают свои кнопки: у отсрочки их две. */
.list-card-actions form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pico-spacing);
}

/* «Убрать» приглушена контуром и подписью и не во всю ширину — pico тянет
   button[type=submit] на 100%, а столбик полноширинных «Убрать» и есть то,
   ради чего раздел переехал на строки. Высота остаётся кнопочной (.btn,
   3.25rem) — приглушённая не значит мелкая. Правило висит на классе формы,
   а не на `.list-card form`: в карточке правки места форм две, и вторая кнопка
   там — «Сохранить», которую приглушать не за что. Приглушает нейтраль вместо
   бренда, а не осветление: на заливке строки --dpp-border даёт 1.12:1 — контур
   пропадает вовсе, а --dpp-text-secondary в подписи 16/600 не дотягивает
   до 4.5:1. */
.list-card-remove .btn {
  width: fit-content;
  margin-bottom: 0;
  border-color: var(--dpp-text-secondary);
  color: var(--dpp-text-primary);
}

/* «Изменить» у места — шириной по подписи: полная ширина есть только
   у главного действия раздела, а `.btn` — display: flex, то есть ссылка-кнопка
   растягивается сама, без всякого width. Без правила дерево получает столбик
   полноширинных «Изменить» — ровно то, ради чего раздел переехал на строки. */
.list-card-edit .btn {
  width: fit-content;
}

/* --- Хаб карточки оборудования (E34) --------------------------------------- */

/* Обложка на хабе. Обрезка (`cover`), в отличие от кадра карточки
   подтверждения: шильдик по ней не читают — она отвечает на «тот ли это
   аппарат», и поля по бокам вместо этого съедали бы первый экран на 390 px.
   Снимок целиком открывается по ссылке. */
.asset-cover {
  display: block;
  aspect-ratio: 4 / 3;
  max-height: 30vh;
  margin-bottom: var(--dpp-space-4);
}

.asset-cover img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--dpp-radius-md);
  background: var(--dpp-bg-subtle);
  object-fit: cover;
}

/* Плашка состояния: рисуется только у неработающего аппарата, поэтому
   и цвет внимания. */
.asset-state {
  margin-bottom: var(--dpp-space-4);
  padding: var(--dpp-space-2) var(--dpp-space-3);
  border-radius: var(--dpp-radius-sm);
  background: var(--dpp-bg-warning);
  color: var(--dpp-text-primary);
  font-size: var(--dpp-font-caption);
  font-weight: 600;
  line-height: var(--dpp-line-caption);
}

/* Действия хаба: вопрос про аппарат и «Добавить». Кнопками, а не входами —
   раздела и счётчика за ними нет. */
.asset-actions {
  display: grid;
  gap: var(--dpp-space-4);
  margin-bottom: var(--pico-spacing);
}

/* --- «Сведения» карточки (E34) ---------------------------------------------- */

/* Две карты раздела. Различаются не одним цветом: у каждой свой заголовок
   («Записано», «Можно дополнить») — состояние, закодированное только тоном,
   доступность кита запрещает. */
.fact-card {
  margin-bottom: var(--dpp-space-4);
  padding: var(--dpp-space-3);
  border: 1px solid var(--dpp-border);
  border-radius: var(--dpp-radius-md);
}

.fact-recorded {
  background: var(--dpp-bg-success);
}

.fact-wanted {
  background: var(--dpp-bg-warning);
}

/* --- Страница по ссылке мастеру (E15) --------------------------------------- */

/* Вопрос владельца: ради него ссылку и отправляют, а строкой среди сведений
   он потерялся бы. */
.guest-question {
  margin-bottom: var(--dpp-space-4);
  padding: var(--dpp-space-3);
  border-radius: var(--dpp-radius-md);
  background: var(--dpp-bg-success);
}

/* Подвал гостя: чей это доступ и до какого срока. Единственное на странице,
   что говорит о владельце, — навигации у гостя нет. */
.guest-footer {
  margin-top: var(--dpp-space-6);
  padding-top: var(--dpp-space-3);
  border-top: 1px solid var(--dpp-border);
  color: var(--dpp-text-secondary);
}

/* Кадры проблемы у гостя — сетка, как галерея объекта: их снимают пачкой,
   и колонка в один столбец уводит бумаги ниже второго экрана. Место под кадр
   держит соотношение: габаритов у тега нет, а loading="lazy" доставляет
   картинку после отрисовки — без него страница прыгает под пальцем. Обрезки
   нет: это снимок-доказательство, мастер смотрит на него, а не выбирает. */
.guest-frames {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--pico-spacing);
  margin-bottom: var(--pico-block-spacing-vertical);
}

.guest-frames img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  background: var(--pico-code-background-color);
  border-radius: var(--pico-border-radius);
}

/* Строка состава пакета: галочка, подпись и счёт записей. Без сетки счёт
   ложится на подпись — у метки pico своя базовая строка, и `small` съезжает
   под галочку поверх текста. Высота строки — та же гребёнка в 48 px, что
   у чипов тегов: по чекбоксу с подписью в одну строку пальцем не попадают. */
.share-blocks label:has(input[type="checkbox"]) {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: var(--dpp-space-2);
  min-height: 3rem;
  margin-bottom: 0;
}

.share-blocks .share-block-meta {
  grid-column: 2;
  color: var(--dpp-text-secondary);
}
