/* Компонентные стили приложения. Только var(--app-*) / var(--bs-*), никаких литеральных цветов. */

body {
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

/* Крупные числа и таймеры */
.num {
  font-family: var(--app-font-num);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.text-secondary-app { color: var(--app-text-secondary); }
.text-muted-app { color: var(--app-text-muted); }

/* Карточка — базовая поверхность интерфейса. Отступов у неё нет намеренно:
   их задаёт роль карточки (.app-stat, .app-workout…), а для простой карточки
   «заголовок, текст, кнопка» есть .app-panel. Голая .app-card без роли —
   это текст, прилипший к рамке. */
.app-card {
  background-color: var(--app-card);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  box-shadow: var(--app-shadow-card);
}

.app-panel { padding: var(--app-space-4) var(--app-card-inset); }

/* ---------- Шапка ---------- */

.app-header {
  position: sticky;
  top: 0;
  z-index: 1030;
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: var(--app-header-height);
  /* env(safe-area-inset-top): в standalone на телефоне с «бровью» контент
     иначе уезжает под статус-бар (viewport-fit=cover). Задаём тремя
     свойствами, а не шорткатом: медиазапросы меняют только вертикальный
     отступ, и шорткат там затирал бы безопасную зону. */
  padding-block-start: calc(var(--app-space-2) + env(safe-area-inset-top));
  padding-block-end: var(--app-space-2);
  padding-inline: var(--app-gutter);
  background-color: var(--app-bg);
}

.app-logo {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 800;
  font-size: 1.3rem;
  letter-spacing: -0.02em;
  color: var(--app-text);
  text-decoration: none;
}

/* На светлой теме сам #C6F13E на белом почти не виден — берём «чернильный» акцент */
.app-logo svg { color: var(--app-accent-text); }

.app-nav {
  display: none;
  gap: 1.25rem;
}

.app-nav a {
  color: var(--app-text-secondary);
  font-weight: 600;
  text-decoration: none;
}

.app-nav a.is-active { color: var(--app-text); }

.app-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--app-tap-target);
  height: var(--app-tap-target);
  padding: 0;
  border: 0;
  border-radius: var(--app-radius-pill);
  background-color: transparent;
  color: var(--app-text-secondary);
}

.app-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--app-tap-target);
  height: var(--app-tap-target);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-pill);
  background-color: var(--app-raised);
  color: var(--app-accent-text);
  font-weight: 700;
  text-decoration: none;
}

/* Иконка темы: показываем ту, что соответствует текущей теме */
[data-bs-theme="dark"] .app-theme-icon-dark,
[data-bs-theme="light"] .app-theme-icon-light { display: block; }
[data-bs-theme="dark"] .app-theme-icon-light,
[data-bs-theme="light"] .app-theme-icon-dark { display: none; }

/* ---------- Каркас ПК ----------
   Боковая панель и инструменты шапки появляются только на ПК (блок 768). На
   телефоне обёртки каркаса не образуют боксов, и дерево страницы прежнее.
   Правило стоит после .app-icon-btn: тот задаёт display и, будучи ниже,
   снова показал бы кнопку сворачивания. */

.app-shell,
.app-shell-main { display: contents; }

.app-sidebar,
.app-sidebar-toggle,
.app-header-search { display: none; }

/* ---------- Основная область ---------- */

.app-main {
  /* Нижняя навигация фиксированная, поэтому её высоту страница компенсирует
     отступом. В standalone на iPhone навигация выше на домашнюю полосу
     (env(safe-area-inset-bottom) ≈ 34px), и без этого слагаемого последний
     элемент страницы — «Показать ещё» в истории — уезжал под неё. */
  padding: var(--app-space-2) var(--app-gutter)
    calc(var(--app-bottom-nav-height) + env(safe-area-inset-bottom) + var(--app-space-6));
}

.app-page-title {
  font-weight: 800;
  font-size: var(--app-title-size);
  letter-spacing: -0.02em;
  margin-bottom: 1rem;
}

/* ---------- Обратная связь ---------- */

.app-feedback-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--app-space-2);
  margin-bottom: var(--app-space-2);
  color: var(--app-text-secondary);
  font-size: 0.875rem;
}
.app-feedback-text { overflow-wrap: anywhere; }
/* Переписка — столбик реплик; ответ администратора — с акцентной рейкой слева,
   чтобы его было видно в длинной ленте. Рейка — тенью, а не рамкой: рамка
   сдвинула бы текст ответа относительно соседних реплик. Тень карточки сюда
   не добавляется списком: в тёмной теме она «none», и список стал бы
   невалидным — пропало бы всё объявление. */
.app-feedback-thread {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-2);
}
.app-feedback-message.is-admin { box-shadow: inset 3px 0 0 var(--app-accent); }
.app-feedback-meta .app-badge { flex: 0 0 auto; white-space: nowrap; }
.app-feedback > section + section { margin-top: var(--app-space-6); }

/* Заголовок справочника с кнопкой «Создать» справа. Отступ снизу — тот же,
   что у голого заголовка страницы. */
.app-catalog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--app-space-3);
  margin-bottom: 1rem;
}
.app-catalog-create {
  display: inline-flex;
  align-items: center;
  gap: var(--app-space-1);
  flex: 0 0 auto;
}

.app-section-title {
  font-size: 1rem;
  font-weight: 700;
  color: var(--app-text-secondary);
  margin-bottom: 0.5rem;
}

/* ---------- Нижняя навигация (мобильная) ---------- */

.app-bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1030;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  align-items: end;
  height: calc(var(--app-bottom-nav-height) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background-color: var(--app-bg);
  border-top: 1px solid var(--app-line);
}

/* Кнопка «+» — button, а не ссылка: она открывает модалку, а не страницу */
.app-bottom-nav a,
.app-bottom-nav button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  height: var(--app-bottom-nav-height);
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--app-text-secondary);
  font-size: 0.75rem;
  font-weight: 600;
  text-decoration: none;
}

.app-bottom-nav a.is-active { color: var(--app-accent-text); }

.app-fab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 0.5rem;
  border: 0;
  border-radius: var(--app-radius-pill);
  background-color: var(--app-accent);
  color: var(--app-on-accent);
  /* Кнопка приподнята над панелью — как в макете */
  transform: translateY(-10px);
  box-shadow: var(--app-accent-glow);
}

/* Кнопка новой тренировки в шапке — только на десктопе */
.app-header-cta {
  display: none;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5rem 1.125rem;
}

/* ---------- Экраны: заголовок с кнопкой назад ---------- */

/* Стрелка стоит на строке заголовка, а не посередине блока «заголовок +
   подзаголовок»: с центровкой она съезжала между названием и датой. Сдвиг —
   половина разницы между высотой строки заголовка (line-height 1.2 у h1) и
   кнопкой 44px; у однострочной головы результат тот же, что и с центровкой. */
.app-screen-head {
  display: flex;
  align-items: flex-start;
  gap: 0.25rem;
  margin-bottom: 1rem;
}

.app-screen-head .app-icon-btn {
  flex: 0 0 auto;
  margin-top: calc((var(--app-title-size) * 1.2 - var(--app-tap-target)) / 2);
  margin-left: -0.625rem;
}

/* ---------- Чипы ---------- */

.app-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Тумблер из двух половин (чузер «+»): одна строка, а не столбик на 375px */
.app-chips-split {
  flex-wrap: nowrap;
}

.app-chips-split .app-chip {
  flex: 1 1 0;
  justify-content: center;
  padding-inline: 0.5rem;
}

.app-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: var(--app-tap-target);
  padding: 0.5rem 1rem;
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-pill);
  background-color: var(--app-raised);
  color: var(--app-text);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
}

.app-chip-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

/* Выбор вида спорта — обводка акцентом (как в макете формы) */
.app-chip-input:checked + .app-chip {
  border-color: var(--app-accent);
  box-shadow: inset 0 0 0 1px var(--app-accent);
}

/* Активный фильтр — залит акцентом (как в макете истории) */
.app-chip.is-active {
  background-color: var(--app-accent);
  border-color: var(--app-accent);
  color: var(--app-on-accent);
}

.app-chip-input:focus-visible + .app-chip {
  outline: 2px solid var(--app-accent);
  outline-offset: 2px;
}

.app-chip-add {
  border-style: dashed;
  background-color: transparent;
  color: var(--app-text-secondary);
}

/* Прокручиваемый ряд уходит под край экрана, а не обрезается жёсткой линией за
   16px до него: отрицательные поля выводят полосу на ширину вьюпорта, а
   padding и scroll-padding возвращают первый чип на вертикаль страницы. */
.app-filters {
  margin-bottom: 1rem;
  margin-inline: calc(-1 * var(--app-gutter));
  padding-inline: var(--app-gutter);
  padding-bottom: 0.25rem;
  scroll-padding-inline: var(--app-gutter);
  overflow-x: auto;
  flex-wrap: nowrap;
}

.app-filters .app-chip { flex: 0 0 auto; }

/* Две оси фильтров истории: виды спорта и места. Обёртка держит отступ до
   ленты, между рядами — space-2: два ряда по 1rem развели бы заголовок и первую
   карточку на пол-экрана телефона. При одном ряде результат равен прежнему.
   overflow здесь ставить нельзя — срежет горизонтальную прокрутку чипов. */
.app-filter-axes { margin-bottom: var(--app-space-4); }
.app-filter-axes .app-filters { margin-bottom: var(--app-space-2); }
.app-filter-axes .app-filters:last-child { margin-bottom: 0; }

/* Номер упражнения в тренировке — кружок слева от названия. Живёт на четырёх
   экранах (итог, живой режим «сейчас»/«дальше»/«выполнено», история подходов),
   поэтому стоит здесь, рядом с точкой-маркером спорта, а не в секции живого режима.

   Это не кнопка: 1.75rem, а не --app-tap-target — по номеру не тапают, и 44px
   рядом с названием читались бы как круглая кнопка действия. От номера подхода
   (.app-live-set-num) отличается сразу тремя признаками: у того нет фигуры, он
   стоит внутри залитой строки и приглушён сильнее.

   min-width вместо width: упражнений бывает 10+, и двузначный номер должен
   растягивать кружок, а не сплющивать цифры. Гарнитура — Manrope, а не .num:
   Unbounded шире, и на двух цифрах кружок перестал бы быть кругом. */
.app-exercise-num {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  height: 1.75rem;
  padding-inline: var(--app-space-1);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-pill);
  background-color: var(--app-raised);
  color: var(--app-text-secondary);
  font-size: 0.8125rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* Точка-маркер вида спорта */
.app-sport-dot {
  display: inline-block;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--app-radius-pill);
  background-color: var(--dot, var(--app-text-muted));
}

/* ---------- Поля в две колонки и длительность ---------- */

/* Поля пары выравниваются по низу ячейки. На 375px «Средний пульс ·
   необязательно» переносится на вторую строку, и с выравниванием по верху поле
   пульса стояло на 20px ниже соседнего поля дистанции. Раньше так было сделано
   только в модалке, но узкие колонки есть и на странице. */
.app-field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: end;
  gap: 0.75rem 0.75rem;
  margin-bottom: 1rem;
}

.app-duration {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

/* Поле «ч»/«мин» узкое (57px в половине колонки на 375px), а Chrome держит у
   type=number место под стрелки: «00» обрезалось до «0». Стрелки здесь не
   нужны — число набирают с цифровой клавиатуры, — поэтому убираем их и
   отдаём цифрам часть бокового отступа. */
.app-duration input {
  padding-inline: var(--app-space-2);
  text-align: center;
  -moz-appearance: textfield;
  appearance: textfield;
}

.app-duration input::-webkit-inner-spin-button,
.app-duration input::-webkit-outer-spin-button {
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
}
.app-duration-unit { color: var(--app-text-secondary); font-size: 0.875rem; }

.app-computed {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.875rem 1rem;
  margin-bottom: 1rem;
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-field);
  background-color: var(--app-raised);
}

.app-computed strong { font-size: 1.125rem; }

.app-form-actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

/* Кнопка во всю ширину — классом, а не утилитой w-100: та идёт с !important,
   и там, где на ПК кнопка встаёт в ряд, её ширину было бы не снять. */
.app-btn-fill { width: 100%; }

/* Колонки формы кардио: на телефоне боксов не образуют, на ПК — две колонки. */
.app-form-cols,
.app-form-col { display: contents; }

/* Обёртки итога тренировки: на телефоне боксов не образуют, сеткой и
   колонками становятся на ПК (блок 768). */
.app-summary,
.app-summary-list { display: contents; }

/* Кардио-части тренировки: строка на каждую плюс кнопка добавления. Строка —
   тихая кнопка во всю ширину карточки, как .app-live-set: это список, а не
   набор действий, и рамка у каждой строки превратила бы его в частокол. */
.app-cardio-parts {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-2);
  /* Те же отступы и тот же ритм, что у карточки упражнения: это соседние
     блоки одного экрана, и разъезжаться им незачем. */
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 1rem;
}

.app-cardio-part {
  display: flex;
  align-items: center;
  /* Переносим, а не ужимаем: «Велосипед · 9 км · 0:20 · 27,0 км/ч» на 375px в
     строку не влезает, и без переноса от названия остаётся «Вел…». Влезает —
     одна строка; не влезает — метрика уходит под название и остаётся целой. */
  flex-wrap: wrap;
  gap: var(--app-space-2);
  min-height: var(--app-tap-target);
  padding: var(--app-space-2) var(--app-space-3);
  border: 0;
  border-radius: var(--app-radius-field);
  background-color: var(--app-raised);
  color: var(--app-text);
  text-align: left;
}

.app-cardio-part-name {
  font-weight: 700;
  /* Обрезка — последняя мера: сначала метрика уйдёт на вторую строку */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Метрика прижата к карандашу справа: так у всех строк одна вертикаль.
   nowrap — чтобы «9 км · 0:20 · 27,0 км/ч» не переносилось на вторую строку и
   строки частей не разъезжались по высоте: ужиматься должно название. */
.app-cardio-part-meta {
  margin-left: auto;
  color: var(--app-text-secondary);
  white-space: nowrap;
}

/* Тренажёр и настройки части — последней строкой (order) под названием,
   с отступом на точку и зазор: текст встаёт на одну вертикаль с названием. */
.app-cardio-part-machine {
  display: flex;
  flex-basis: 100%;
  align-items: center;
  gap: var(--app-space-1);
  order: 1;
  padding-left: calc(0.625rem + var(--app-space-2));
  color: var(--app-text-secondary);
  font-size: 0.875rem;
}

.app-cardio-part-machine svg { flex: none; }

/* Чипы стиля хода — под чипами вида спорта, которые они уточняют. */
.app-style-chips { margin-top: var(--app-space-2); }

/* Набор высоты в ленте — приписка к дистанции: мельче и вторичным цветом,
   чтобы читалась как уточнение, а не как вторая метрика. На телефоне — под
   километрами: в строку с ними «15,2 км · 180 м» выталкивало темп на вторую
   строку. В строку — только в плотной ленте с 1200, где у колонки свой трек. */
.app-elevation {
  display: flex;
  align-items: center;
  gap: var(--app-space-1);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--app-text-secondary);
}

.app-elevation svg { flex: none; }

/* Блок «Тренажёр» кардио-формы: заголовок-переключатель, как у «Тренажёров»
   на странице упражнения. */
.app-cardio-machine > summary { list-style: none; }
.app-cardio-machine > summary::-webkit-details-marker { display: none; }
.app-cardio-machine[open] .app-machines-chevron { transform: rotate(180deg); }
.app-cardio-machine[open] > summary { margin-bottom: var(--app-space-2); }

.app-cardio-add { margin-top: var(--app-space-1); }

/* Предложение подготовить тренировку на введённый будущий день. Отделено от
   основной кнопки рамкой сверху: это другое намерение, а не второй способ
   сделать то же самое. Своего отступа снизу нет — его задаёт gap контейнера. */
.app-offer {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-2);
  padding-top: var(--app-space-3);
  border-top: 1px solid var(--app-line);
}

.app-offer-text {
  color: var(--app-text-secondary);
  font-size: 0.875rem;
}

.btn-danger-quiet {
  background-color: transparent;
  border: 1px solid var(--app-line);
  color: var(--app-danger);
}

/* :hover этой кнопки живёт в секции отделки; здесь остаётся фокус — он нужен
   и на тач-устройствах, где hover-медиазапрос не срабатывает. */
.btn-danger-quiet:focus {
  border-color: var(--app-danger);
  color: var(--app-danger);
}

/* ---------- Лента истории ---------- */

.app-week-title {
  margin: 1.25rem 0 0.5rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--app-text-secondary);
}

.app-week-title:first-child { margin-top: 0; }

.app-workout {
  display: block;
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 0.75rem;
  color: var(--app-text);
  text-decoration: none;
}

.app-workout-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.625rem;
}

/* Заголовок силовой — группы мышц, а не короткое «Силовая», поэтому шапка
   должна держать дату на месте: min-width сверху донизу разрешает флекс-детям
   сжиматься (по умолчанию у них min-width: auto, и длинный заголовок выдавил бы
   дату), обрезка забирает остаток, а дата не сжимается вовсе. */
.app-workout-sport {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-workout-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-workout-head > .text-secondary-app {
  flex: 0 0 auto;
}

.app-metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.25rem;
  margin: 0;
}

.app-metrics dt {
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--app-text-secondary);
}

.app-metrics dd {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 700;
}

.app-workout-note {
  margin: 0.625rem 0 0;
  font-size: 0.9375rem;
}

/* Место и заметка карточки истории. На телефоне обёртка не образует бокса,
   поэтому существующие карточки рендерятся ровно как до появления места;
   колонкой subgrid она становится только на 1200px. */
.app-workout-meta { display: contents; }

.app-workout-meta > .app-place {
  display: flex;
  /* Тот же оптический отступ, что у .app-workout-note выше. */
  margin: 0.625rem 0 0;
}

.app-empty {
  padding: 2rem 1.5rem;
  text-align: center;
}

.app-empty-title {
  margin-bottom: 0.375rem;
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-empty-text { margin-bottom: 0; }

/* Компактный вариант: заглушка графика в панели упражнения не должна занимать
   пол-экрана — она сообщает о нехватке данных, а не приглашает к действию. */
.app-empty-slim {
  padding: var(--app-space-3) var(--app-card-inset);
  text-align: start;
}

.app-empty-actions { margin-top: 1rem; }

/* ---------- Модалка ---------- */

.app-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1050;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 1rem;
  /* Модалка — шторка, прижатая к низу. В standalone на iPhone её нижний край и
     последняя строка списка попадали в зону домашней полосы, где живёт
     системный жест. На десктопе слагаемое равно нулю и ничего не меняет. */
  padding-bottom: calc(1rem + env(safe-area-inset-bottom));
  background-color: var(--app-backdrop);
}

.app-modal {
  width: 100%;
  max-width: var(--app-measure-narrow);
  padding: 1.25rem;
}

.app-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.app-modal-title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* Обёртки колонок широкого окна. На телефоне боксов не образуют — дерево и
   отступы там прежние; колонками они становятся только на ПК (блок 768). */
.app-modal-cols,
.app-modal-col,
.app-picker-list { display: contents; }

/* Отступ над переключателем «Начать сейчас / Подготовить» — классом, а не
   утилитой mt-3: та идёт с !important, и в колонке окна её было бы не снять. */
.app-start-mode { margin-top: var(--app-space-4); }

[x-cloak] { display: none !important; }

/* ---------- Сообщения ---------- */

.app-alert {
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
  border: 1px solid var(--app-line);
  border-left: 3px solid var(--app-text-muted);
  border-radius: var(--app-radius-field);
  background-color: var(--app-card);
  color: var(--app-text);
  font-size: 0.9375rem;
}

.app-alert-success { border-left-color: var(--app-success); }
.app-alert-danger { border-left-color: var(--app-danger); }
.app-alert-info { border-left-color: var(--app-accent); }

/* Сообщение с действием («Тренировка удалена» → «Восстановить»): текст и
   кнопка в строку. Кнопка — тач-таргет, но отрицательные поля по вертикали
   съедают её высоту в отступах сообщения: плашка не раздувается вдвое. */
.app-alert.has-action {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--app-space-3);
}

.app-alert-action {
  flex: 0 0 auto;
  min-height: var(--app-tap-target);
  margin-block: calc(-1 * var(--app-space-2));
  padding: 0 var(--app-space-3);
  border: 0;
  border-radius: var(--app-radius-pill);
  background: none;
  color: var(--app-accent-text);
  font-weight: 700;
}

/* «Недавно удалённые»: пояснение под заголовком и ссылка внизу истории. */
.app-trash-lead { margin-bottom: var(--app-space-4); }

.app-trash-link {
  margin: var(--app-space-6) 0 0;
  text-align: center;
  font-size: 0.875rem;
}

.app-trash-restore { flex: 0 0 auto; }

/* Подпись строки корзины длинная, и флекс сжимал бы точку в полоску. */
.app-trash-row > .app-sport-dot { flex: none; }

/* «Удалить аккаунт»: блок выгрузки отделён от предупреждения и от пароля. */
.app-deletion-export {
  margin: var(--app-space-4) 0;
  padding: var(--app-space-4) 0;
  border-block: 1px solid var(--app-line);
}

.app-deletion-note {
  margin: var(--app-space-3) 0 0;
  font-size: 0.875rem;
}

/* ---------- Формы и кнопки ---------- */

.form-control,
.form-select {
  min-height: var(--app-tap-target);
  background-color: var(--app-raised);
  border-color: var(--app-line);
  color: var(--app-text);
}

.form-control:focus,
.form-select:focus {
  background-color: var(--app-raised);
  border-color: var(--app-accent);
  color: var(--app-text);
  box-shadow: 0 0 0 0.2rem var(--bs-focus-ring-color);
}

.form-control::placeholder { color: var(--app-text-muted); }

.form-label {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--app-text-secondary);
  margin-bottom: 0.375rem;
}

.form-text {
  font-size: 0.8125rem;
  line-height: 1.35;
}

/* Список требований к паролю от валидаторов Django */
.form-text ul {
  margin: 0.25rem 0 0;
  padding-left: 1.1rem;
}

.form-text li + li { margin-top: 0.125rem; }

.btn {
  min-height: var(--app-tap-target);
  border-radius: var(--app-radius-pill);
  font-weight: 700;
}

/* Кнопка-ссылка: у <button> браузер сам ставит текст по центру высоты, у <a>
   текст прижимается к верхнему padding, и при min-height 44px «Открыть» стояла
   на 3px выше соседней «Повторить». Только a.btn — у <button> с этим всё в
   порядке, а display у них переключают свои классы (.app-header-cta). */
a.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn-accent {
  background-color: var(--app-accent);
  border-color: var(--app-accent);
  color: var(--app-on-accent);
}

/* :hover — в секции отделки; фокус и нажатие остаются здесь. */
.btn-accent:focus,
.btn-accent:active {
  background-color: var(--app-accent);
  border-color: var(--app-accent);
  color: var(--app-on-accent);
  filter: brightness(0.94);
}

.btn-quiet {
  background-color: var(--app-raised);
  border: 1px solid var(--app-line);
  color: var(--app-text);
}

.app-link {
  color: var(--app-text-secondary);
  font-weight: 600;
  text-decoration: none;
}

.app-link-accent { color: var(--app-accent-text); }

/* Ссылка «перейти дальше» под списком: строчным текстом и стрелкой, чтобы не
   спорить с заголовками секций. Высота — тач-таргет. */
.app-link-go {
  display: inline-flex;
  align-items: center;
  gap: var(--app-space-1);
  min-height: var(--app-tap-target);
  font-size: 0.875rem;
}

/* Ссылка-действие, которое открывает окно, — кнопка с видом .app-link. */
.app-link-button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
}

/* ---------- Экраны входа/регистрации ---------- */

.app-auth {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100dvh;
  padding: 2rem 1rem;
}

.app-auth-brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin: 1.5rem 0 1.5rem;
}

.app-auth-card {
  width: 100%;
  max-width: var(--app-measure-narrow);
  padding: 1.5rem;
}

.app-auth-title {
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: -0.02em;
  margin-bottom: 0.25rem;
}

.app-auth-subtitle {
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
  margin-bottom: 1.25rem;
}

.app-auth-footer {
  width: 100%;
  max-width: var(--app-measure-narrow);
  margin-top: 1.25rem;
  text-align: center;
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

.app-auth-theme {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
}

/* Бренд-панель входа: ниже 1200 обёртка не образует бокса, подпись скрыта. */
.app-auth-aside { display: contents; }
.app-auth-tagline { display: none; }

/* Поле-ловушка для ботов: невидимо и недоступно с клавиатуры */
.app-honeypot {
  position: absolute !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- Живой режим ---------- */

/* Полоса «связи нет» на живом экране: сообщает, что изменения не доехали. */
.app-live-offline {
  display: flex;
  align-items: center;
  gap: var(--app-space-3);
  padding: var(--app-space-3) var(--app-space-4);
  margin-bottom: var(--app-space-3);
  border: 1px solid var(--app-danger);
  border-radius: var(--app-radius-field);
  background-color: var(--app-card);
  color: var(--app-text);
  font-size: 0.9375rem;
}

.app-live-offline-btn {
  flex: 0 0 auto;
  min-height: 36px;
  padding: var(--app-space-2) var(--app-space-3);
}

.app-live-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.app-live-status {
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

.app-rest {
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 1rem;
}

.app-rest-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.app-rest-label {
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--app-text-muted);
}

.app-rest-skip {
  min-height: var(--app-tap-target);
  padding: 0 0.25rem;
  border: 0;
  background-color: transparent;
  color: var(--app-accent-text);
  font-weight: 700;
}

.app-rest-body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0.25rem 0 0.875rem;
}

.app-rest-adjust {
  flex: 0 0 auto;
  border: 1px solid var(--app-line);
  background-color: var(--app-raised);
  color: var(--app-text);
  font-size: 0.9375rem;
  font-weight: 700;
}

/* Крупные цифры таймера: акцент как текст — только через --app-accent-text */
.app-rest-time {
  font-size: clamp(3rem, 18vw, 4.25rem);
  line-height: 1;
  color: var(--app-accent-text);
}

.app-progress {
  height: 6px;
  border-radius: var(--app-radius-pill);
  background-color: var(--app-raised);
  overflow: hidden;
}

.app-progress-bar {
  width: 0;
  height: 100%;
  border-radius: var(--app-radius-pill);
  background-color: var(--app-accent);
  transition: width 0.25s linear;
}

.app-live-exercise {
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 1rem;
}

.app-live-exercise-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

/* Номер и название — одна группа: у шапки justify-content: space-between, и
   третий прямой ребёнок развёл бы номер с названием на всю ширину. Обёртка
   оставляет у шапки ровно двух детей, поэтому сама .app-live-exercise-head не
   меняется ни на одном из четырёх экранов, где она уже работает.

   min-width: 0 обязателен: без него группа как флекс-элемент имеет ширину по
   содержимому и выдавила бы мету за край карточки на 375px. overflow-wrap —
   про имена, которые придумывают пользователи: «Французскийжимлёжа» реален. */
.app-live-exercise-title {
  display: flex;
  align-items: center;
  gap: var(--app-space-3);
  min-width: 0;
  overflow-wrap: anywhere;
}

.app-live-exercise-name {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-live-exercise-meta {
  color: var(--app-text-secondary);
  font-size: 0.875rem;
  font-weight: 600;
  white-space: nowrap;
}

.app-live-last {
  margin: 0.25rem 0 0;
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

.app-live-sets {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.app-live-set {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: var(--app-tap-target);
  padding: 0.625rem 1rem;
  border: 0;
  border-radius: var(--app-radius-field);
  background-color: var(--app-raised);
  color: var(--app-text);
  font-weight: 700;
  text-align: left;
}

/* Плановый (ещё не выполненный) подход читается как план, а не как факт */
.app-live-set.is-planned {
  border: 1px dashed var(--app-line);
  background-color: transparent;
  color: var(--app-text-secondary);
}

.app-live-set-num {
  min-width: 1.25rem;
  color: var(--app-text-muted);
  font-weight: 700;
}

.app-live-set-check {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--app-accent-text);
}

/* Карандаш у подхода на экране правки записанной тренировки: подход — кнопка,
   тап раскрывает его степперы. Приглушённый — это подсказка, а не действие. */
.app-live-set-icon {
  display: inline-flex;
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--app-text-muted);
}

.app-correct-hint {
  margin: var(--app-space-1) 0 0;
  font-size: 0.875rem;
}

/* Текущий подход: 1px акцента на белой карточке невидим — двойная линия, как у чипов */
.app-live-current {
  margin-top: 0.75rem;
  padding: 0.75rem;
  border: 1px solid var(--app-accent);
  border-radius: var(--app-radius-card);
  box-shadow: inset 0 0 0 1px var(--app-accent);
}

.app-live-current-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

/* Весу нужна колонка пошире: «77,5» длиннее, чем счётчик повторов */
.app-live-current .app-field-grid {
  grid-template-columns: 1.2fr 1fr;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

/* Одно поле (только повторы или только время) занимает всю ширину карточки */
.app-live-current .app-field-grid.is-single {
  grid-template-columns: 1fr;
}

/* У текущего подхода черновика под степперами ничего нет: «Начать тренировку»
   ушла наверх экрана. Поле сетки снизу с паддингом панели дало бы двойной
   отступ. */
.app-live-current .app-field-grid:last-child {
  margin-bottom: 0;
}

/* Старт черновика — «Начать тренировку» и «Записать тренировку задним
   числом» — на месте карточки отдыха, которая появится после старта:
   действия всей тренировки, а не упражнения. Отступ снизу — как у отдыха. */
.app-draft-start {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-2);
  margin-bottom: var(--app-space-4);
}

.app-stepper-label {
  margin-bottom: 0.375rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--app-text-secondary);
  text-align: center;
}

.app-stepper {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  padding: 0.1875rem;
  border-radius: var(--app-radius-field);
  background-color: var(--app-raised);
}

.app-stepper-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--app-tap-target);
  height: var(--app-tap-target);
  border: 0;
  border-radius: calc(var(--app-radius-field) - 4px);
  background-color: var(--app-card);
  color: var(--app-text);
  font-size: 1.25rem;
  font-weight: 700;
}

.app-stepper-value {
  flex: 1 1 auto;
  min-width: 0;
  /* На 375px значению остаётся ~50px — размер подстраивается под вьюпорт */
  font-size: clamp(1.125rem, 5vw, 1.5rem);
  text-align: center;
}

.app-stepper-hint {
  margin-top: 0.375rem;
  font-size: 0.8125rem;
  color: var(--app-text-muted);
  text-align: center;
}

/* Значение ждёт подтверждения сервера: показано предсказание, а не то, что в базе */
.app-stepper-value.is-stale {
  color: var(--app-text-muted);
  text-decoration: underline dotted;
  text-underline-offset: 0.25em;
}

/* Ручной ввод занимает всю ширину поля: кнопки на это время скрыты */
.app-stepper-input {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--app-tap-target);
  text-align: center;
  font-family: var(--app-font-num);
  font-size: clamp(1.125rem, 5vw, 1.5rem);
}

.app-btn-big {
  min-height: 56px;
  font-size: 1.125rem;
}

/* Кнопка с подписью второй строкой («Продолжить тренировку» / «идёт 23 мин»).
   Высота та же, что у главной кнопки живого экрана. */
.app-btn-resume {
  flex-direction: column;
  gap: 0.125rem;
  min-height: 56px;
  line-height: 1.2;
}

.app-btn-sub {
  font-size: 0.8125rem;
  font-weight: 600;
}

/* Заметка упражнения: строка-кнопка под подсказкой «прошлый раз».
   Своя заметка читается как текст, пустая — как действие. */
.app-note-btn {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  width: 100%;
  min-height: var(--app-tap-target);
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
  text-align: left;
}

.app-note-btn.is-empty {
  color: var(--app-text-muted);
  font-weight: 700;
}

.app-note-icon { flex: 0 0 auto; }

.app-live-links {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.25rem;
}

.app-live-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--app-tap-target);
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--app-text-muted);
  font-weight: 700;
}

.app-live-link.is-accent { color: var(--app-accent-text); }

.app-queue-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.875rem var(--app-card-inset);
  margin-bottom: 0.75rem;
  color: var(--app-text);
  text-align: left;
}

.app-queue-text {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

/* anywhere — предохранитель: названия придумывают пользователи, и одно длинное
   слово без пробелов иначе растянуло бы строку очереди за край экрана. */
.app-queue-name {
  font-weight: 800;
  overflow-wrap: anywhere;
}

.app-queue-hint {
  color: var(--app-text-secondary);
  font-size: 0.875rem;
}

.app-pick-chevron {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--app-text-muted);
}

.app-live-add-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  min-height: 56px;
  margin-bottom: 1rem;
  border: 1px dashed var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: transparent;
  color: var(--app-text-secondary);
  font-weight: 700;
}

/* Списки выбора в модалках: старт тренировки, поиск упражнения */
.app-pick-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  max-height: 50vh;
  margin-top: 0.75rem;
  overflow-y: auto;
}

/* Строки-формы (старт, «создать») ведут себя как обычные строки списка */
.app-pick-list form { display: contents; }

.app-pick-row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: var(--app-tap-target);
  padding: 0.625rem 0.75rem;
  border: 0;
  border-radius: var(--app-radius-field);
  background-color: transparent;
  color: var(--app-text);
  font-weight: 600;
  text-align: left;
  text-decoration: none;
}

.app-pick-meta {
  margin-left: auto;
  color: var(--app-text-muted);
  font-size: 0.875rem;
}

.app-pick-meta + .app-pick-chevron,
/* Подпись плана не переносится, а имя при нужде обрезается: у датированного
   плана в строке стоят день и две цели, и без этого строка вырастала бы вдвое
   на длинном названии вида спорта. Тот же приём, что в шапке карточки истории. */
.app-pick-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-pick-meta { white-space: nowrap; }

/* Результаты поиска упражнений — исключение из обрезки выше: имена справочника
   точные, и различаются они хвостом («…на наклонной скамье» против «…лёжа»).
   Две строки, дальше всё-таки многоточие: без предела одна строка результатов
   могла бы занять пол-экрана телефона. anywhere — предохранитель на случай
   длинного слова без пробелов, которое иначе растянуло бы модалку. */
.app-pick-name.is-wrap {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
  overflow-wrap: anywhere;
}

.app-pick-name + .app-pick-chevron { margin-left: auto; }

.app-pick-meta ~ .app-pick-chevron { margin-left: 0.5rem; }

.app-pick-create {
  border: 1px dashed var(--app-line);
  color: var(--app-accent-text);
  font-weight: 700;
}

/* Кнопка силовой карточки истории: «Повторить» во всю ширину */
.app-workout-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.875rem;
}

.app-workout-actions form { flex: 1 1 auto; }

/* Силовая карточка нажимается целиком, как кардио, но ссылкой быть не может: в ней
   форма «Повторить». Ссылка — заголовок, её ::after растянут на карточку. Обрезка
   заголовка (overflow: hidden) его не задевает: блок-контейнер у ::after — сама
   карточка, а она предок заголовка. Над ссылкой поднята только форма, а не весь
   ряд кнопок: пустое место ряда и шеврон остаются частью ссылки. */
.app-workout-stretch { position: relative; }

.app-workout-open {
  color: inherit;
  text-decoration: none;
}

.app-workout-open::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--app-radius-card);
}

.app-workout-stretch .app-workout-actions form {
  position: relative;
  z-index: 1;
}

/* Фокус с клавиатуры — рамкой всей карточки, а не одного заголовка: рамку рисует
   растянутый ::after. Без :has(), поэтому фокус виден в любом браузере. */
.app-workout-open:focus-visible { outline: none; }

.app-workout-open:focus-visible::after {
  outline: var(--app-focus-outline);
  outline-offset: var(--app-focus-offset);
}

/* Шеврон нужен только плотной строке на ≥1200px: на телефоне карточка очевидно
   нажимаемая, и указатель там лишний. У силовой он в ряду кнопок, у кардио —
   прямо в карточке, поэтому селектор потомка. */
.app-workout .app-pick-chevron { display: none; }

/* ---------- Суперсеты: суперсет, трисет, суперсет из N ----------

   В коде — circuit, «круг». Суперсет — обёртка над карточками своих
   упражнений (теми же, что у одиночных): полоса акцентом слева и подпись
   сверху. Отступы между карточками дают сами карточки, у обёртки — только
   снизу, после ряда действий. */
.app-circuit {
  padding-left: var(--app-space-2);
  border-left: 2px solid var(--app-accent-text);
}

.app-circuit-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--app-space-3);
  margin-bottom: var(--app-space-2);
}

.app-circuit-label { font-weight: 800; }

.app-circuit-round {
  color: var(--app-text-secondary);
  font-size: 0.875rem;
}

.app-circuit-actions {
  display: flex;
  justify-content: space-between;
  gap: var(--app-space-3);
  margin-bottom: var(--app-space-4);
}

/* Окно «Суперсеты»: строки упражнений, между соседними — «связать». */
.app-circuit-list {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-2);
}

.app-circuit-row {
  display: flex;
  align-items: center;
  gap: var(--app-space-3);
  min-height: var(--app-tap-target);
}

.app-circuit-name {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 700;
}

.app-circuit-remove { flex: none; }

.app-circuit.is-modal { padding-block: var(--app-space-1); }

.app-circuit-link {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: var(--app-space-2);
  min-height: var(--app-tap-target);
  padding: 0 var(--app-space-4);
  border: 1px dashed var(--app-line);
  border-radius: var(--app-radius-pill);
  background: none;
  color: var(--app-accent-text);
  font-weight: 700;
}

/* ---------- Справочник упражнений ---------- */

/* Шторка упражнения живёт только на ≥1200px, поверх таблицы. По умолчанию её
   нет — на мобильном упражнение открывается отдельной страницей. Таблица
   справочника — только ПК (блок 768): телефон читает плитки и группы. */
.app-catalog-detail,
.app-catalog-table { display: none; }

/* Заголовок блока экрана («Я тренирую» / «Весь справочник»). Единственная дельта
   к .app-section-title — основной цвет текста: под ним стоят заголовки групп, и
   различать два уровня одним размером в узкой колонке не выходит. */
.app-catalog-title { color: var(--app-text); }

/* «Я тренирую» — упражнения, которые пользователь действительно делал. Шесть из
   двадцати четырёх: остальные восемнадцать несут одно название, и плотная строка
   для них честнее плитки.

   Почему не .app-records-item: на ≥1200px он намеренно превращается в строку
   карточки рекордов — в каталоге это дало бы ровно то, от чего уходим, и
   отменять пришлось бы шесть свойств. Почему не .app-stat: это плитка сводки,
   никогда не ссылка, и её значение в 2rem перекричало бы название, а название
   здесь главное. */
.app-trained-list {
  display: grid;
  /* Две колонки на всех ширинах: на 375px внутри плитки остаётся 141px, при мере
     34rem — 242px, в колонке 30rem — 210px. Третья колонка потребовала бы сузить
     плитку до 149px, а самая длинная строка рекорда («120 повторов») просит 132px. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--app-space-3);
  margin-bottom: var(--app-space-6);
}

.app-trained-item {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-1);
  /* 12px, а не 20px как у плиток дашборда: плитка живёт в узкой колонке, и каждый
     лишний пиксель отступа отбирает его у названия. */
  padding: var(--app-space-3);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
  color: var(--app-text);
  text-decoration: none;
}

/* Типографика — та же, что у .app-row-name: два блока экрана читаются как один
   список, а не как две разные страницы. Высота зарезервирована под две строки,
   поэтому плитки в ряду стоят ровно. Клэмп не даёт вырасти третьей строке (на
   мобильном её разрешаем: там строка вмещает 14 букв, и обрезать имя, которое
   сегодня спокойно переносится, было бы ухудшением). overflow-wrap — на случай
   одного неразрывного слова: названия придумывают пользователи. */
.app-trained-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  line-height: 1.25;
  font-weight: 800;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

/* Размер — как у .app-row-value: одно и то же число не должно менять вес при
   переезде из строки в плитку. nowrap потому, что самая длинная строка рекорда
   укладывается в 141px мобильной плитки только целиком. */
.app-trained-value {
  font-size: 1.125rem;
  white-space: nowrap;
}

/* Тренировки есть, а рекорда нет: подходы записаны с нулевой метрикой либо
   единицу упражнения меняли. Прочерк — принятая в проекте форма «значения нет». */
.app-trained-value.is-empty { color: var(--app-text-muted); }

/* Подвал: «сколько» слева, «когда» справа. Разделителя-точки нет намеренно — на
   375px строка «в 21 тренировке · 28 авг» не влезает, а перенос оставил бы
   висящую точку. flex-wrap — предохранитель для «в 100 тренировках». */
.app-trained-meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  column-gap: var(--app-space-2);
  /* Подпись прижата к низу: плитки в ряду одной высоты (грид растягивает), и без
     этого у короткого названия внутри зияла бы пустая строка. Резервировать под
     название две строки было хуже — дыра появлялась у всех, кроме самых длинных. */
  margin-top: auto;
  font-size: 0.75rem;
  color: var(--app-text-secondary);
}

.app-trained-date { color: var(--app-text-muted); }

/* ---------- Мои замеры ---------- */

.app-body-actions {
  margin-top: 0;
  margin-bottom: var(--app-space-4);
}
.app-body-title { margin-top: var(--app-space-6); }
.app-body-own { margin-top: var(--app-space-4); }
.app-body-chart { margin-bottom: var(--app-space-4); }

/* Строка замера — кнопка правки (модалка с днём и значением): обвязку кнопки
   снимаем, строка выглядит как соседние ссылки. */
.app-body-entry { text-align: left; }

/* ---------- Страница упражнения ---------- */

/* Ритм между блоками задаёт колонка, а не поля блоков: раньше у карточки
   графика своего отступа не было вовсе, и на 1200px она вставала вплотную к
   настройкам, а на телефоне — к заголовку «По местам». Блоки меняют порядок
   (order на 1200px), и поле «снизу» у одного из них всегда оказывалось не там. */
.app-exercise {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-4);
}

.app-exercise > .app-screen-head,
.app-by-location .app-rows,
.app-machines .app-rows { margin-bottom: 0; }

/* «Тренажёры» — сворачиваемая секция: заголовок и есть summary, шеврон
   поворачивается вместо системного треугольника. */
.app-machines-summary {
  display: flex;
  align-items: center;
  gap: var(--app-space-2);
  min-height: var(--app-tap-target);
  margin-bottom: 0;
  cursor: pointer;
  list-style: none;
}
.app-machines-summary::-webkit-details-marker { display: none; }
.app-machines-chevron { transition: var(--app-transition-ui); }
.app-machines details[open] .app-machines-chevron { transform: rotate(180deg); }
.app-machine-line {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.app-machine-line svg,
.app-note-btn svg { flex: 0 0 auto; }

/* Настройки — одной карточкой на всех ширинах. На телефоне блоки стояли без
   подложки, но с её отступами: текст съезжал на 20px от вертикали страницы,
   а между блоками зияло по 44px пустоты. Внутри карточки блоки разделены
   линией, как строки списка. */
.app-settings-row {
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
}

.app-settings-row > .app-profile-block { margin-bottom: 0; }

.app-settings-row > .app-profile-block + .app-profile-block {
  border-top: 1px solid var(--app-line);
}

/* Блок без чипов — значение только для чтения (у упражнений из общего
   справочника): строкой «название — значение», как в настройках телефона.
   Столбиком три таких блока занимали полэкрана и уводили график за кромку. */
.app-settings-row > .app-profile-block:not(:has(.app-chips)) {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--app-space-3);
}

.app-settings-row > .app-profile-block:not(:has(.app-chips)) > .app-profile-block-title {
  margin-bottom: 0;
}

/* Поле «свой»/«своя» размером с пару чипов: во всю ширину карточки оно
   читалось как отдельная форма, а на 1200px ещё и задавало ширину колонки. */
.app-settings-row .app-group-input { width: 9rem; }

/* Настройки упражнения на телефоне сворачиваются (см. _exercise_detail_body):
   кнопка — заголовок секции со строкой текущих значений, как «Тренажёры». */
.app-settings-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--app-space-3);
  width: 100%;
  min-height: var(--app-tap-target);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
}

.app-settings-toggle-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.app-settings-summary {
  overflow: hidden;
  color: var(--app-text-secondary);
  font-size: 0.875rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Заголовок с шевроном — тот же, что у «Тренажёров»; высоту тач-таргета даёт
   кнопка целиком, а не строка заголовка. */
.app-settings-toggle .app-machines-summary { min-height: 0; }
.app-settings.is-open .app-settings-toggle { margin-bottom: var(--app-space-2); }
.app-settings.is-open .app-machines-chevron { transform: rotate(180deg); }
.app-settings:not(.is-open) > .app-settings-row { display: none; }

/* ---------- Дашборд ---------- */

.app-dash-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

.app-dash {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* minmax(0, …): canvas и длинные названия иначе распирают колонку */
.app-dash-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

.app-stat { padding: var(--app-space-4) var(--app-card-inset); }
.app-stat.is-desktop { display: none; }

.app-stat-label {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--app-text-secondary);
}

.app-stat-value {
  margin: 0.125rem 0;
  font-size: 2rem;
  line-height: 1.2;
}

.app-stat-unit { font-size: 1rem; }

.app-stat-sub {
  font-size: 0.875rem;
  color: var(--app-text-secondary);
}

.app-stat-delta {
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--app-success);
}

.app-stat-delta.is-down { color: var(--app-text-secondary); }
.app-stat-delta.is-flat { color: var(--app-text-muted); }

/* Части плитки и таблицы «Последние», которые есть только на ПК (блок 768):
   иконка, строка пояснения, бейдж динамики, ячейки и шапка таблицы. Обёртки
   тела плитки на телефоне не образуют боксов — плитка прежняя. */
.app-stat-body,
.app-stat-text { display: contents; }

.app-stat-icon,
.app-stat-foot,
.app-stat-badge,
.app-latest-cell,
.app-recent-head { display: none; }

/* Карточки «Цель на неделю» и «Статистика» — только ПК (блок 768) */
.app-goal,
.app-stats { display: none; }

/* Текст шапки встаёт на общий отступ карточки (у шапки и легенды свои 4px),
   а полотно графика — на 4px шире: подписям осей это место нужнее. */
.app-chart-card {
  padding: var(--app-space-5) calc(var(--app-card-inset) - var(--app-space-1)) var(--app-space-3);
}

.app-chart-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0 0.25rem;
}

.app-chart-title {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-chart-meta { font-size: 0.875rem; }

.app-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin-top: 0.375rem;
  padding: 0 0.25rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--app-text-secondary);
}

.app-chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* Высоту canvas задаёт обёртка: maintainAspectRatio у графиков выключен */
.app-chart-body {
  position: relative;
  height: 260px;
  margin-top: 0.5rem;
}

/* Основная колонка: график, панель недели, «Подготовлено» и «Последние». Ритм —
   зазором колонки, как у .app-dash: пустая панель недели (:empty) не оставляет
   дыры. */
.app-dash-main {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.app-week-detail {
  /* Прокрутка htmx show:top не должна увозить панель под липкую шапку */
  scroll-margin-top: calc(var(--app-header-height) + 0.5rem);
}

.app-week-detail:empty { display: none; }
.app-week-detail-card { padding: var(--app-space-4) var(--app-card-inset); }

.app-week-detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.app-week-detail-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: var(--app-text-secondary);
}

.app-dash-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}

.app-dash-side {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.app-latest-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.875rem var(--app-card-inset);
  margin-bottom: 0.75rem;
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
  color: var(--app-text);
  text-decoration: none;
}

.app-latest-row:last-child { margin-bottom: 0; }

.app-latest-text {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

.app-latest-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-latest-meta {
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

/* Рекорды: на мобильном — сетка плиток */
.app-records-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

.app-records-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: var(--app-space-4) var(--app-card-inset);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
  color: var(--app-text);
  text-decoration: none;
}

.app-records-label {
  font-size: 0.9375rem;
  color: var(--app-text-secondary);
  overflow-wrap: anywhere;
}

.app-records-value { font-size: 1.375rem; }

.app-records-sub {
  font-size: 0.875rem;
  color: var(--app-text-muted);
}

/* Карточка-прожектор топ-упражнения — только десктоп */
.app-spotlight {
  display: none;
  padding: var(--app-space-5) var(--app-card-inset);
  color: var(--app-text);
  text-decoration: none;
}

.app-spotlight-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.app-spotlight-name {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 800;
}

.app-spotlight-body {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.75rem;
}

.app-spotlight-record { font-size: 1.75rem; }

.app-spotlight-spark {
  position: relative;
  flex: 1 1 auto;
  max-width: 55%;
  height: 64px;
}

.app-exercise-link { color: var(--app-text); text-decoration: none; }

/* ---------- Профиль и справочники ---------- */

.app-profile-user {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: var(--app-card-inset);
  margin-bottom: 1.5rem;
}

.app-avatar-lg {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  font-size: 1.5rem;
}

.app-profile-email {
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

/* Акцент как текст — только --app-accent-text: лайм на светлой карточке нечитаем */
.app-profile-verified {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--app-accent-text);
  font-size: 0.9375rem;
  font-weight: 700;
}

.app-profile-block {
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 0.75rem;
}

.app-profile-block-title {
  margin-bottom: 0.625rem;
  font-size: 1.0625rem;
  font-weight: 800;
}

.app-theme-choice { flex-wrap: nowrap; }

.app-theme-choice .app-chip {
  flex: 1 1 0;
  justify-content: center;
  padding: 0.5rem 0.75rem;
}

/* В макете выбранный сегмент залит акцентом, а не обведён им, как чипы спорта */
.app-theme-choice .app-chip-input:checked + .app-chip {
  background-color: var(--app-accent);
  border-color: var(--app-accent);
  color: var(--app-on-accent);
  box-shadow: none;
}

.app-rows {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

/* Форма выхода не должна ломать колонку строк */
.app-rows form { display: contents; }

/* «Мои тренажёры»: производитель и его модели одной группой; модели — с
   отступом, чтобы было видно, чьи они. Группа — ячейка доски на ПК. */
.app-machine-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.app-row.app-machine-model-row { width: auto; margin-left: var(--app-space-6); }
.app-machine-add { align-self: flex-start; margin-left: var(--app-space-6); }

.app-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 56px;
  padding: 0.875rem var(--app-card-inset);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
  color: var(--app-text);
  font-weight: 700;
  text-align: left;
  text-decoration: none;
}

.app-row-icon {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--app-text-secondary);
}

/* Гантель-логотип рисуется 24px, остальные иконки строк — 20px, и название
   «Моих упражнений» стояло на 4px правее соседей. Размер задаёт колонка. */
.app-row-icon svg {
  width: 20px;
  height: 20px;
}

.app-row-text {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  flex: 1 1 auto;
  min-width: 0;
}

.app-row-link {
  color: var(--app-text);
  text-decoration: none;
}

.app-row-name {
  font-weight: 800;
  letter-spacing: -0.01em;
  /* Тот же предохранитель, что у .app-queue-name: строка справочника переносит
     длинное имя, а не выталкивает рекорд за край. */
  overflow-wrap: anywhere;
}

.app-row-meta {
  color: var(--app-text-secondary);
  font-size: 0.875rem;
  font-weight: 400;
}

.app-row-value {
  flex: 0 0 auto;
  font-size: 1.125rem;
}

.app-row-count {
  flex: 0 0 auto;
  color: var(--app-text-secondary);
  font-weight: 400;
}

/* Число того, что ждёт действия (у «Админки»: новые обращения и заявки), —
   акцентной плашкой, а не цифрой: серым «3» оно читалось как количество
   записей рядом с «Мои места 2». */
.app-count-badge {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 var(--app-space-2);
  border-radius: var(--app-radius-pill);
  background-color: var(--app-accent);
  color: var(--app-on-accent);
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
}

.app-row.is-danger,
.app-row.is-danger .app-row-icon { color: var(--app-danger); }

.app-row-delete {
  flex: 0 0 auto;
  color: var(--app-text-muted);
}

/* Название места открывает переименование. Кнопка носит .app-row-text, как
   ссылка плотной строки каталога, поэтому здесь остаётся только снять обвязку
   кнопки. Карандаш внутри названия — встроенная подсказка (приём .app-note-btn),
   а не третья кнопка 44px: в строку на 375px она не помещалась. */
.app-location-edit {
  min-height: var(--app-tap-target);
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--app-text);
  text-align: left;
}

/* Карандаш идёт за названием и не должен приклеиваться к последней букве. */
.app-location-edit .app-note-icon,
.app-title-edit .app-note-icon { margin-left: 0.375rem; }

/* Заголовок своего упражнения — кнопка переименования: тот же приём, что у
   строки места. Шрифт и размер наследуются от .app-page-title, поэтому кнопка
   не отличается от обычного заголовка ничем, кроме карандаша. Карандаш в
   потоке текста, а не отдельной иконкой справа: у длинного названия он тогда
   перенесётся вместе с последним словом, а не повиснет в пустоте. */
.app-title-edit {
  padding: 0;
  border: 0;
  background-color: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
}

/* Звезда «место по умолчанию»: приглушённая у обычного места, акцентная у
   выбранного. Заливку переключает атрибут fill в разметке — иначе пустой и
   залитый контур пришлось бы держать двумя SVG. */
.app-star {
  flex: 0 0 auto;
  color: var(--app-text-muted);
}

/* --app-accent-text, а не --app-accent: сам лайм на белой карточке нечитаем,
   и токен для «акцент как цвет на фоне» в проекте уже есть. */
.app-star.is-active { color: var(--app-accent-text); }

.app-rest-value {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
}

/* ---------- Плотный список: одна карточка, строки внутри ----------

   Вариант скоупнут контейнером: сам .app-row живёт ещё в профиле, «Моих видах
   спорта» и base.html, и трогать его нельзя — там строки как раз должны быть
   отдельными карточками.

   Строка — сетка, а не flex: тогда рекорды выстраиваются в общую колонку и
   правый край перестаёт выглядеть рваным на списке, где рекорд есть у пары
   упражнений из двадцати пяти. */
.app-rows-dense {
  gap: 0;
  margin-bottom: var(--app-space-4);
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-card);
  background-color: var(--app-card);
  box-shadow: var(--app-shadow-card);
  overflow: hidden;
}

.app-rows-dense .app-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--app-space-3);
  /* Тап-таргет остаётся 44px по правилу CLAUDE.md, но карточной обвязки нет. */
  min-height: 44px;
  padding: var(--app-space-2) var(--app-space-4);
  border: 0;
  border-radius: 0;
  background-color: transparent;
  box-shadow: none;
}

/* Разделитель между строками, а не рамка вокруг каждой */
.app-rows-dense .app-row + .app-row {
  border-top: 1px solid var(--app-line);
}

/* Subgrid здесь пробовали и отказались: общая колонка рекорда резервирует
   ширину во всех строках, и на 375px длинные названия («Разведение рук с
   гантелями») переезжали на две строки ради выравнивания двух чисел из
   двадцати пяти. Название важнее — колонка рекорда остаётся по строке. */

/* Заголовок группы мышц стал третьим уровнем: над ним теперь стоит «Весь
   справочник». Микро-подпись — та же типографика, что у .app-stepper-label и
   .app-rest-label, поэтому нового типографического стиля не появляется, а
   справочник звучит тише блока с плитками. */
.app-group-title {
  margin-top: var(--app-space-4);
  margin-bottom: var(--app-space-2);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--app-text-muted);
}

/* Первый заголовок справочника — без отступа сверху. Через обёртку группы,
   а не :first-of-type: заголовок первый в своей обёртке у каждой группы. */
.app-catalog-group:first-child > .app-group-title { margin-top: 0; }

/* Группа справочника (заголовок + строки): на телефоне бокса не образует,
   на ПК — неразрывный блок в колонках справочника. */
.app-catalog-group { display: contents; }

.app-search { margin-bottom: 1rem; }

.app-profile-version {
  margin-top: 1.5rem;
  font-size: 0.875rem;
  text-align: center;
}

/* ---------- Что нового ---------- */

.app-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.625rem;
  border: 1px solid var(--app-line);
  border-radius: var(--app-radius-pill);
  color: var(--app-text-secondary);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* «Новое» — метка непрочитанной новости, а не тип: гаснет по нажатию на
   карточку. Это кнопка (до неё можно дойти с клавиатуры), поэтому снимаем
   обвязку кнопки: фон и высоту строки. */
.app-badge-new {
  border-color: var(--app-accent-text);
  background-color: transparent;
  color: var(--app-accent-text);
  line-height: inherit;
}

.app-badge-fix {
  border-color: var(--app-line);
  color: var(--app-text-secondary);
}

.app-news {
  padding: var(--app-space-4) var(--app-card-inset);
  margin-bottom: 0.75rem;
}

/* Непрочитанная новость кликабельна целиком: нажатие гасит «Новое». */
.app-news.is-new { cursor: pointer; }

/* Обёртка ленты новостей: на телефоне бокса не образует, на ПК — доска. */
.app-news-list { display: contents; }

.app-news-head {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 0.5rem;
}

.app-news-date {
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

/* 8px до текста, как в макете: с 4px вторая строка длинного заголовка
   слипалась с первой строкой новости. */
.app-news-title {
  margin: 0 0 var(--app-space-2);
  font-size: 1.125rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.app-news-body {
  margin: 0;
  color: var(--app-text-secondary);
}

/* ---------- Место тренировки ----------

   Одна строка «булавка + название» на четырёх экранах: карточка истории, итог,
   голова живого режима, строка профиля. font-weight: inherit — вес решает
   контейнер: в карточке истории это 400, в .app-row — 700, и класс не спорит
   ни с одним из них. */
.app-place,
.app-plan-day,
.app-when-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  /* Без этого имя не даёт ужаться флекс-родителю, и голова живого режима
     разъезжалась бы на длинном названии зала. */
  min-width: 0;
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
  font-weight: inherit;
}

.app-place svg,
.app-plan-day svg,
.app-when-btn svg {
  flex: 0 0 auto;
  color: var(--app-text-muted);
}

/* Обрезка живёт на тексте, а не на контейнере: у inline-flex ellipsis не
   работает. white-space здесь НЕ задаётся — на телефоне длинное название должно
   переноситься; nowrap включают те места, где строка обязана быть одной. */
.app-place-name,
.app-plan-day-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* «Места нет» — приглушённый и жирный: это приглашение действовать, а не
   значение. Дословно приём .app-note-btn.is-empty. */
.app-place.is-empty,
.app-plan-day.is-empty {
  color: var(--app-text-muted);
  font-weight: 700;
}

/* Кнопка правки места: тихая текстовая кнопка без обвязки, как .app-note-btn.
   Тап-таргет нужен в зале; на 768px он снимается — там курсор.

   .app-plan-day — плановый день черновика, .app-when-btn — дата и длительность
   записанной: та же роль «иконка плюс текст в мета-строке», поэтому они идут
   теми же правилами, а не своей копией. Имена всё же отдельные: класс со
   словом «place» на дате читался бы как ошибка. */
.app-place-btn,
.app-plan-day-btn,
.app-when-btn {
  min-height: var(--app-tap-target);
  padding: 0;
  border: 0;
  background-color: transparent;
  font-weight: 700;
  text-align: left;
  white-space: nowrap;
}

/* Голова живого режима и подзаголовок итога: статус (или дата) и место — одна
   переносящаяся строка. На 375px место уезжает на вторую строку, с 768px
   помещается рядом. */
.app-live-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--app-space-3);
}

/* Текущий выбор в списке модалки. Галочка, а не рамка: у .app-pick-row рамки
   нет по построению. Цвет — --app-accent-text: лайм на карточке нечитаем
   в светлой теме. */
.app-pick-check {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--app-accent-text);
}

/* ---------- Экспорт и импорт ---------- */

/* Две группы страницы — история и справочник. Ритм задаёт группа: заголовок,
   панели и отчёт стоят через один шаг, а между группами — заметный зазор, чтобы
   было видно, где кончается одна книга и начинается другая. */
.app-transfer-group {
  display: flex;
  flex-direction: column;
  gap: var(--app-space-3);
}

.app-transfer-group > .app-section-title { margin-bottom: 0; }

.app-transfer-group + .app-transfer-group { margin-top: var(--app-space-8); }

/* Обёртка двух групп «Экспорта и импорта»: на телефоне бокса не образует,
   на ПК — две колонки (блок 768). */
.app-transfer { display: contents; }

/* Панель, к которой прокручивает якорь после загрузки справочника: шапка сайта
   липкая и накрыла бы её заголовок. */
.app-transfer-anchor {
  scroll-margin-top: calc(var(--app-header-height) + var(--app-space-3));
}

.app-import-summary {
  margin: 0;
  padding-left: 0;
  list-style: none;
  color: var(--app-text);
  font-size: 0.9375rem;
}

.app-import-list {
  margin: 0;
  padding-left: var(--app-space-5);
  color: var(--app-text-secondary);
  font-size: 0.9375rem;
}

.app-import-summary li + li,
.app-import-list li + li {
  margin-top: var(--app-space-1);
}

/* Незагруженные строки — тем же приёмом, что у сообщений: полоса слева вместо
   заливки, чтобы список ошибок не выглядел тревожнее, чем он есть. */
.app-import-errors {
  padding-left: var(--app-space-4);
  border-left: 3px solid var(--app-text-muted);
  list-style: none;
}

/* ---------- Десктоп ---------- */

/* ---------- Отделка: наведение, фокус, переходы ----------

   Все hover-состояния собраны здесь. Раньше они были размазаны по компонентам,
   и при добавлении новых порядок в каскаде начинал решать судьбу. Декларации
   перенесены дословно — изменилось только место и то, что они теперь внутри
   @media (hover: hover).

   Анимируем только цвет, фон, рамку, filter, тень и transform. НИКОГДА
   width/height/padding/grid-template-* — они дёргают раскладку, и на длинном
   списке истории это заметно. Единственный переход по width в проекте —
   .app-progress-bar: это индикатор таймера отдыха, а не отделка. */

.app-card,
.btn,
.app-chip,
.app-icon-btn,
.app-nav a,
.app-link,
.app-row,
.app-row-delete,
.app-star,
.app-location-edit,
.app-title-edit,
.app-place-btn,
.app-when-btn,
.app-cardio-part,
.app-workout,
.app-latest-row,
.app-news,
.app-records-item,
.app-spotlight,
.app-queue-row,
.app-pick-row,
.app-live-add-tile,
.app-trained-item,
.app-stepper-btn,
.app-note-btn,
.app-live-link,
.app-rest-skip,
.app-rest-adjust {
  transition: var(--app-transition-ui);
}

/* Наведение — только там, где есть настоящий курсор. На тапе :hover залипает до
   следующего тапа, и строка остаётся подсвеченной уже после перехода. */
@media (hover: hover) and (pointer: fine) {
  .app-side-link:not(.is-active):hover {
    background-color: var(--app-raised);
    color: var(--app-text);
  }

  .app-tab:not(.is-active):hover { color: var(--app-text); }

  .app-news.is-new:hover { border-color: var(--app-text-muted); }

  .app-catalog-table tbody tr:hover > td { background-color: var(--app-raised); }
  .app-catalog-sort:hover { color: var(--app-text); }

  /* Карточки и строки, кликабельные целиком: рамка, подъём и тень */
  .app-workout-link:hover,
  .app-row:hover,
  .app-latest-row:hover,
  a.app-records-item:hover,
  .app-spotlight:hover,
  .app-queue-row:hover,
  .app-live-add-tile:hover,
  .app-trained-item:hover {
    border-color: var(--app-text-muted);
    transform: translateY(var(--app-lift));
    box-shadow: var(--app-shadow-hover);
  }

  /* Нажатие возвращает элемент на место — иначе клик не ощущается */
  .app-workout-link:active,
  .app-row:active,
  .app-latest-row:active,
  a.app-records-item:active,
  .app-spotlight:active,
  .app-queue-row:active,
  .app-live-add-tile:active,
  .app-trained-item:active {
    transform: translateY(0);
    box-shadow: var(--app-shadow-card);
  }

  /* Поверхности-кнопки: подсветка яркостью, чтобы не изобретать цвет вне макетов */
  .app-chip:hover,
  .app-stepper-btn:hover,
  .app-rest-adjust:hover,
  .btn-quiet:hover {
    filter: var(--app-hover-filter);
  }

  /* Тихие текстовые кнопки: усиление контраста */
  .app-note-btn:hover,
  .app-place-btn:hover,
  .app-when-btn:hover,
  .app-live-link:hover,
  .app-rest-skip:hover,
  .app-nav a:hover {
    color: var(--app-text);
  }

  .app-icon-btn:hover,
  .app-cardio-part:hover,
  .app-pick-row:hover {
    background-color: var(--app-raised);
    color: var(--app-text);
  }

  .app-link:hover {
    color: var(--app-text);
    text-decoration: underline;
  }

  .app-link-accent:hover { color: var(--app-accent-text); }
  .app-exercise-link:hover { text-decoration: underline; }
  .app-row-delete:hover { color: var(--app-danger); }
  /* Перебивает .app-icon-btn:hover выше: у звезды меняется цвет, а не фон. */
  .app-star:hover { color: var(--app-accent-text); }
  /* Карандаш зажигается вместе с названием — подсказка, что строка правится. */
  .app-location-edit:hover .app-note-icon,
  .app-title-edit:hover .app-note-icon { color: var(--app-accent-text); }

  .btn-danger-quiet:hover {
    border-color: var(--app-danger);
    color: var(--app-danger);
  }

  .btn-accent:hover {
    background-color: var(--app-accent);
    border-color: var(--app-accent);
    color: var(--app-on-accent);
    filter: brightness(0.94);
  }
}

/* Видимый фокус с клавиатуры. :where() даёт нулевую специфичность, поэтому любое
   компонентное правило его перебьёт. Текстовые поля исключены — у них свой ринг
   (.form-control:focus). [tabindex] нужен ради .app-stepper-value: у ручного
   ввода значения подхода видимого фокуса не было вовсе. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: var(--app-focus-outline);
  outline-offset: var(--app-focus-offset);
}

/* Внутри прокручиваемого списка обводку снаружи срезает край контейнера. */
.app-pick-row:focus-visible { outline-offset: -2px; }

@media (min-width: 768px) {
  /* Ширины компонентов здесь больше не задаются: их даёт мера страницы
     (--app-measure на .app-main). Остаются только ширины самих элементов —
     модалка, карточка входа, ряд кнопок формы: это не мера страницы. */
  .app-form-actions { max-width: var(--app-measure-narrow); }
  .app-rest-time { font-size: 4.5rem; }
  /* Дашборд: четыре плитки в ряд, график выше */
  .app-dash-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .app-chart-body { height: 320px; }
  /* Число в степпере подхода на ПК — от ширины самой панели, а не экрана.
     На телефоне 5vw годится: панель растёт вместе с экраном. На ПК панель
     стоит в колонке (живой экран, экран правки) и бывает ~280px при любой
     ширине окна, а 5vw там уже упёрлось в максимум — и «83,75» выталкивало
     соседний степпер за рамку. cqi — проценты ширины панели. */
  .app-live-current { container-type: inline-size; }
  .app-live-current .app-stepper-value { font-size: clamp(1.125rem, 6.5cqi, 1.5rem); }
  /* Окно на ПК — диалог по центру шириной с форму, а не шторка телефона.
     Потолок по высоте и своя прокрутка: длинный список не уводит заголовок и
     кнопку за край экрана. Широкий вариант — для окон с двумя колонками. */
  .app-modal-backdrop { align-items: center; }
  .app-modal {
    max-width: var(--app-measure-form);
    max-height: calc(100dvh - 2 * var(--app-space-8));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .app-modal-wide { max-width: var(--app-measure-wide); }
  /* Ряд кнопок формы ограничен узкой мерой (правило выше) — в окне он должен
     быть шириной с поля, иначе кнопка короче инпута над ней. */
  .app-modal .app-form-actions { max-width: none; }

  /* Окно новой тренировки: слева начатое и подготовленное, справа новая.
     Первые дети колонок без верхнего отступа — иначе колонки стартуют
     с разной высоты. */
  .app-modal-wide .app-modal-cols {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--app-space-6);
    align-items: start;
  }
  .app-modal-wide .app-modal-col { display: block; min-width: 0; }
  .app-modal-wide .app-modal-col > :first-child { margin-top: 0; }

  /* Выбор упражнения: слева найденное со своей прокруткой, справа создание.
     Пока создавать нечего, найденное занимает обе колонки и раскладывается
     в две — виден вдвое больший кусок справочника. Если найденного нет,
     форма создания встаёт во всю ширину. */
  .app-picker {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--app-space-2) var(--app-space-6);
    align-items: start;
    max-height: none;
    overflow: visible;
  }
  .app-picker-list {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-1);
    min-width: 0;
    max-height: 50vh;
    overflow-y: auto;
  }
  /* Форма создания — колонка целиком. Своё правило .app-pick-list form
     растворяет формы в строках списка (так живут виды спорта в окне старта),
     и без display: block подписи и чипы разошлись бы по разным колонкам. */
  .app-picker > form { display: block; min-width: 0; }
  .app-picker:not(:has(> form)) > .app-picker-list {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--app-space-1) var(--app-space-6);
    align-content: start;
  }
  .app-picker:not(:has(> .app-picker-list > *)) > form { grid-column: 1 / -1; }
  /* Здесь курсор, а не палец: 44px в голове экрана только раздували бы её, а
     место — не степпер веса. Тот же довод, что у .app-settings-row .app-chip.
     Благодаря этому десктопная голова живого режима и итога не двигается. */
  .app-place-btn,
  .app-when-btn { min-height: 0; }
  /* На телефоне «Повторить» занимает всю ширину — там это верно. На десктопе
     растянутая на пол-карточки кнопка рядом с компактной «Открыть» выглядит
     случайной. */
  .app-workout-actions form { flex: 0 0 auto; }
  /* Прокрутка чипов мышью бесполезна и прячет часть фильтров за краем */
  .app-filters {
    flex-wrap: wrap;
    overflow-x: visible;
    margin-inline: 0;
    padding-inline: 0;
    padding-bottom: 0;
  }
  /* Здесь ряды переносятся, и шаг между осями (8px) совпадал с шагом между
     перенесёнными строками одной оси: группы мышц сливались со снарядами. */
  .app-filter-axes .app-filters { margin-bottom: var(--app-space-4); }
  .app-empty {
    max-width: var(--app-measure-form);
    margin-inline: auto;
    padding: var(--app-space-12) var(--app-space-8);
  }

  /* Компактная подсказка живёт внутри колонки, а не в центре страницы: пустое
     состояние всей страницы (мера 34rem, отступ 3rem) заняло бы здесь первый
     экран. Порядок важен — .app-empty выше имеет ту же специфичность и без
     повтора здесь перебивал .app-empty-slim: заглушка графика на десктопе
     становилась узким блоком по центру с отступом в 3rem. */
  .app-empty-slim {
    max-width: none;
    margin-inline: 0;
    padding: var(--app-space-3) var(--app-card-inset);
    text-align: start;
  }

  /* На широком экране карточка входа стоит по центру, а не прижата к верху */
  .app-auth { justify-content: center; padding-bottom: 4rem; }
  .app-auth-brand { margin-top: 0; }
  /* Шапка ограничена той же мерой, что и контент: без этого на мониторе шире
     1440px лого уезжает к краю окна, а страница стоит по центру. */
  .app-header {
    max-width: var(--app-measure-page);
    margin-inline: auto;
    /* Безопасную зону сохраняем и здесь: iPhone в standalone-ландшафте шире
       768px, и padding-block затёр бы её вместе с отступом. */
    padding-block-start: calc(var(--app-header-pad) + env(safe-area-inset-top));
    padding-block-end: var(--app-header-pad);
  }
  .app-nav { display: flex; }
  .app-header-cta { display: inline-flex; }
  .app-bottom-nav { display: none; }
  .app-main {
    max-width: var(--app-measure-page);
    margin-inline: auto;
    padding: var(--app-space-4) var(--app-gutter) var(--app-space-12);
    /* Мера содержимого по умолчанию: во всю колонку страницы. Экраны, которым
       нужна колонка поуже, ставят свой модификатор через {% block main_class %}. */
    --app-measure: none;
  }
  /* Единственное место, где решается «во всю ширину или колонкой по центру».
     Центрируются и заголовки — поэтому h2 профиля больше не разъезжаются
     с блоками, которые они подписывают. :where обнуляет специфичность: мера
     страницы — значение по умолчанию, и собственное ограничение компонента
     (.app-form-actions, .app-empty) побеждает при любом порядке в файле. */
  :where(.app-main) > * {
    max-width: var(--app-measure);
    margin-inline: auto;
  }
  /* Страницы-диалоги: подтверждения, выход, смена пароля, ошибки и служебные
     страницы allauth. На ПК — карточка шириной с форму по центру экрана, и по
     горизонтали, и по вертикали, как окно: узкая колонка у верхнего края
     широкой страницы выглядела потерянной. width: 100% — авто-поля во
     flex-колонке иначе сжали бы заголовок до ширины текста. */
  .app-main-dialog {
    --app-measure: var(--app-measure-form);
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: calc(100dvh - var(--app-header-height));
  }
  .app-main-dialog > * { width: 100%; }

  /* ---------- Каркас ПК: боковая панель (приём TailAdmin) ----------

     Сетка «панель | шапка и страница». С 768 до 1200 панель — «рельс» из одних
     иконок (подписи — в title), с 1200 — развёрнутая с подписями, и её можно
     свернуть кнопкой в шапке (shell.js ставит data-sidebar-collapsed на <html>).
     Панель липкая на всю высоту окна со своей прокруткой. */
  .app-shell {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    min-height: 100dvh;
  }
  .app-sidebar {
    grid-column: 1;
    position: sticky;
    top: 0;
    display: flex;
    flex-direction: column;
    width: var(--app-measure-rail);
    height: 100dvh;
    overflow-y: auto;
    padding: 0 var(--app-space-4) var(--app-space-6);
    border-right: 1px solid var(--app-line);
    background-color: var(--app-card);
  }
  .app-shell-main {
    grid-column: 2;
    display: block;
    min-width: 0;
  }
  .app-sidebar-label { display: none; }
  .app-sidebar-logo {
    justify-content: center;
    min-height: var(--app-header-height);
    margin-bottom: var(--app-space-2);
  }
  .app-sidebar-nav {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-1);
  }
  /* Подпись группы в «рельсе» — короткая черта: текст не помещается. Под
     логотипом черта лишняя, поэтому первая группа в рельсе не видна. */
  .app-sidebar-group {
    margin: var(--app-space-3) var(--app-space-2);
    border-top: 1px solid var(--app-line);
    color: var(--app-text-muted);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .app-sidebar-group:first-child { display: none; }
  .app-side-link {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--app-space-3);
    min-height: var(--app-tap-target);
    padding: var(--app-space-2) var(--app-space-3);
    border-radius: var(--app-radius-field);
    color: var(--app-text-secondary);
    font-weight: 600;
    text-decoration: none;
    transition: var(--app-transition-ui);
  }
  .app-side-link svg {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
  }
  .app-side-link.is-active {
    background-color: var(--app-accent-soft);
    color: var(--app-accent-text);
  }
  /* Точка «есть новое» у «Что нового». В «рельсе» подписи нет, и точка садится
     на угол иконки, как бейдж уведомления; в развёрнутой панели (1200) она
     уходит в конец строки. */
  .app-side-link { position: relative; }
  .app-side-dot {
    position: absolute;
    top: var(--app-space-2);
    right: var(--app-space-2);
  }

  /* Шапка рядом с панелью: логотип и разделы переехали в панель, вместо них —
     поиск по справочнику. Фон и линия идут во всю колонку, как у TailAdmin,
     а содержимое стоит по краю страницы: отступ добирает половину того, что
     колонка шире --app-measure-page, — ровно на столько .app-main сдвигает
     контент своим margin auto. Линия — тенью, а не рамкой: рамка прибавила бы
     пиксель к высоте, и липкие блоки (top: --app-header-height) заезжали бы
     под шапку. */
  .app-header.has-sidebar {
    max-width: none;
    padding-inline: max(
      var(--app-gutter),
      calc((100% - var(--app-measure-page)) / 2 + var(--app-gutter))
    );
    background-color: var(--app-card);
    box-shadow: inset 0 -1px 0 var(--app-line);
  }
  .app-header.has-sidebar .app-logo,
  .app-header.has-sidebar .app-nav { display: none; }
  /* Круглые кнопки шапки — с рамкой, как в TailAdmin: на фоне карточки
     прозрачная кнопка без рамки читалась бы просто иконкой. */
  .app-header.has-sidebar .app-icon-btn { border: 1px solid var(--app-line); }
  .app-header.has-sidebar .app-sidebar-toggle { border-radius: var(--app-radius-field); }
  .app-header-search {
    position: relative;
    display: flex;
    align-items: center;
    flex: 0 1 var(--app-measure-narrow);
    min-width: 0;
    color: var(--app-text-muted);
  }
  .app-header-search > svg {
    position: absolute;
    left: var(--app-space-3);
    pointer-events: none;
  }
  .app-header-search-input {
    width: 100%;
    min-height: var(--app-tap-target);
    padding-inline: calc(var(--app-space-8) + var(--app-space-2));
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-field);
    background-color: var(--app-card);
    color: var(--app-text);
    font-size: 0.9375rem;
  }
  .app-header-search-input::placeholder { color: var(--app-text-muted); }
  .app-header-search-input:focus {
    border-color: var(--app-accent-text);
    outline: none;
    box-shadow: 0 0 0 0.2rem var(--bs-focus-ring-color);
  }
  .app-header-kbd {
    position: absolute;
    right: var(--app-space-3);
    padding: 0 var(--app-space-2);
    border: 1px solid var(--app-line);
    border-radius: var(--bs-border-radius-sm);
    background-color: var(--app-raised);
    color: var(--app-text-muted);
    font: 600 0.75rem/1.6 var(--app-font-sans);
  }

  /* ---------- Форма кардио и плана: две колонки ----------

     Слева что и когда, справа сколько, место и заметка; ошибки и кнопки —
     во всю ширину под колонками. Страница ставит меру app-main-dual: поле
     шире формы на телефоне (34rem на колонку) удобнее не становится. */
  .app-main-dual { --app-measure: var(--app-measure-dual); }
  .app-form-cols {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--app-space-8);
    align-items: start;
  }
  .app-form-col { display: block; min-width: 0; }

  /* ---------- «Что нового»: лента строк ----------

     Новость — строка во всю ширину, по одной в ряд (решение 03.10.2026: доску
     плиток неудобно читать подряд): слева дата и ярлыки, справа заголовок и
     текст. Хронология читается сверху вниз, как на телефоне. Текст держит меру
     строки (--app-measure-text): во всю ширину 1600px абзац растянулся бы на
     полторы сотни знаков. Ярлык «Новое» остаётся в колонке слева — его кнопка
     и клик по строке работают как раньше. */
  .app-news-list {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-3);
  }
  .app-news-list > .app-news { margin-bottom: 0; }
  .app-news {
    display: grid;
    grid-template-columns: var(--app-measure-meta) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    grid-template-areas:
      "head title"
      "head body";
    column-gap: var(--app-space-6);
    padding-block: var(--app-space-5);
  }
  .app-news-head {
    grid-area: head;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--app-space-2);
    margin-bottom: 0;
  }
  /* Дата — первой в колонке: по ней новость ищут, ярлыки — уточнение */
  .app-news-date { order: -1; }
  .app-news-title {
    grid-area: title;
    max-width: var(--app-measure-text);
  }
  .app-news-body {
    grid-area: body;
    max-width: var(--app-measure-text);
  }

  /* ---------- Справочники и обмен данными ----------

     Строки «Моих видов спорта» и «Моих мест» — доской: на широкой странице
     столбик карточек во всю ширину читался растянутым телефоном. Своим
     классом, а не .app-rows: те же строки живут в профиле и в каталоге.
     Форма нового места — шириной с форму, слева под доской. «Экспорт и
     импорт» — история и справочник двумя колонками, кнопки по содержимому. */
  .app-rows-board {
    display: grid;
    grid-template-columns: repeat(var(--app-board-cols), minmax(0, 1fr));
    gap: var(--app-space-2) var(--app-space-4);
  }
  .app-rows-board > .app-empty { grid-column: 1 / -1; }
  .app-location-add {
    max-width: var(--app-measure-form);
    margin-inline: 0;
  }
  .app-transfer {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--app-space-8);
    align-items: start;
  }
  .app-transfer > .app-transfer-group + .app-transfer-group { margin-top: 0; }
  .app-transfer .app-btn-fill { width: auto; }

  /* ---------- Профиль ----------

     Дашборд из карточек: на 768 две колонки, карточка пользователя во всю
     строку; на 1200 — четыре колонки (ниже, в блоке 1200). align-items: start —
     иначе короткая секция растянулась бы по высоте соседней. */
  .app-profile-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-areas:
      "user user"
      "settings catalogs"
      "app account";
    gap: var(--app-space-4) var(--app-space-8);
    align-items: start;
  }
  .app-profile-user { grid-area: user; margin-bottom: 0; }
  .app-profile-settings { grid-area: settings; }
  .app-profile-catalogs { grid-area: catalogs; }
  .app-profile-app { grid-area: app; }
  .app-profile-account { grid-area: account; }

  /* ---------- Мои замеры ----------

     Кнопки — по содержимому и у левого края: «+ Замер» шириной с форму висел
     посередине пустой строки, а «+ Свой параметр» во всю ширину страницы
     выглядел главным действием. */
  .app-body-actions {
    flex-direction: row;
    margin-inline: 0;
  }
  .app-body-actions .app-btn-fill,
  .app-body-own { width: auto; }

  /* ---------- Справочник упражнений: таблица ----------

     С 768 справочник — таблица (приём TailAdmin «Basic Table»), а телефонные
     раскладки прячутся: плитки «Я тренирую», заголовки блоков и группы со своими
     заголовками. Прячем именно группы, а не .app-catalog-all: в нём живёт
     пустое состояние ({% empty %}), и при пустом поиске оно должно остаться.
     Карточка таблицы — overflow: clip, а не hidden: hidden сделал бы её
     контейнером прокрутки, и шапка таблицы перестала бы залипать. */
  .app-catalog-title,
  .app-trained-list,
  .app-catalog-group { display: none; }
  .app-catalog-table {
    display: block;
    overflow: clip;
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-card);
    background-color: var(--app-card);
    box-shadow: var(--app-shadow-card);
  }
  .app-catalog-table table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
  }
  /* Боковые отступы ячеек до 1200 — 8px: на 768 под названия иначе
     оставалось ~150px, и почти каждое переносилось. С 1200 — 12px. */
  .app-catalog-table th,
  .app-catalog-table td {
    padding: var(--app-space-3) var(--app-space-2);
    text-align: start;
    vertical-align: middle;
  }
  .app-catalog-table :is(th, td):first-child { padding-inline-start: var(--app-card-inset); }
  .app-catalog-table :is(th, td):last-child { padding-inline-end: var(--app-card-inset); }
  /* Шапка таблицы залипает под шапкой сайта: справочник длинный, и в его
     середине без неё не понять, где «Тренировок», а где рекорд. */
  .app-catalog-table thead th {
    position: sticky;
    top: var(--app-header-height);
    z-index: 1;
    border-bottom: 1px solid var(--app-line);
    background-color: var(--app-card);
    color: var(--app-text-muted);
    font-size: 0.8125rem;
    font-weight: 600;
    white-space: nowrap;
  }
  .app-catalog-sort {
    display: inline-flex;
    align-items: center;
    gap: var(--app-space-1);
    color: inherit;
    text-decoration: none;
    transition: var(--app-transition-ui);
  }
  .app-catalog-sort.is-active { color: var(--app-text); }
  /* Строка кликабельна целиком (exercise.js), разделитель — линия сверху. */
  .app-catalog-table tbody tr { cursor: pointer; }
  .app-catalog-table tbody tr + tr > td { border-top: 1px solid var(--app-line); }
  /* overflow-wrap: на 768 таблица сжимается, а не уезжает за край экрана. */
  .app-catalog-table td {
    color: var(--app-text-secondary);
    font-size: 0.9375rem;
    overflow-wrap: anywhere;
    transition: var(--app-transition-ui);
  }
  .app-catalog-table td.num { color: var(--app-text); }
  /* Прочерк «значения нет» — тихий: шрифтом цифр он выглядел бы жирной чертой
     и спорил бы с настоящими числами в колонке. */
  .app-catalog-none {
    color: var(--app-text-muted);
    font-family: var(--app-font-sans);
    font-weight: 400;
  }
  .app-catalog-table .app-row-name { font-weight: 700; }
  .app-catalog-table .app-row-meta {
    display: block;
    font-size: 0.8125rem;
  }
  /* Все колонки, кроме названия, — по содержимому: остаток ширины забирает
     название. Цифры прижаты вправо, как в любой таблице. */
  .app-catalog-table :is(
    .app-catalog-col-group,
    .app-catalog-col-workouts,
    .app-catalog-col-last,
    .app-catalog-col-record,
    .app-catalog-col-actions
  ) {
    width: 1%;
    white-space: nowrap;
  }
  .app-catalog-table :is(.app-catalog-col-workouts, .app-catalog-col-record) { text-align: end; }
  /* Кнопка удаления 44px не раздувает строку: высоту задают текстовые ячейки. */
  .app-catalog-table td.app-catalog-col-actions { padding-block: 0; }

  /* ---------- Обратная связь: форма и обращения рядом ---------- */
  .app-feedback {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--app-space-6);
    align-items: start;
  }
  .app-feedback > section + section { margin-top: 0; }

  /* ---------- Админка ----------

     Пользователи — та же таблица, что у справочника (.app-catalog-table), а
     телефонные строки прячутся. Строка таблицы здесь не кликабельна целиком:
     ссылка одна, «Изменить», — поэтому курсор обычный. Состояние системы —
     секции колонками доски. */
  .app-admin-user-rows { display: none; }
  .app-admin-users-table tbody tr { cursor: auto; }
  .app-admin-users-table .app-admin-col-fit {
    width: 1%;
    white-space: nowrap;
  }
  .app-admin-users-table .app-admin-col-num { text-align: end; }
  .app-admin-system {
    display: grid;
    grid-template-columns: repeat(var(--app-board-cols), minmax(0, 1fr));
    gap: 0 var(--app-space-6);
    align-items: start;
  }

  /* ---------- Страница упражнения (и панель каталога с 1200) ---------- */

  /* Доска карточек (история подходов упражнения): колонки — токеном,
     одним на все доски. Итог тренировки сюда намеренно НЕ входит: там список
     упражнений — последовательность, и сетка по строкам её ломает. */
  .app-board {
    display: grid;
    grid-template-columns: repeat(var(--app-board-cols), minmax(0, 1fr));
    gap: var(--app-space-4);
    align-items: start;
  }
  .app-board > * {
    max-width: none;
    margin-bottom: 0;
  }
  .app-board > .app-empty { grid-column: 1 / -1; }

  /* Настройки упражнения — сетка 2×2 по содержимому внутри карточки. Сама
     карточка (фон, рамка) задана для всех ширин выше; здесь только раскладка.
     Растянутые колонки (1fr) разъезжались по всей ширине панели с пустотами
     между блоками, а чипы шага веса всё равно переносились.

     Колонки — max-content, а не auto-fit: auto-fit при ширине «по содержимому»
     схлопывается в одну колонку, а max-content заодно гарантирует, что пять
     чипов шага веса встанут в одну строку. Пропущенный блок (у планки шага
     веса нет) пустую колонку не оставляет: max-content пустого трека — ноль.

     Блоков четыре («Снаряд» — вторая ось справочника), и колонок ровно две:
     при трёх четвёртый блок уезжал во второй ряд в одиночку, а 2×2 заполняет
     оба ряда целиком. align-items: start — ряд не растягивает блоки по своей
     высоте.

     Карточка во всю ширину колонки, а колонки внутри — по содержимому: раньше
     карточка сжималась до содержимого (align-self: start) и стояла на 50px уже
     графика над ней, и правый край экрана становился рваным. Отступ между
     рядами дают сами блоки (их padding), поэтому зазора между рядами нет:
     с ним второй ряд отъезжал на 48px от первого. */
  /* С 768 настройки — компактный ряд, сворачивать нечего: переключателя нет,
     ряд виден и у «свёрнутых» (класс состояния остаётся от телефона). */
  .app-settings-toggle { display: none; }
  .app-settings-row,
  .app-settings:not(.is-open) > .app-settings-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, max-content));
    column-gap: var(--app-space-6);
    align-items: start;
  }

  /* В сетке блоки стоят рядом, а не друг под другом: линия-разделитель списка
     здесь была бы чертой над правой колонкой. Строка «название — значение»
     тоже только для столбика: в сетке у соседнего блока заголовок над чипами,
     и оба блока ряда должны читаться одинаково. */
  .app-settings-row > .app-profile-block + .app-profile-block { border-top: 0; }

  .app-settings-row > .app-profile-block:not(:has(.app-chips)) { display: block; }

  .app-settings-row > .app-profile-block:not(:has(.app-chips)) > .app-profile-block-title {
    margin-bottom: 0.625rem;
  }

  /* Пять чипов шага веса требовали 297px при колонке в 280px и переносили «5»
     на вторую строку. Здесь курсор, а не палец, поэтому сжимаем по горизонтали:
     высота тап-таргета (44px) не меняется. */
  .app-settings-row .app-chip {
    padding-inline: var(--app-space-3);
  }

  /* График — сразу под заголовком: он и есть причина открыть упражнение, а блок
     настроек с чипами шага веса занимает почти весь первый экран. На мобильном
     порядок в разметке остаётся прежним, здесь его меняет order (.app-exercise —
     flex-колонка на всех ширинах, см. «Страница упражнения»). */
  .app-exercise > .app-exercise-head,
  .app-exercise > .app-screen-head { order: 0; }
  .app-exercise > .app-chart-card,
  .app-exercise > .app-empty { order: 1; }
  .app-exercise > .app-settings { order: 2; }
  /* Сводка по местам — над длинной историей подходов. Своей секцией, а не
     заголовком и списком по отдельности: без order они уехали бы к шапке, а
     заголовок остался бы рядом с «Историей подходов». */
  .app-exercise > .app-by-location { order: 3; }
  .app-exercise > .app-machines { order: 4; }
  .app-exercise > .app-exercise-history { order: 5; }

  /* График в панели крупнее: он и есть причина открыть упражнение */
  .app-exercise .app-chart-body { height: 360px; }


  /* ---------- Дашборд на средних экранах ----------

     Плитки, график, «Подготовлено» и таблица последних — во всю ширину; под
     ними прожектор и рекорды. Карточки со строками, как в макете, — с 768, а не
     с 1200: на планшете и узком ноутбуке стопка телефонных плиток во всю ширину
     и делала дашборд «полумобильным». На 1200 боковая часть становится колонкой
     справа от графика. */

  /* Плитка сводки — приём TailAdmin: квадрат с иконкой, ниже подпись, число и
     строка пояснения. Бейдж динамики — напротив иконки, а не у числа, как у
     TailAdmin: плиток четыре в ряд, и рядом с бейджем «8280 кг» переносилось
     на две строки. Тело плитки (.app-stat-body) не образует бокса и на ПК —
     иначе бейдж нельзя было бы поднять в верхний ряд сетки. Динамику на ПК
     несёт бейдж, поэтому строка .app-stat-delta здесь не нужна.
     .is-desktop — ради специфичности: базовое .app-stat.is-desktop прячет
     плитки тоннажа и дистанции на телефоне. */
  .app-stat,
  .app-stat.is-desktop {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "icon badge"
      "text text";
    align-items: center;
    padding: var(--app-space-5) var(--app-card-inset);
  }
  .app-stat-icon {
    grid-area: icon;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--app-space-12);
    height: var(--app-space-12);
    border-radius: var(--app-radius-field);
    background-color: var(--app-raised);
    color: var(--app-text);
  }
  .app-stat-text {
    grid-area: text;
    display: block;
    min-width: 0;
    margin-top: var(--app-space-5);
  }
  .app-stat-delta { display: none; }
  .app-stat-foot { display: block; }
  /* Спад не красим в «ошибку»: неделя отдыха — не провал, и строка динамики
     на телефоне его тоже не красит. Зелёный — только у роста. */
  .app-stat-badge {
    grid-area: badge;
    justify-self: end;
    display: inline-flex;
    align-items: center;
    gap: var(--app-space-1);
    padding: 0.125rem var(--app-space-2);
    border-radius: var(--app-radius-pill);
    background-color: var(--app-raised);
    color: var(--app-text-muted);
    font-size: 0.8125rem;
    font-weight: 700;
    white-space: nowrap;
  }
  .app-stat-badge.is-up {
    background-color: var(--app-success-soft);
    color: var(--app-success);
  }
  .app-stat-badge.is-down { color: var(--app-text-secondary); }

  /* «Последние тренировки» — таблица (приём TailAdmin): шапка с линиями сверху
     и снизу, строки через линию. Колонки задаёт список, строки берут их через
     subgrid, как в истории: ширины считаются по самой широкой ячейке, без
     магических чисел. Старый браузер без subgrid остаётся на строках. */
  @supports (grid-template-columns: subgrid) {
    .app-recent .app-latest-list {
      display: grid;
      grid-template-columns:
        max-content             /* точка вида спорта */
        minmax(0, 1fr)          /* тренировка */
        max-content             /* дата */
        max-content             /* время */
        max-content             /* нагрузка */
        minmax(0, max-content)  /* место: обрезается первым */
        max-content;            /* тип */
      column-gap: var(--app-space-5);
    }
    .app-recent-head,
    .app-recent .app-latest-row {
      display: grid;
      grid-column: 1 / -1;
      grid-template-columns: subgrid;
      align-items: center;
    }
    .app-recent-head {
      padding-block: var(--app-space-3);
      border-block: 1px solid var(--app-line);
      color: var(--app-text-muted);
      font-size: 0.8125rem;
      font-weight: 600;
    }
    .app-recent .app-latest-row {
      padding-block: var(--app-space-3);
      border-bottom: 1px solid var(--app-line);
      border-radius: 0;
    }
    .app-recent .app-latest-row:last-child { border-bottom: 0; }
    .app-recent .app-latest-text { display: block; }
    .app-recent .app-latest-meta { display: none; }
    .app-latest-cell {
      display: block;
      color: var(--app-text-secondary);
      font-size: 0.875rem;
      white-space: nowrap;
    }
    .app-latest-cell.num {
      color: var(--app-text);
      font-size: 0.9375rem;
    }
    .app-latest-place {
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .app-kind {
      display: inline-block;
      padding: 0.125rem var(--app-space-2);
      border-radius: var(--app-radius-pill);
      background-color: var(--app-raised);
      color: var(--app-text-secondary);
      font-size: 0.75rem;
      font-weight: 700;
    }
  }
  .app-spotlight { display: block; }

  /* До 1200 боковая часть не образует бокса, и её карточки встают в сетку
     дашборда: цель и прожектор — рядом сразу под плитками, на виду, рекорды —
     внизу во всю ширину. Порядок задаёт order, DOM прежний (на нём держится
     телефон). Без прожектора цель занимает ряд целиком. На 1200 боковая
     часть снова колонка справа. */
  .app-dash {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .app-dash-side { display: contents; }
  .app-dash-stats,
  .app-dash-main,
  .app-stats,
  .app-records { grid-column: 1 / -1; }
  .app-goal,
  .app-spotlight { order: 1; }
  .app-dash-main { order: 2; }
  .app-stats { order: 3; }
  .app-records { order: 4; }
  .app-dash:not(:has(.app-spotlight)) .app-goal { grid-column: 1 / -1; }

  /* «Цель на неделю» — приём TailAdmin «Monthly Target»: полукруглая шкала
     с процентом, фраза и полоса из трёх чисел по низу карточки. */
  .app-goal {
    display: flex;
    flex-direction: column;
    padding: var(--app-space-5) var(--app-card-inset);
  }
  .app-goal-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--app-space-3);
  }
  .app-goal-title {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 800;
    letter-spacing: -0.01em;
  }
  .app-goal-sub {
    margin: var(--app-space-1) 0 0;
    color: var(--app-text-muted);
    font-size: 0.875rem;
  }
  .app-goal-gauge {
    position: relative;
    width: 100%;
    max-width: var(--app-measure-goal);
    margin: var(--app-space-4) auto 0;
  }
  .app-goal-gauge svg {
    display: block;
    width: 100%;
    height: auto;
  }
  .app-goal-track,
  .app-goal-bar {
    fill: none;
    stroke-width: 16;
    stroke-linecap: round;
  }
  .app-goal-track { stroke: var(--app-raised); }
  /* «Чернильный» акцент, как у логотипа: сам лайм на белой карточке почти
     не виден, а в тёмной теме токены совпадают. */
  .app-goal-bar { stroke: var(--app-accent-text); }
  .app-goal-value {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .app-goal-percent {
    font-size: 2rem;
    line-height: 1.1;
  }
  .app-goal-caption {
    color: var(--app-text-secondary);
    font-size: 0.875rem;
  }
  .app-goal-message {
    margin: var(--app-space-4) 0 0;
    color: var(--app-text-secondary);
    text-align: center;
  }
  .app-goal > .btn {
    align-self: center;
    margin-top: var(--app-space-4);
  }
  /* Полоса чисел — во всю ширину карточки, по её нижнему краю: отступы
     карточки вычитаются полями, скругление повторяет её углы. */
  .app-goal-facts {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--app-space-2);
    margin: var(--app-space-5) calc(-1 * var(--app-card-inset)) calc(-1 * var(--app-space-5));
    padding: var(--app-space-4) var(--app-card-inset);
    border-top: 1px solid var(--app-line);
    border-radius: 0 0 var(--app-radius-card) var(--app-radius-card);
    background-color: var(--app-raised);
    text-align: center;
  }
  /* Подпись может уйти в две строки («Прошлая неделя» в узкой колонке) —
     значения всё равно стоят на одной линии по низу полосы. */
  .app-goal-facts > div {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
  .app-goal-facts dt {
    color: var(--app-text-muted);
    font-size: 0.8125rem;
    font-weight: 600;
  }
  .app-goal-facts dd {
    margin: var(--app-space-1) 0 0;
    font-size: 1.0625rem;
  }

  /* «Статистика» — приём TailAdmin «Statistics»: заголовок слева, вкладки
     справа, под ними легенда с итогом года и график по месяцам. Высота тела
     одна у заготовки и у карточки с данными — подгрузка не сдвигает страницу. */
  .app-stats {
    display: block;
    padding: var(--app-space-5) calc(var(--app-card-inset) - var(--app-space-1)) var(--app-space-3);
  }
  .app-stats-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--app-space-3);
    padding-inline: var(--app-space-1);
  }
  .app-stats-sub {
    margin: var(--app-space-1) 0 0;
    color: var(--app-text-muted);
    font-size: 0.875rem;
  }
  .app-stats-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--app-space-2) var(--app-space-4);
    margin-top: var(--app-space-3);
    padding-inline: var(--app-space-1);
  }
  .app-stats-meta .app-chart-legend { margin: 0; }
  .app-stats-total {
    color: var(--app-text-secondary);
    font-size: 0.875rem;
  }
  .app-stats-total .num {
    margin-left: var(--app-space-1);
    color: var(--app-text);
    font-size: 1rem;
  }
  .app-stats-body {
    position: relative;
    height: 320px;
    margin-top: var(--app-space-2);
  }
  .app-stats-empty {
    margin: var(--app-space-6) 0;
    color: var(--app-text-secondary);
    text-align: center;
  }
  .app-stats .app-empty { margin-top: var(--app-space-4); }

  /* Переключатель-сегменты (TailAdmin): подложка, активный сегмент — карточкой
     поверх неё. Поверхности по темам разные (--app-segment-*): в тёмной
     активный светлее подложки, в светлой — белая карточка на сером. */
  .app-tabs {
    display: inline-flex;
    gap: var(--app-space-1);
    padding: var(--app-space-1);
    border-radius: var(--app-radius-field);
    background-color: var(--app-segment-bg);
  }
  .app-tab {
    padding: var(--app-space-2) var(--app-space-3);
    border: 0;
    border-radius: calc(var(--app-radius-field) - var(--app-space-1));
    background-color: transparent;
    color: var(--app-text-secondary);
    font-size: 0.875rem;
    font-weight: 600;
    transition: var(--app-transition-ui);
  }
  .app-tab.is-active {
    background-color: var(--app-segment-active);
    box-shadow: var(--app-shadow-card);
    color: var(--app-text);
  }

  /* Первый силовой рекорд живёт в прожекторе — плитку прячем */
  .app-records-item.is-spotlight { display: none; }
  /* Если он был единственным, прячем и саму карточку: она стала бы пустой */
  .app-records.is-only-spotlight { display: none; }

  /* Рекорды и последние — единые карточки со строками, как в макете */
  .app-records,
  .app-latest {
    padding: var(--app-space-4) var(--app-card-inset);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-card);
    background-color: var(--app-card);
    box-shadow: var(--app-shadow-card);
  }

  /* Заголовки карточек колонки — тем же стилем, что «Часы по неделям» и имя
     в прожекторе, как в эталоне. Серый подзаголовок раздела годится там, где он
     стоит над списком (телефон), а внутри карточки он спорил с заголовком
     графика рядом: два разных «заголовка карточки» на одном экране. */
  .app-records .app-section-title,
  .app-latest .app-section-title {
    color: var(--app-text);
    font-size: 1.125rem;
    font-weight: 800;
    letter-spacing: -0.01em;
  }

  .app-records-list { display: block; }

  .app-records-item {
    flex-direction: row;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.375rem 0;
    border: 0;
    border-radius: 0;
    background-color: transparent;
    box-shadow: none;
  }

  /* Название прижато влево, значение и темп/скорость — вправо. Переносится
     только название: «Лыжи · классика · дистанция» иначе сжимало значение до
     «14 / км» в две строки. */
  .app-records-label { margin-right: auto; }

  .app-records-value {
    flex: none;
    font-size: 1.0625rem;
    font-weight: 700;
    white-space: nowrap;
  }

  .app-records-sub {
    flex: none;
    font-size: 0.8125rem;
    white-space: nowrap;
  }

  .app-latest-row {
    padding: 0.375rem 0;
    margin-bottom: 0;
    border: 0;
    border-radius: 0;
    background-color: transparent;
    box-shadow: none;
  }

  .app-latest-row .app-pick-chevron { display: none; }

  .app-latest-text {
    flex: 1 1 auto;
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
  }

  .app-latest-meta {
    font-size: 0.875rem;
    white-space: nowrap;
  }

  /* ---------- История на средних экранах: доска ----------

     Карточки — в колонки доски, заголовки недель, «Показать ещё» и пустое
     состояние — во всю строку: недели по-прежнему структурируют ленту.
     Подгрузка вставляет карточки прямо в сетку. С 1200 (при поддержке
     subgrid) лента становится таблицей плотных строк — её правила ниже
     перебивают эти по порядку в файле. */
  .app-feed {
    display: grid;
    grid-template-columns: repeat(var(--app-board-cols), minmax(0, 1fr));
    gap: var(--app-space-3) var(--app-space-4);
  }
  .app-feed > .app-week-title,
  .app-feed > .btn,
  .app-feed > .app-empty { grid-column: 1 / -1; }
  .app-feed > .app-week-title { margin: var(--app-space-2) 0 0; }
  .app-feed > .app-workout { margin-bottom: 0; }

  /* ---------- Итог тренировки ----------

     Дашборд во всю ширину: шапка с действиями справа, итоги в ряд, ниже
     упражнения и кардио колонками. Колонки — CSS columns, а не сетка: они
     заполняются сверху вниз, и 1, 2, 3 идут по первой — порядок упражнений,
     ради которого итог на телефоне одноколоночный, сохраняется. */
  .app-summary {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "head actions"
      "totals totals"
      "list list";
    column-gap: var(--app-space-6);
    align-items: start;
  }
  .app-summary > .app-screen-head { grid-area: head; }
  .app-summary > .app-summary-totals { grid-area: totals; }
  .app-summary > .app-summary-list {
    grid-area: list;
    display: block;
    columns: var(--app-board-cols);
    column-gap: var(--app-space-4);
  }
  .app-summary-list > * { break-inside: avoid; }
  .app-summary-list > .app-empty { column-span: all; }
  .app-summary > .app-form-actions {
    grid-area: actions;
    flex-direction: row;
    margin-top: 0;
    max-width: none;
  }
  .app-summary .app-btn-fill { width: auto; }
  /* Три числа сводки расходятся по ширине равномерно */
  .app-summary-totals .app-metrics {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* ---------- Живой режим и черновик: две колонки с 768 ----------

     Слева текущее упражнение со степперами, справа стопка: отдых, «Дальше»,
     «Выполнено», кардио. Ряды заданы явно: с автоматическими колонки были бы
     связаны общими рядами, и правая стопка начиналась бы только под высокой
     карточкой степперов. Последний ряд 1fr забирает лишнюю высоту текущего
     упражнения; пустой ряд (нет отдыха у черновика, нет выполненных)
     схлопывается в ноль — поэтому row-gap нет, отступы дают сами блоки.
     #exercises живого экрана отдаёт свой бокс через display: contents: его id
     и класс переезжают вместе со свопом, а пустому div терять нечего.

     Липким таймер не делаем: в сетке липкость ограничена ячейкой, а ряд
     таймера равен его же высоте — двигаться ему некуда. Прежнее «таймер
     остаётся на виду» так и не работало; теперь таймер стоит наверху своей
     колонки рядом с текущим упражнением. */
  .app-live-screen {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(6, auto) 1fr;
    column-gap: var(--app-space-8);
    align-items: start;
  }
  .app-live-offline { grid-column: 1 / -1; grid-row: 1; }
  .app-live-head { grid-column: 1 / -1; grid-row: 2; }
  /* Только у живого экрана: тот же id носит регион экрана правки записанной
     тренировки, и там он — колонки итога (.app-summary-list). */
  .app-live-screen > #exercises { display: contents; }
  .app-live-now { grid-column: 1; grid-row: 3 / -1; }
  /* Старт черновика занимает слот отдыха — там после старта встанет таймер. */
  .app-live-screen > :is(.app-rest, .app-draft-start) { grid-column: 2; grid-row: 3; }
  .app-live-plan { grid-column: 2; grid-row: 4; }
  .app-live-done { grid-column: 2; grid-row: 5; }
  .app-live-screen > .app-cardio-parts,
  .app-live-screen > .app-cardio-add-tile { grid-column: 2; grid-row: 6; }
  /* Текущего упражнения нет (новый черновик, всё выполнено) — рабочая колонка
     пуста, и блоки встают во всю ширину, а не жмутся вправо. */
  .app-live-screen:not(:has(.app-live-now *))
    :is(.app-rest, .app-draft-start, .app-live-plan, .app-live-done, .app-cardio-parts,
      .app-cardio-add-tile) {
    grid-column: 1 / -1;
  }
}

@media (min-width: 1200px) {
  /* Каркас ПК с 1200: панель развёрнута с подписями, пока её не свернули —
     свёрнутая остаётся тем же «рельсом», что с 768. */
  :root:not([data-sidebar-collapsed]) .app-sidebar {
    width: var(--app-measure-sidebar);
    padding-inline: var(--app-space-5);
  }
  :root:not([data-sidebar-collapsed]) .app-sidebar-label { display: inline; }
  :root:not([data-sidebar-collapsed]) .app-sidebar-logo,
  :root:not([data-sidebar-collapsed]) .app-side-link { justify-content: flex-start; }
  :root:not([data-sidebar-collapsed]) .app-sidebar-logo { padding-inline: var(--app-space-3); }
  :root:not([data-sidebar-collapsed]) .app-side-dot {
    position: static;
    margin-left: auto;
  }
  /* У «Админки» в развёрнутой панели вместо точки — число новых обращений. */
  :root:not([data-sidebar-collapsed]) .app-side-dot-rail { display: none; }
  :root:not([data-sidebar-collapsed]) .app-side-count {
    display: inline-flex;
    margin-left: auto;
  }
  :root:not([data-sidebar-collapsed]) .app-sidebar-group {
    display: block;
    margin: var(--app-space-6) var(--app-space-3) var(--app-space-2);
    border-top: 0;
  }
  :root:not([data-sidebar-collapsed]) .app-sidebar-group:first-child { margin-top: 0; }
  .app-sidebar-toggle { display: inline-flex; }

  /* Отступ шапки растёт токеном --app-header-pad (tokens.css): вместе с ним
     пересчитывается и --app-header-height, от которого встают липкие блоки. */
  .app-main { padding-bottom: var(--app-space-16); }

  /* Профиль на 1200: четыре колонки. Верхний ряд — пользователь и три плитки
     справочников со счётчиками, как плитки дашборда; ниже настройки,
     приложение и аккаунт. Счётчики уже посчитаны — запросов не прибавляется,
     а у профиля бюджет 9 из 9. */
  .app-profile-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-areas:
      "user catalogs catalogs catalogs"
      "settings settings app account";
    /* Один зазор на весь верхний ряд: между карточкой пользователя и плитками
       тот же, что между самими плитками, иначе ряд читается двумя группами. */
    column-gap: var(--app-space-6);
  }
  .app-profile-user,
  .app-profile-catalogs { align-self: stretch; }
  /* Карточка пользователя — плитка, как соседние: аватар сверху, почта и
     отметка внизу. Рядом с боковой панелью колонка ~250px, и в строку с
     аватаром длинный адрес ломался посреди слова. Почта — одной строкой с
     многоточием, полный адрес — в title. */
  .app-profile-user {
    flex-direction: column;
    align-items: flex-start;
    justify-content: space-between;
  }
  .app-profile-user > div { max-width: 100%; }
  .app-profile-email {
    overflow: hidden;
    overflow-wrap: normal;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Заголовок над плитками не нужен: ряд читается как плитки дашборда.
     Читалкам он остаётся — прячем визуально, как .visually-hidden. */
  .app-profile-catalogs > .app-section-title {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /* Четыре плитки — 2×2, а не в ряд: рядом с боковой панелью пять плиток в
     ряду дали бы ~150px, и «Мои упражнения» ломалось бы посреди слова. Ряды
     равной высоты: подпись есть не у всех плиток, и без 1fr верхний ряд
     выходил бы ниже нижнего, а числа стояли бы на разной высоте. */
  .app-profile-catalogs .app-rows {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 1fr;
    gap: var(--app-space-6);
    height: 100%;
    margin-bottom: 0;
  }
  .app-profile-catalogs .app-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "icon text chevron"
      "count count count";
    align-content: space-between;
    row-gap: var(--app-space-3);
  }
  .app-profile-catalogs .app-row-icon { grid-area: icon; }
  .app-profile-catalogs .app-row-text { grid-area: text; }
  .app-profile-catalogs .app-pick-chevron { grid-area: chevron; }
  /* Счётчик — число плитки, тем же размером, что значения дашборда */
  .app-profile-catalogs .app-row-count {
    grid-area: count;
    color: var(--app-text);
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.2;
  }

  /* ---------- Вход и регистрация: бренд-панель ----------

     Слева панель с логотипом и короткой подписью о дневнике, справа форма
     посередине своей половины. Ряды 1fr auto auto 1fr держат карточку и
     подвал вместе по центру по вертикали. */
  .app-auth {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: 1fr auto auto 1fr;
    justify-items: center;
    padding: 0;
  }
  /* stretch по обеим осям: базовое align-items: center у .app-auth центрировал
     бы панель по вертикали её высотой, а не на всю высоту экрана. */
  .app-auth-aside {
    grid-column: 1;
    grid-row: 1 / -1;
    justify-self: stretch;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--app-space-6);
    padding: var(--app-space-16);
    border-right: 1px solid var(--app-line);
    background-color: var(--app-card);
  }
  .app-auth-brand { margin: 0; }
  .app-auth-tagline {
    display: block;
    max-width: var(--app-measure-form);
    margin: 0;
    color: var(--app-text-secondary);
    font-size: 1.125rem;
    line-height: 1.5;
  }
  .app-auth-card { grid-column: 2; grid-row: 2; }
  .app-auth-footer { grid-column: 2; grid-row: 3; }

  /* ---------- Справочник упражнений: шторка поверх таблицы ----------

     Клик по строке (exercise.js ставит .is-open) открывает упражнение шторкой
     справа: таблица остаётся во всю ширину и не теряет прокрутку, а соседнюю
     строку можно открыть, не закрывая шторку, — поэтому без затемнения. Шторка
     стоит под шапкой сайта (z-index 1020: у шапки 1030, у окон 1050) и
     прокручивается сама: в ней история подходов на несколько экранов, а
     страница под ней остаётся на месте. Фон — фон страницы, а не карточки:
     карточки внутри выглядят так же, как на странице упражнения. */
  .app-catalog.is-open > .app-catalog-detail {
    position: fixed;
    inset-block: var(--app-header-height) 0;
    inset-inline-end: 0;
    z-index: 1020;
    display: block;
    width: var(--app-measure-drawer);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 var(--app-space-6) var(--app-space-8);
    border-inline-start: 1px solid var(--app-line);
    background-color: var(--app-bg);
    box-shadow: var(--app-shadow-drawer);
  }
  /* Шторка уже страницы: доска истории подходов — в две колонки. */
  .app-catalog-detail { --app-board-cols: 2; }
  /* С 1200 места хватает — ячейкам возвращаются полные 12px по бокам. Крайние
     ячейки держат отступ карточки: их правило специфичнее. */
  .app-catalog-table th,
  .app-catalog-table td { padding-inline: var(--app-space-3); }
  /* Фокус в шторку ставит код, а не человек: обводка вокруг всей шторки
     только мешала бы. Внутри неё фокус по-прежнему видно. */
  .app-catalog-detail:focus-visible { outline: none; }

  /* Открытое упражнение в таблице — фон и акцентная рейка у первой ячейки.
     Рейка — inset-тенью: у ячеек таблицы нет своей рамки слева. */
  .app-catalog-table tr.is-active > td { background-color: var(--app-raised); }
  .app-catalog-table tr.is-active > td:first-child {
    box-shadow: inset 3px 0 0 var(--app-accent);
  }

  /* Отдельная страница упражнения шире шторки: четыре блока настроек встают
     в один ряд, а не 2×2 с пустой половиной карточки. Колонки по содержимому
     и сжимаемые — у своего упражнения чипы групп и снарядов переносятся
     внутри блока, а не выталкивают его за край. Шторка — не прямой ребёнок
     main, её правило не задевает. */
  :where(.app-main) > .app-exercise > .app-settings > .app-settings-row {
    grid-template-columns: repeat(4, minmax(0, max-content));
  }

  /* Липкой становится шапка шторки, а не вся она: шапка отвечает на
     единственный вопрос, который теряется при прокрутке, — «что я сейчас
     читаю», и держит крестик закрытия под рукой. Шторка прокручивается сама,
     поэтому top: 0. Поля шторки шапка перекрывает отрицательными полями, чтобы
     её фон шёл от края до края и под ней не проезжал график. Рендерится только
     в шторке ({% if in_panel %}), поэтому страницу упражнения правило не
     задевает. */
  .app-exercise-head {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--app-space-3);
    margin-inline: calc(-1 * var(--app-space-6));
    padding: var(--app-space-4) var(--app-space-6) var(--app-space-3);
    background-color: var(--app-bg);
  }
  .app-exercise-head-text { min-width: 0; }


  /* ---------- Живой режим и черновик на 1200: три зоны ----------

     «Сейчас», «Дальше» и колонка из отдыха, «Выполнено» и кардио. Колонки
     равные: степперы шире ~28rem не становятся удобнее, а таймер сохраняет
     телефонные пропорции вместо полосы во всю страницу. Базовая сетка — в блоке
     768; здесь только раскладка колонок. */
  .app-live-screen {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(5, auto) 1fr;
  }
  .app-live-plan { grid-column: 2; grid-row: 3 / -1; }
  .app-live-screen > :is(.app-rest, .app-draft-start) { grid-column: 3; grid-row: 3; }
  .app-live-done { grid-column: 3; grid-row: 4; }
  .app-live-screen > .app-cardio-parts,
  .app-live-screen > .app-cardio-add-tile { grid-column: 3; grid-row: 5; }
  /* Без текущего упражнения «Дальше» забирает и рабочую колонку. */
  .app-live-screen:not(:has(.app-live-now *)) .app-live-plan { grid-column: 1 / span 2; }
  .app-live-screen:not(:has(.app-live-now *))
    :is(.app-rest, .app-draft-start, .app-live-done, .app-cardio-parts, .app-cardio-add-tile) {
    grid-column: 3;
  }

  /* Дашборд: сетка «плитки во всю ширину; график, подготовленное и таблица
     последних слева, прожектор и рекорды справа». Зазоры — как у TailAdmin на
     широком экране: 24px между карточками и плитками. */
  .app-dash {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-areas:
      "stats stats"
      "main side"
      "year year";
    align-items: start;
    gap: var(--app-space-6);
  }
  .app-stats { grid-area: year; }
  .app-dash-stats { gap: var(--app-space-6); }
  .app-dash-main,
  .app-dash-side { gap: var(--app-space-6); }

  .app-dash-stats { grid-area: stats; }
  .app-dash-main { grid-area: main; }
  /* Колонка справа снова бокс (до 1200 она растворена в сетке): цель,
     прожектор и рекорды столбиком. */
  .app-dash-side {
    grid-area: side;
    display: flex;
  }

  /* ---------- История: плотные строки ----------

     Колонки задаёт список, а карточки берут их через subgrid — поэтому цифры
     выравниваются по вертикали через все недели, и ни одного магического числа
     ширины не нужно: max-content считается по самой широкой ячейке всего
     списка. display: contents на карточке не подошёл бы — она потеряла бы фон,
     рамку и поверхность для наведения, а кардио-строка перестала бы быть одной
     ссылкой. Старый браузер без subgrid остаётся на карточках. */
  @supports (grid-template-columns: subgrid) {
    .app-feed {
      display: grid;
      grid-template-columns:
        minmax(0, max-content)  /* вид спорта */
        max-content             /* дата */
        max-content             /* время */
        max-content             /* сколько: упражнений или дистанция */
        max-content             /* насколько: тоннаж или темп/скорость */
        minmax(0, 1fr)          /* заметка забирает остаток ширины */
        max-content;            /* действия */
      column-gap: var(--app-space-6);
      row-gap: 0;
      align-items: center;
    }

    /* Неделя — одна таблица-карточка, как у TailAdmin: строки стоят вплотную и
       делят рамку (соседняя заезжает на пиксель), скруглены только верх первой
       строки недели и низ последней. Всё на соседстве в плоском списке —
       обёртки недели в разметке нет, а подгрузка «Показать ещё» дописывает
       строки той же недели прямо за предыдущими. */
    .app-feed > .app-workout + .app-workout {
      margin-top: -1px;
      border-top-left-radius: 0;
      border-top-right-radius: 0;
    }
    .app-feed > .app-workout:has(+ .app-workout) {
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
    }
    .app-feed > .btn { margin-top: var(--app-space-4); }

    /* В таблице строка не «взлетает» при наведении, как карточка, а
       подсвечивается фоном: подъём одной строки рвал бы общую рамку недели. */
    @media (hover: hover) and (pointer: fine) {
      .app-feed > .app-workout-link:hover {
        border-color: var(--app-line);
        background-color: var(--app-raised);
        box-shadow: none;
        transform: none;
      }
    }

    /* Заголовки недель, «Показать ещё» и пустое состояние — во всю строку:
       недели продолжают структурировать список, а колонки у него общие. */
    .app-feed > .app-week-title,
    .app-feed > .btn,
    .app-feed > .app-empty {
      grid-column: 1 / -1;
    }

    .app-feed > .app-week-title {
      margin: var(--app-space-5) 0 var(--app-space-1);
    }

    .app-workout {
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: subgrid;
      align-items: center;
      padding: var(--app-space-3) var(--app-space-4);
      margin-bottom: 0;
      box-shadow: none;
    }

    /* Зазор в subgrid-оси берёт родитель, но прописываем то же значение и здесь:
       результат не должен зависеть от того, как это трактует движок. */
    .app-workout-head {
      grid-column: 1 / 3;
      display: grid;
      grid-template-columns: subgrid;
      column-gap: var(--app-space-6);
      margin-bottom: 0;
    }

    .app-metrics {
      grid-column: 3 / 6;
      display: grid;
      grid-template-columns: subgrid;
      column-gap: var(--app-space-6);
    }

    /* Подписи метрик остаются у каждой ячейки своей — единой шапки таблицы быть
       не может: у силовой это «упражнений/тоннаж», у кардио «дистанция/темп». */
    .app-metrics dt {
      font-size: 0.75rem;
      color: var(--app-text-muted);
    }

    .app-metrics .app-elevation {
      display: inline-flex;
      margin-left: var(--app-space-2);
    }

    /* Место с заметкой и действия — с явными номерами колонок: ни места, ни
       заметки нет у каждой тренировки, и без этого кнопки уехали бы в их
       колонку. Отдельного трека под место нет намеренно: max-content
       зарезервировал бы его ширину во всех строках, а у истории, записанной до
       появления справочника, места нет вовсе — колонка стояла бы пустой. Тот же
       довод, по которому от subgrid отказался .app-rows-dense. */
    .app-workout-meta {
      grid-column: 6;
      display: block;
      min-width: 0;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      font-size: 0.9375rem;
    }

    /* Одним текстовым прогоном с единственной точкой обрезки: два независимых
       ellipsis дали бы обрезанное место рядом с обрезанной заметкой.

       Классы перечислены явно, а не через `> *`: мобильное правило
       `.app-workout-meta > .app-place` весит два класса и перебило бы селектор
       с универсальным — место осталось бы флекс-боксом и уехало на свою строку
       вместе с заметкой. Та же ловушка, что у .app-empty-slim и .app-empty. */
    .app-workout-meta > .app-place,
    .app-workout-meta > .app-workout-note {
      display: inline;
      margin: 0;
    }

    /* Зазор, а не символ-разделитель: разделитель остался бы висеть, когда
       вторая часть обрезана в ноль. Специфичность выше правила с `margin: 0`
       выше — иначе обнуление отступа победило бы независимо от порядка. */
    .app-workout-meta > .app-place + .app-workout-note {
      margin-left: var(--app-space-3);
    }

    .app-workout-actions {
      grid-column: 7;
      align-items: center;
      margin-top: 0;
    }

    /* Шеврон силовой — после «Повторить», у правой кромки (margin-left: auto
       базового .app-pick-chevron). */
    .app-workout-actions > .app-pick-chevron { display: block; }

    /* К правому краю колонки действий: ширину колонки задают «Повторить» и
       шеврон силовых строк, и шеврон кардио висел бы в её начале, посреди
       карточки, а не на одной вертикали с шевронами силовых. */
    .app-workout > .app-pick-chevron {
      display: block;
      grid-column: 7;
      justify-self: end;
      margin-left: 0;
    }
  }
}
