/*
 * TrenFlow — компоненты дизайн-системы (см. specs/design-system.md).
 *
 * Грузится ПОСЛЕ bootstrap.css. Стратегия: там, где бутстрап 5.2 уже читает свои
 * --bs-* переменные, мы подменяем переменные (тогда правки не разъезжаются с его
 * версиями); где цвета захардкожены (поля ввода, футер), переопределяем свойства.
 * Ни одно значение не задаётся литералом — только через токены из tokens.css.
 */

/* ——————————————————————————— База ——————————————————————————— */

body {
  background-color: var(--tf-ground);
  color: var(--tf-ink);
  font-family: var(--tf-font-body);
  font-size: var(--tf-size-body);
  line-height: var(--tf-line-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6,
.tf-display {
  font-family: var(--tf-font-display);
  color: var(--tf-ink);
  font-weight: 700;
  line-height: var(--tf-line-tight);
  letter-spacing: -.01em;
  text-wrap: balance;
}

h1, .tf-h1 { font-size: var(--tf-size-h1); text-transform: uppercase; letter-spacing: -.02em; }
h2, .tf-h2 { font-size: var(--tf-size-h2); text-transform: uppercase; }
h3, .tf-h3 { font-size: var(--tf-size-h3); }
h4, .tf-h4 { font-size: var(--tf-size-h4); }

a { color: var(--tf-track); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--tf-track-deep); }

::selection { background: var(--tf-track); color: var(--tf-on-accent); }

:focus-visible {
  outline: 2px solid var(--tf-track);
  outline-offset: 2px;
}

hr { border-color: var(--tf-hair); opacity: 1; }

code, kbd, samp, pre, .tf-num {
  font-family: var(--tf-font-mono);
  font-variant-numeric: tabular-nums;
}
code { color: var(--tf-track); background: var(--tf-track-soft); padding: 1px 5px; border-radius: var(--tf-radius); }
pre code { background: none; padding: 0; }

/* Надзаголовок, лид, подпись — три роли текста помимо заголовка и абзаца */
.tf-eyebrow {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--tf-track);
}
.tf-lede {
  font-size: var(--tf-size-lede);
  line-height: 1.45;
  color: var(--tf-ink-2);
  max-width: 62ch;
}
.tf-meta {
  font-family: var(--tf-font-mono);
  font-size: 12.5px;
  color: var(--tf-fog);
  font-variant-numeric: tabular-nums;
}
.tf-muted { color: var(--tf-ink-2); }
.tf-prose { max-width: 68ch; }

/* Шапка раздела: надзаголовок + H1 + действия справа */
.tf-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--tf-sp-4);
  flex-wrap: wrap;
  padding-bottom: var(--tf-sp-4);
  border-bottom: var(--tf-border);
  margin-bottom: var(--tf-sp-5);
}
.tf-page-head__title { display: flex; flex-direction: column; gap: var(--tf-sp-1); }

/* Раскладка: отступы задаёт контейнер, а не соседи */
.tf-stack { display: flex; flex-direction: column; gap: var(--tf-sp-5); }
.tf-stack--tight { gap: var(--tf-sp-3); }
.tf-row { display: flex; align-items: center; gap: var(--tf-sp-3); flex-wrap: wrap; }
.tf-scroll-x { overflow-x: auto; }

/* ——————————————————————————— Знак ——————————————————————————— */

/*
 * Знак «Интервал»: пять столбиков недельной нагрузки со скосом вправо.
 * Рисуется инлайновым SVG (widget common\widgets\Mark), чтобы цвета брались из
 * токенов и знак сам следовал теме.
 */
.tf-mark { display: block; flex: none; }
.tf-mark__bar { fill: var(--tf-bar-light); }
.tf-mark__bar--hard { fill: var(--tf-track); }

.tf-lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--tf-sp-2);
  text-decoration: none;
}
.tf-lockup:hover { text-decoration: none; }
.tf-wordmark {
  font-family: var(--tf-font-display);
  font-weight: 700;
  font-size: 21px;
  line-height: 1;
  letter-spacing: -.015em;
  color: var(--tf-ink);
  white-space: nowrap;
}
.tf-wordmark em { font-style: normal; color: var(--tf-track); }

/* Заглушка аватара (инициалы) — widget common\widgets\Avatar */
.tf-avatar-fallback {
  background: var(--tf-hair);
  color: var(--tf-ink-2);
  text-align: center;
  font-family: var(--tf-font-display);
  font-weight: 600;
}

/* Круглый аватар со знаком — под бота и как заглушка */
.tf-mark-round {
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--tf-ink);
  padding: 22%;
  box-sizing: border-box;
}

/* ——————————————— График нагрузки: тот же элемент, что знак ——————————————— */

.tf-load {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 48px;
}
.tf-load__bar {
  display: block;
  width: 10px;
  min-height: 3px;
  background: var(--tf-bar-light);
  border-radius: var(--tf-radius);
}
.tf-load__bar--hard { background: var(--tf-track); }
.tf-load__bar--rest { background: var(--tf-hair); }
.tf-load__bar--done { background: var(--tf-pine); }

/* ——————————————————————————— Навигация ——————————————————————————— */

.tf-nav {
  background: var(--tf-surface);
  border-bottom: var(--tf-border);
  min-height: var(--tf-nav-height);
  padding-top: var(--tf-sp-2);
  padding-bottom: var(--tf-sp-2);
  --bs-navbar-color: var(--tf-ink-2);
  --bs-navbar-hover-color: var(--tf-ink);
  --bs-navbar-active-color: var(--tf-track);
  --bs-navbar-brand-color: var(--tf-ink);
  --bs-navbar-brand-hover-color: var(--tf-ink);
  --bs-navbar-toggler-border-color: var(--tf-hair);
  --bs-navbar-toggler-focus-width: 3px;
}
.tf-nav .nav-link {
  font-family: var(--tf-font-display);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  padding-left: var(--tf-sp-3);
  padding-right: var(--tf-sp-3);
}
.tf-nav .nav-link.active,
.tf-nav .nav-link:hover { color: var(--tf-track); }
.tf-nav .navbar-toggler-icon {
  /* иконка бутстрапа — картинка с зашитым цветом; рисуем свою из токенов */
  background-image: none;
  position: relative;
}
.tf-nav .navbar-toggler-icon::before,
.tf-nav .navbar-toggler-icon::after,
.tf-nav .navbar-toggler-icon > span {
  content: '';
  position: absolute;
  left: 2px;
  right: 2px;
  height: 2px;
  background: var(--tf-ink);
}
.tf-nav .navbar-toggler-icon::before { top: 5px; }
.tf-nav .navbar-toggler-icon::after { bottom: 5px; }
.tf-nav .navbar-toggler-icon > span { top: 50%; transform: translateY(-50%); }

/* Кнопка темы: только иконка, без подписи (common\widgets\ThemeToggle) */
.tf-theme {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  padding: 0;
  background: none;
  border: var(--tf-border);
  border-radius: 999px;
  color: var(--tf-ink-2);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.tf-theme:hover {
  color: var(--tf-ink);
  border-color: var(--tf-fog);
  background: var(--tf-surface-2);
}
.tf-theme:focus-visible {
  outline: 2px solid var(--tf-track);
  outline-offset: 2px;
}

/*
 * Какую иконку показать, знает только CSS: при системной теме сервер её не
 * знает, а выбор в JS моргал бы. Схема та же, что у токенов: база — светлая,
 * дальше медиавыражение (кроме явного light) и оба data-theme.
 * Иконка показывает, куда переключит клик: светлая тема — луна, тёмная — солнце.
 */
.tf-theme__icon { display: none; }
.tf-theme__icon--moon { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tf-theme__icon--moon { display: none; }
  :root:not([data-theme="light"]) .tf-theme__icon--sun { display: block; }
}

:root[data-theme="dark"] .tf-theme__icon--moon { display: none; }
:root[data-theme="dark"] .tf-theme__icon--sun { display: block; }

:root[data-theme="light"] .tf-theme__icon--sun { display: none; }
:root[data-theme="light"] .tf-theme__icon--moon { display: block; }

/* ——————————————————————————— Кнопки ——————————————————————————— */

.btn {
  --bs-btn-font-family: var(--tf-font-display);
  --bs-btn-font-size: 14px;
  --bs-btn-font-weight: 600;
  --bs-btn-border-radius: var(--tf-radius);
  --bs-btn-padding-x: var(--tf-sp-4);
  --bs-btn-padding-y: 9px;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.btn-sm { --bs-btn-font-size: 12.5px; --bs-btn-padding-x: var(--tf-sp-3); --bs-btn-padding-y: 5px; }
.btn-lg { --bs-btn-font-size: 16px; --bs-btn-padding-x: var(--tf-sp-5); --bs-btn-padding-y: 12px; }

.btn-primary {
  --bs-btn-bg: var(--tf-track);
  --bs-btn-border-color: var(--tf-track);
  --bs-btn-color: var(--tf-on-accent);
  --bs-btn-hover-bg: var(--tf-track-deep);
  --bs-btn-hover-border-color: var(--tf-track-deep);
  --bs-btn-hover-color: var(--tf-on-accent);
  --bs-btn-active-bg: var(--tf-track-deep);
  --bs-btn-active-border-color: var(--tf-track-deep);
  --bs-btn-active-color: var(--tf-on-accent);
  --bs-btn-disabled-bg: var(--tf-fog);
  --bs-btn-disabled-border-color: var(--tf-fog);
}
.btn-secondary,
.btn-outline-secondary {
  --bs-btn-bg: transparent;
  --bs-btn-color: var(--tf-ink);
  --bs-btn-border-color: var(--tf-hair);
  --bs-btn-hover-bg: var(--tf-surface-2);
  --bs-btn-hover-color: var(--tf-ink);
  --bs-btn-hover-border-color: var(--tf-fog);
  --bs-btn-active-bg: var(--tf-surface-2);
  --bs-btn-active-color: var(--tf-ink);
  --bs-btn-active-border-color: var(--tf-fog);
}
/*
 * Цвет надписи у залитых кнопок — `--tf-on-accent`, а не белый из бутстрапа:
 * в тёмной теме заливка светлая, и белая надпись на ней не читается.
 */
.btn-success {
  --bs-btn-bg: var(--tf-pine);
  --bs-btn-border-color: var(--tf-pine);
  --bs-btn-color: var(--tf-on-accent);
  --bs-btn-hover-bg: var(--tf-pine);
  --bs-btn-hover-border-color: var(--tf-pine);
  --bs-btn-hover-color: var(--tf-on-accent);
  --bs-btn-active-color: var(--tf-on-accent);
}
.btn-danger {
  --bs-btn-bg: var(--tf-danger);
  --bs-btn-border-color: var(--tf-danger);
  --bs-btn-color: var(--tf-on-accent);
  --bs-btn-hover-bg: var(--tf-danger);
  --bs-btn-hover-border-color: var(--tf-danger);
  --bs-btn-hover-color: var(--tf-on-accent);
  --bs-btn-active-color: var(--tf-on-accent);
}
.btn-link {
  --bs-btn-color: var(--tf-track);
  --bs-btn-hover-color: var(--tf-track-deep);
  text-transform: none;
  letter-spacing: 0;
}

/* ——————————————————————————— Формы ——————————————————————————— */

.form-label {
  font-family: var(--tf-font-display);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
  margin-bottom: var(--tf-sp-2);
}
.form-control,
.form-select {
  /* длинный вариант в select иначе не даёт полю сжаться ниже своей ширины
     и распирает страницу на узком экране */
  min-width: 0;
  /* та же нижняя граница касания, что у кнопок: в поле попадают пальцем */
  min-height: 44px;
  max-width: 100%;
  background-color: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  color: var(--tf-ink);
  font-family: var(--tf-font-body);
  font-size: var(--tf-size-body);
  padding: 9px var(--tf-sp-3);
}
.form-control:focus,
.form-select:focus {
  background-color: var(--tf-surface-2);
  border-color: var(--tf-track);
  box-shadow: var(--tf-focus);
  color: var(--tf-ink);
}
.form-control::placeholder { color: var(--tf-fog); }
.form-control:disabled,
.form-select:disabled { background-color: var(--tf-surface); color: var(--tf-fog); }
.form-text, .help-block { color: var(--tf-fog); font-size: var(--tf-size-small); }
.form-check-input { border-color: var(--tf-hair); background-color: var(--tf-surface-2); }
.form-check-input:checked { background-color: var(--tf-track); border-color: var(--tf-track); }
.form-check-input:focus { border-color: var(--tf-track); box-shadow: var(--tf-focus); }

/*
 * Раскладка структуры тренировки под полем «Название» (specs/structured-training.md):
 * структура — производная названия, а не ввод, поэтому подсказка тихая —
 * подложка и мелкий кегль, а не рамка поля. Не разобралось — блок просто
 * пустой (partial ничего не печатает), а не ошибка.
 */
.tf-workout-outline {
  margin: calc(-1 * var(--tf-sp-2)) 0 var(--tf-sp-3);
  padding: var(--tf-sp-2) var(--tf-sp-3);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
}
.tf-workout-outline__list { list-style: none; margin: 0 0 var(--tf-sp-1); padding: 0; }
.tf-workout-outline__row[data-depth="1"] { margin-left: var(--tf-sp-4); color: var(--tf-fog); }
.tf-workout-outline__total { margin: 0; color: var(--tf-ink); }

/*
 * Bootstrap 5 выбросил .form-inline, а yii\bootstrap5\ActiveForm с
 * layout: inline её по-прежнему ставит — поэтому описываем сами. От узкого
 * экрана: столбец, в строку выстраивается с 36em.
 */
.form-inline { display: flex; flex-direction: column; gap: var(--tf-sp-3); }
/*
 * `!important` здесь обязателен: отступ полю ставит утилита бутстрапа `.mb-3`,
 * а утилиты у него сами объявлены с `!important`. Без этого поле оставалось на
 * 1rem выше строки, и кнопки рядом с ним («Показать», «За всё время» в фильтре
 * прошедших тренировок) выравнивались по низу этого отступа — то есть съезжали
 * вниз относительно самого поля.
 */
.form-inline .form-group,
.form-inline .mb-3 { margin-bottom: 0 !important; }
@media (min-width: 36em) {
  .form-inline { flex-direction: row; flex-wrap: wrap; align-items: flex-end; }
}

.is-invalid, .has-error .form-control {
  border-color: var(--tf-danger);
}
.invalid-feedback, .help-block-error {
  color: var(--tf-danger);
  font-size: var(--tf-size-small);
}

/*
 * Карточка формы (specs/design-system.md).
 *
 * Кабинет — рабочий инструмент: форма отвечает на один вопрос («что я сделал
 * на тренировке», «на какой старт бегу»), поэтому она живёт отдельным блоком с
 * ограниченной шириной строки, а не растягивается на весь монитор. 40rem —
 * та же логика, что у .tf-prose: длинная строка поля читается хуже короткой,
 * а глазу нужен край, чтобы видеть форму целиком.
 *
 * На телефоне рамка и внутренние отступы убираются: там форма и так занимает
 * весь экран, а рамка вокруг рамки съедает ширину полей.
 */
.tf-form {
  max-width: 40rem;
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  padding: var(--tf-sp-5);
}
.tf-form > form > .mb-3:last-of-type,
.tf-form > form > .form-group:last-child { margin-bottom: 0; }

/*
 * Смысловая группа полей. Форма отчёта — это три разных вопроса подряд
 * (что со статусом, какие цифры, что рассказать), и без надзаголовков они
 * читаются одним потоком из восьми полей.
 */
.tf-form-section + .tf-form-section {
  margin-top: var(--tf-sp-5);
  padding-top: var(--tf-sp-5);
  border-top: var(--tf-border);
}
.tf-form-section__title {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-fog);
  margin: 0 0 var(--tf-sp-3);
}
.tf-form-section__note {
  color: var(--tf-ink-2);
  font-size: var(--tf-size-small);
  margin: calc(-1 * var(--tf-sp-2)) 0 var(--tf-sp-3);
}

/*
 * Панель отправки. Отделена линией, потому что «Сохранить» — это выход из
 * формы, а не ещё одно поле. На телефоне прилипает к низу экрана: длинную
 * форму иначе приходится долистывать до кнопки, а на iOS её ещё и перекрывает
 * системная полоса — отсюда safe-area.
 */
.tf-form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tf-sp-3);
  margin-top: var(--tf-sp-5);
  padding-top: var(--tf-sp-4);
  border-top: var(--tf-border);
}
.tf-form-actions .btn { min-height: 44px; }

/*
 * Выбор из готовых вариантов «плитками» вместо столбика галочек: дистанции
 * забега — короткие ярлыки, и в столбик они занимают полэкрана, а пальцем по
 * ним попадают хуже, чем по плитке в 44 px.
 */
.tf-choice-chips { display: flex; flex-wrap: wrap; gap: var(--tf-sp-2); }
.tf-choice-chips .form-check {
  margin: 0;
  padding: 0;
  min-height: 0;
}
.tf-choice-chips .form-check-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.tf-choice-chips .form-check-label {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-pill);
  color: var(--tf-ink-2);
  cursor: pointer;
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-small);
  letter-spacing: .02em;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.tf-choice-chips .form-check-input:checked + .form-check-label {
  background: var(--tf-track-soft);
  border-color: var(--tf-track);
  color: var(--tf-track-deep);
}
.tf-choice-chips .form-check-input:focus-visible + .form-check-label {
  border-color: var(--tf-track);
  box-shadow: var(--tf-focus);
}

/*
 * Выбор одного варианта «карточками» с пояснением: цель бегуна, стаж
 * (specs/ai-coach.md). Отличие от плиток выше — не в красоте: у этих вариантов
 * ярлык ничего не объясняет («Возвращаюсь после паузы» — и что?), а от выбора
 * зависит характер нагрузки, поэтому пояснение обязано стоять внутри самого
 * варианта, а не сноской под списком.
 */
.tf-choice-cards {
  display: grid;
  gap: var(--tf-sp-2);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.tf-choice-cards .form-check { margin: 0; padding: 0; min-height: 0; }
.tf-choice-cards .form-check-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.tf-choice-cards .form-check-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  height: 100%;
  padding: var(--tf-sp-3) var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-left: 3px solid var(--tf-hair);
  border-radius: var(--tf-radius);
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}
.tf-choice-cards__title {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-h4);
  font-weight: 600;
  color: var(--tf-ink);
  line-height: 1.2;
}
.tf-choice-cards__hint {
  font-size: var(--tf-size-small);
  color: var(--tf-fog);
  line-height: 1.35;
}
.tf-choice-cards .form-check-input:checked + .form-check-label {
  background: var(--tf-track-soft);
  border-color: var(--tf-track);
}
.tf-choice-cards .form-check-input:checked + .form-check-label .tf-choice-cards__title {
  color: var(--tf-track-deep);
}
.tf-choice-cards .form-check-input:checked + .form-check-label .tf-choice-cards__hint {
  color: var(--tf-ink-2);
}
.tf-choice-cards .form-check-input:focus-visible + .form-check-label {
  border-color: var(--tf-track);
  box-shadow: var(--tf-focus);
}

/*
 * Блок формы: заголовок, одна поясняющая строка и поля. Анкета из семи полей
 * подряд читается как допрос — три подписанных блока по смыслу («цель»,
 * «форма», «расписание») человек проходит, не перечитывая.
 */
.tf-form-block {
  max-width: 40rem;
  padding-top: var(--tf-sp-5);
  border-top: var(--tf-border);
  margin-bottom: var(--tf-sp-5);
}
.tf-form-block:first-of-type { padding-top: 0; border-top: 0; }
.tf-form-block__head {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-h3);
  margin-bottom: var(--tf-sp-1);
}
.tf-form-block__lede {
  color: var(--tf-fog);
  font-size: var(--tf-size-small);
  margin-bottom: var(--tf-sp-4);
}

/*
 * Число с единицей измерения в самом поле: «12» без «км» рядом читается как
 * что угодно, а подпись слева от поля этого не решает — глаз проверяет
 * введённое, а не заголовок.
 */
.tf-metric { position: relative; max-width: 12rem; }
.tf-metric .form-control { padding-right: 3.5rem; }
.tf-metric__unit {
  position: absolute;
  right: var(--tf-sp-3);
  top: 50%;
  transform: translateY(-50%);
  color: var(--tf-fog);
  font-size: var(--tf-size-small);
  pointer-events: none;
}

/* Два числовых поля рядом, пока экран это позволяет. */
.tf-metric-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tf-sp-4);
}
.tf-metric-row > * { flex: 1 1 12rem; }

/*
 * Сводка над формой: задание тренировки, ответ тренера — то, ради чего форму
 * открыли. Ширина та же, что у формы, иначе карточка и поля стоят уступом.
 */
.tf-form-brief {
  max-width: 40rem;
  margin-bottom: var(--tf-sp-4);
}
.tf-form-brief dt {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-small);
  font-weight: 600;
  color: var(--tf-fog);
}
.tf-form-brief dd { margin-bottom: var(--tf-sp-2); }
.tf-form-brief dd:last-child { margin-bottom: 0; }

/* Обязательное поле помечено сразу у подписи, а не только сообщением об ошибке. */
.form-label.required::after {
  content: '*';
  margin-left: .25em;
  color: var(--tf-track);
}

/* ——————————————————————————— Карточки ——————————————————————————— */

.card,
.tf-card {
  --bs-card-bg: var(--tf-surface);
  --bs-card-border-color: var(--tf-hair);
  --bs-card-border-radius: var(--tf-radius-lg);
  --bs-card-cap-bg: var(--tf-surface-2);
  --bs-card-color: var(--tf-ink);
  --bs-card-spacer-x: var(--tf-sp-5);
  --bs-card-spacer-y: var(--tf-sp-4);
}
.tf-card {
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  padding: var(--tf-sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-3);
}
.tf-card--raised { box-shadow: var(--tf-shadow); }

/* ——————————————————————————— Таблицы и GridView ——————————————————————————— */

.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--tf-ink);
  --bs-table-border-color: var(--tf-hair);
  --bs-table-striped-bg: var(--tf-surface);
  --bs-table-striped-color: var(--tf-ink);
  --bs-table-hover-bg: var(--tf-track-soft);
  --bs-table-hover-color: var(--tf-ink);
  --bs-table-accent-bg: transparent;
}
.table > thead th {
  font-family: var(--tf-font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
  border-bottom: 2px solid var(--tf-hair);
  white-space: nowrap;
}
.table > tbody td { vertical-align: middle; }
.table td .tf-num, .table td.tf-num { font-variant-numeric: tabular-nums; }

/*
 * Плотность строки — из нашей шкалы отступов, а не из бутстрапа: у него `.table`
 * это 8 px, а `.table-sm` — 4 px, и список тренировок слипался в сплошное
 * полотно (блок F пилота). Селектор взят тот же, что у бутстрапа, чтобы спор
 * решался порядком файлов, а не специфичностью; `.table-sm` после этого меняет
 * не отступ, а переменную — иначе он вернул бы свои 4 px в каждом гриде, где
 * стоит во вью.
 */
.table { --tf-cell-y: var(--tf-sp-3); --tf-cell-x: var(--tf-sp-3); }
.table-sm { --tf-cell-y: var(--tf-sp-2); --tf-cell-x: var(--tf-sp-2); }
.table > :not(caption) > * > * { padding: var(--tf-cell-y) var(--tf-cell-x); }

/*
 * Список тренировок: ОДНА таблица на все недели, неделя — строка-разделитель.
 * Пока каждая неделя была отдельной таблицей, ширину колонок браузер считал по
 * её содержимому, и неделя с длинным описанием съезжала относительно соседней.
 * Ширины поэтому заданы здесь один раз (`<colgroup>` во вью только раздаёт
 * классы): гибкой остаётся колонка задания, числовые фиксированы, и взгляд идёт
 * по колонке сверху вниз.
 */
/*
 * Фон строки задаём переменной, а не `background`: бутстрап красит ячейки сам
 * (`background-color: var(--bs-table-bg)` плюс inset box-shadow от
 * `--bs-table-accent-bg`) селектором, который специфичнее любого класса на `tr`,
 * поэтому свой `background` здесь просто не виден. Тот же приём, что у
 * `.table-active` у самого бутстрапа.
 */
.tf-plan-week { --bs-table-accent-bg: var(--tf-surface); }
.tf-plan-week > * {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-small);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
}
/*
 * Раздел («Предстоящие» / «Прошедшие» / «Без даты») — разделитель старше недели,
 * поэтому и подложка глубже (`--tf-ground` против `--tf-surface` у недели), и
 * буква крупнее. Спор со строкой недели решает порядок: правило раздела ниже.
 */
.tf-plan-section { --bs-table-accent-bg: var(--tf-ground); }
/* Селектор с двумя классами: у отступа ячейки (`.table > :not(caption) > * > *`)
   специфичность выше, чем у одиночного класса, и padding-top иначе не доедет. */
.table .tf-plan-section > * {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-h4);
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--tf-ink);
  /* Подложка раздела совпадает с фоном страницы, поэтому границу и воздух
     сверху рисуем явно: иначе переход от прошедшего к будущему читается как
     случайный пропуск строки. */
  border-top: 2px solid var(--tf-hair);
  padding-top: var(--tf-sp-5);
}
/* Сегодня — предстоящая тренировка и единственная выделенная строка; прошедшие
   приглушены целым разделом, поэтому подсветка «сегодня» остаётся заметной. */
.tf-row--today { --bs-table-accent-bg: var(--tf-track-soft); }
/* Прошедшее — фон не трогаем: приглушаем только текст, иначе полстраницы
   становится подложкой и «сегодня» перестаёт выделяться. */
.tf-row--past > * { color: var(--tf-ink-2); }

/*
 * Задание переносится по любому месту, а дата и числа — не переносятся вовсе:
 * «1км+10x400/400+4x200» и «2026-08-19» пробелов не содержат, поэтому без этого
 * первое уезжает за край, а второе рвётся посередине.
 */
.tf-plan-table td:not(.tf-num) { overflow-wrap: anywhere; }
.tf-plan-table td.tf-num, .tf-plan-table th.tf-num { white-space: nowrap; }
/*
 * Клетка «факт / план» — исключение из `nowrap`: в неё встали ДВА числа
 * («21.12 / 24»), и при фиксированной ширине неперенос не сжимает клетку, а
 * выпускает текст на соседнюю колонку — так «Км» залезло на «Время»
 * (найдено владельцем 2026-09-01). Пара переносится на две строки, само число
 * не рвётся: цифры пробелов не содержат, а `overflow-wrap` тут не `anywhere`.
 * Единица («км») в клетке не печатается вовсе — она в заголовке колонки.
 */
.tf-plan-table td.tf-num.tf-metric { white-space: normal; overflow-wrap: normal; }
/*
 * Кнопка в колонке действий из «переносится по любому месту» исключена: перенос
 * рвал надпись внутри самой кнопки, и от «Отчитаться» на десктопе отваливалась
 * последняя буква. Ширину колонки ниже держим с запасом — при `nowrap` узкая
 * колонка выпускает кнопку за край клетки, а не сжимает её.
 */
.tf-plan-table td.tf-act { overflow-wrap: normal; white-space: nowrap; }

@media (min-width: 48em) {
  .tf-plan-table { table-layout: fixed; }
  .tf-plan-table col.tf-col-date { width: 8.5rem; }
  .tf-plan-table col.tf-col-sport { width: 6.5rem; }
  /*
   * 5rem держим сознательно, хотя в двух из трёх числовых колонок теперь пара
   * «21.12 / 24»: сумма фиксированных ширин уже 46.5rem, и любое расширение
   * переваливает за 768 px — то есть даёт горизонтальную прокрутку ровно на
   * границе, с которой таблица включается (specs/design-system.md: страница не
   * должна ехать вбок). Пара вместо этого переносится на две строки, см.
   * `.tf-metric` выше.
   */
  .tf-plan-table col.tf-col-num { width: 5rem; }
  .tf-plan-table col.tf-col-status { width: 8rem; }
  /* В колонке действий одна кнопка «Отчитаться»: правка ушла ссылкой на название. */
  .tf-plan-table col.tf-col-act { width: 8.5rem; }
  .table td.tf-num, .table th.tf-num { text-align: right; }
}

.grid-view { overflow-x: auto; }
/* Шапка и кнопки строки в одну строку — только пока это таблица, а не карточки */
.grid-view th { white-space: nowrap; }
@media (min-width: 48em) {
  .grid-view td:last-child { white-space: nowrap; }
}
.grid-view .summary,
.grid-view .empty {
  color: var(--tf-fog);
  font-size: var(--tf-size-small);
  padding-bottom: var(--tf-sp-2);
}
.grid-view .filters .form-control { padding: 5px var(--tf-sp-2); font-size: var(--tf-size-small); }
.grid-view a.asc:after { border-bottom-color: var(--tf-track); }
.grid-view a.desc:after { border-top-color: var(--tf-track); }

/*
 * Таблица → карточки. Список из восьми колонок на 375 px читается только
 * прокруткой вбок, а аудитория кабинета мобильная, поэтому ниже 48em строка
 * становится карточкой: шапка прячется, подпись печатается перед значением из
 * `data-label`. Атрибут проставляет common\widgets\CardGrid (или вью руками у
 * рукописных таблиц) — разметка остаётся одна на оба режима.
 *
 * Исключение — предпросмотр импорта: там таблица и есть предмет разговора
 * (строки исходного файла), её оставляем в .table-responsive с прокруткой.
 */
@media (max-width: 47.98em) {
  .tf-cards { border: 0; margin-bottom: 0; }
  /* шапка нужна только скринридеру: подписи выводит ::before ячейки */
  .tf-cards > thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .tf-cards, .tf-cards > tbody, .tf-cards > tbody > tr, .tf-cards > tbody > tr > td { display: block; width: auto; }
  .tf-cards > tbody > tr {
    background: var(--tf-surface);
    border: var(--tf-border);
    border-radius: var(--tf-radius-lg);
    padding: var(--tf-sp-2) var(--tf-sp-4);
    margin-bottom: var(--tf-sp-3);
  }
  .tf-cards > tbody > tr > td {
    display: grid;
    grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr);
    gap: var(--tf-sp-3);
    align-items: baseline;
    padding: var(--tf-sp-2) 0;
    border: 0;
    border-bottom: 1px solid var(--tf-hair);
    text-align: left;
    /* «1км+10x400/400+4x200» — одно слово без пробелов: без переноса по любому
       месту оно уезжает за край карточки. */
    overflow-wrap: anywhere;
  }
  .tf-cards > tbody > tr > td:last-child { border-bottom: 0; }
  /* ячейка без значения: в таблице она держит колонку, в карточке — только шумит */
  .tf-cards > tbody > tr > td.tf-void { display: none; }
  .tf-cards > tbody > tr > td::before {
    content: attr(data-label);
    font-family: var(--tf-font-display);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--tf-fog);
  }
  /* ячейка без подписи — это кнопки строки: они занимают всю ширину карточки */
  .tf-cards > tbody > tr > td:not([data-label]) {
    display: flex;
    flex-wrap: wrap;
    gap: var(--tf-sp-2);
    align-items: center;
  }
  .tf-cards > tbody > tr > td:not([data-label])::before { content: none; }
  /* ячейка на всю строку (пустой список, итог) — обычный блок, а не кнопки */
  .tf-cards > tbody > tr > td[colspan] { display: block; }
  /* разделители раздела и недели — заголовки над карточками, а не пустые карточки */
  .tf-cards > tbody > tr.tf-plan-section,
  .tf-cards > tbody > tr.tf-plan-week {
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: var(--tf-sp-4) 0 var(--tf-sp-1);
    margin: 0;
  }
  /* box-shadow, а не background: подложку строки бутстрап рисует именно им */
  .tf-cards > tbody > tr.tf-plan-section > td,
  .tf-cards > tbody > tr.tf-plan-week > td { box-shadow: none; padding: 0; border: 0; }
  /* раздел открывает список — сверху воздуха больше, чем у недели внутри него */
  .tf-cards > tbody > tr.tf-plan-section { padding-top: var(--tf-sp-5); }
  /* Сегодня — карточка целиком, а не её клетки: подложка на клетках
     обрывалась перед кнопкой и выглядела недокрашенной. */
  .tf-cards > tbody > tr.tf-row--today { background: var(--tf-track-soft); }
  /*
   * Пустой день недели — одна тихая строка, а не карточка (specs/plan-calendar.md).
   * Карточкой он занимает столько же места, сколько настоящая тренировка, и на
   * телефоне четыре дня отдыха выдавливают за экран то, ради чего экран открыли.
   * Целью переноса строка при этом остаётся — у неё есть площадь и `data-tf-day`.
   */
  .tf-cards > tbody > tr.tf-day-empty {
    display: flex;
    gap: var(--tf-sp-2);
    align-items: baseline;
    background: transparent;
    border: 0;
    border-bottom: var(--tf-border);
    border-radius: 0;
    padding: var(--tf-sp-2) var(--tf-sp-2);
    margin: 0;
    font-size: var(--tf-size-small);
  }
  .tf-cards > tbody > tr.tf-day-empty > td {
    display: block;
    border: 0;
    padding: 0;
    box-shadow: none;
  }
  .tf-cards > tbody > tr.tf-day-empty > td::before { content: none; }
  .tf-cards > tbody > tr.tf-row-race {
    display: flex;
    gap: var(--tf-sp-2);
    align-items: baseline;
    background: var(--tf-info-soft);
    border: 0;
    border-bottom: var(--tf-border);
    border-radius: 0;
    padding: var(--tf-sp-2);
    margin: 0;
    font-size: var(--tf-size-small);
  }
  .tf-cards > tbody > tr.tf-row-race > td {
    display: block;
    border: 0;
    padding: 0;
    box-shadow: none;
  }
  .tf-cards > tbody > tr.tf-row-race > td::before { content: none; }
  .tf-cards > tbody > tr.tf-row--today > td { box-shadow: none; }
  .tf-cards > tbody > tr > td:not([data-label]) > .btn,
  .tf-cards > tbody > tr > td:not([data-label]) > form { flex: 1 1 auto; }
  .tf-cards > tbody > tr > td:not([data-label]) > form > .btn { width: 100%; }
  .tf-cards > tbody > tr > td form { margin: 0; }
  /* сама таблица уже не широкая — прокрутка обёртки только мешала бы */
  .grid-view:has(.tf-cards) { overflow-x: visible; }
}

/* ——————————————————————————— Статусы ——————————————————————————— */

.alert {
  border-radius: var(--tf-radius);
  border-left-width: 3px;
  font-size: var(--tf-size-small);
}
.alert-success { --bs-alert-bg: var(--tf-ok-soft); --bs-alert-color: var(--tf-ok); --bs-alert-border-color: var(--tf-ok); }
.alert-info { --bs-alert-bg: var(--tf-info-soft); --bs-alert-color: var(--tf-info); --bs-alert-border-color: var(--tf-info); }
.alert-warning { --bs-alert-bg: var(--tf-warn-soft); --bs-alert-color: var(--tf-warn); --bs-alert-border-color: var(--tf-warn); }
.alert-danger { --bs-alert-bg: var(--tf-danger-soft); --bs-alert-color: var(--tf-danger); --bs-alert-border-color: var(--tf-danger); }

/*
 * Пилюля состояния: цвет плюс текст, потому что цветом одним состояние
 * передавать нельзя (дальтонизм, печать, ч/б скриншот).
 */
.tf-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--tf-sp-2);
  font-family: var(--tf-font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: var(--tf-radius-pill);
  background: var(--tf-surface-2);
  border: var(--tf-border);
  color: var(--tf-ink-2);
  white-space: nowrap;
}
.tf-pill::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.tf-pill--ok { background: var(--tf-ok-soft); border-color: var(--tf-ok); color: var(--tf-ok); }
.tf-pill--warn { background: var(--tf-warn-soft); border-color: var(--tf-warn); color: var(--tf-warn); }
.tf-pill--danger { background: var(--tf-danger-soft); border-color: var(--tf-danger); color: var(--tf-danger); }
.tf-pill--accent { background: var(--tf-track-soft); border-color: var(--tf-track); color: var(--tf-track); }
.tf-pill--plain::before { display: none; }

/* Слоты напоминаний: цвет помогает не путать «вечером накануне» с «утром» */
.tf-slot { color: var(--tf-ink-2); }
.tf-slot--eve::before { background: var(--tf-slot-eve); }
.tf-slot--morn::before { background: var(--tf-slot-morn); }
.tf-slot--day::before { background: var(--tf-slot-day); }

.badge {
  font-family: var(--tf-font-display);
  font-weight: 600;
  letter-spacing: .04em;
  border-radius: var(--tf-radius);
  /* Та же причина, что у кнопок: заливка бейджа — акцент или статус. */
  --bs-badge-color: var(--tf-on-accent);
}
.bg-primary { background-color: var(--tf-track) !important; }
.bg-success { background-color: var(--tf-pine) !important; }
.bg-danger { background-color: var(--tf-danger) !important; }
.bg-secondary { background-color: var(--tf-fog) !important; }

/* ——————————————————————————— Прочие компоненты бутстрапа ——————————————————————————— */

.breadcrumb {
  --bs-breadcrumb-divider-color: var(--tf-fog);
  --bs-breadcrumb-item-active-color: var(--tf-ink-2);
  font-family: var(--tf-font-display);
  font-size: 12.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding-top: var(--tf-sp-4);
}
/*
 * Пейджер грида. Бутстрапная разметка (page-item / page-link) приходит от
 * подмены LinkPager в DI — см. `container` в common/config/main.php; без неё от
 * этого блока работают только переменные, а кнопки остаются голыми ссылками.
 *
 * Кнопки стоят отдельными плитками, а не слитной панелью: gap вместо
 * бутстрапного «минус один пиксель» и радиус на каждой. На телефоне такая
 * строка переносится, не оставляя обрубка слитной группы.
 */
.pagination {
  --bs-pagination-bg: var(--tf-surface);
  --bs-pagination-color: var(--tf-ink-2);
  --bs-pagination-border-color: var(--tf-hair);
  --bs-pagination-hover-bg: var(--tf-surface-2);
  --bs-pagination-hover-color: var(--tf-track);
  --bs-pagination-hover-border-color: var(--tf-hair);
  --bs-pagination-focus-bg: var(--tf-surface-2);
  --bs-pagination-focus-color: var(--tf-track);
  --bs-pagination-focus-box-shadow: var(--tf-focus);
  --bs-pagination-active-bg: var(--tf-track);
  --bs-pagination-active-border-color: var(--tf-track);
  --bs-pagination-disabled-bg: var(--tf-surface);
  --bs-pagination-disabled-color: var(--tf-fog);
  --bs-pagination-disabled-border-color: var(--tf-hair);
  --bs-pagination-border-radius: var(--tf-radius);
  /* Кегль и отбивка — мобильные; на десктопе кнопки крупнее, см. ниже. */
  --bs-pagination-padding-x: var(--tf-sp-2);
  --bs-pagination-padding-y: var(--tf-sp-1);
  --bs-pagination-font-size: var(--tf-size-small);
  font-family: var(--tf-font-display);
  font-variant-numeric: tabular-nums;
  flex-wrap: wrap;
  gap: var(--tf-sp-1);
  margin-bottom: 0;
}
.pagination .page-link {
  min-width: 30px;
  text-align: center;
  border-radius: var(--tf-radius);
  /* Бутстрап сдвигает кнопки на -1px, чтобы границы слились; нам нужен зазор. */
  margin-left: 0;
  transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}
.page-item.active .page-link { font-weight: 600; }
/* Шевроны: тот же кегль читается мельче цифр, поэтому чуть крупнее. */
.pagination .prev .page-link,
.pagination .next .page-link { font-size: 17px; line-height: 1.25; }

/*
 * Подвал грида: итог («Показаны записи 1-20 из 57») и страницы — одной строкой
 * состояния под таблицей. У таблицы на одну страницу оба слота пустые — подвал в этом
 * случае не должен занимать места, отсюда `:empty`.
 */
.tf-grid-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--tf-sp-2);
  padding-top: var(--tf-sp-3);
}
.tf-grid-foot:empty { display: none; }
.tf-grid-foot .summary {
  width: 100%;
  padding-bottom: 0;
  text-align: center;
  color: var(--tf-fog);
  font-size: var(--tf-size-small);
}
/*
 * На десктопе итог и страницы разъезжаются по краям строки; на телефоне это
 * два центрированных ряда — иначе девять кнопок в 375 px не влезают и ряд
 * обрывается одинокой стрелкой у правого края.
 */
@media (min-width: 48em) {
  .pagination { --bs-pagination-padding-x: var(--tf-sp-3); }
  .pagination .page-link { min-width: 34px; }
  .tf-grid-foot { justify-content: space-between; gap: var(--tf-sp-3); }
  .tf-grid-foot .summary { width: auto; text-align: left; }
  .tf-grid-foot .tf-pager { margin-left: auto; }
}

.dropdown-menu {
  --bs-dropdown-bg: var(--tf-surface-2);
  --bs-dropdown-color: var(--tf-ink);
  --bs-dropdown-border-color: var(--tf-hair);
  --bs-dropdown-border-radius: var(--tf-radius);
  --bs-dropdown-link-color: var(--tf-ink);
  --bs-dropdown-link-hover-bg: var(--tf-surface);
  --bs-dropdown-link-hover-color: var(--tf-track);
  --bs-dropdown-link-active-bg: var(--tf-track);
  --bs-dropdown-divider-bg: var(--tf-hair);
  font-family: var(--tf-font-display);
  font-size: 14px;
}
.modal-content {
  background-color: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  color: var(--tf-ink);
}
.modal-header, .modal-footer { border-color: var(--tf-hair); }
.list-group {
  --bs-list-group-bg: var(--tf-surface);
  --bs-list-group-color: var(--tf-ink);
  --bs-list-group-border-color: var(--tf-hair);
  --bs-list-group-action-hover-bg: var(--tf-surface-2);
  --bs-list-group-active-bg: var(--tf-track);
  --bs-list-group-active-border-color: var(--tf-track);
}
.nav-tabs {
  --bs-nav-tabs-border-color: var(--tf-hair);
  --bs-nav-tabs-link-active-bg: var(--tf-surface);
  --bs-nav-tabs-link-active-color: var(--tf-track);
  --bs-nav-tabs-link-active-border-color: var(--tf-hair);
  --bs-nav-link-color: var(--tf-ink-2);
  --bs-nav-link-hover-color: var(--tf-track);
  font-family: var(--tf-font-display);
  text-transform: uppercase;
  font-size: 14px;
  letter-spacing: .04em;
}
.progress {
  background-color: var(--tf-hair);
  border-radius: var(--tf-radius);
}
.progress-bar { background-color: var(--tf-track); }

/* Вход/регистрация: узкая колонка по центру, знак сверху */
.tf-auth {
  max-width: 400px;
  margin: 0 auto;
  padding: var(--tf-sp-8) 0 var(--tf-sp-7);
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-5);
}
.tf-auth > .tf-lockup { align-self: center; }

/* ——————————————————————————— Лендинг ——————————————————————————— */

/*
 * Секции публичной страницы (см. specs/landing.md). Живут здесь, а не в
 * frontend/web/css/site.css, потому что лендинг школы (роадмап §6) соберётся из
 * тех же блоков, а файл приложения грузится последним и перебивает токены.
 * Раскладка — от узкого экрана: одна колонка, вторая появляется на ≥ 62em.
 *
 * Ни одного цветового литерала: витрина живёт в обеих темах, а фотографии и
 * скриншоты — единственное, что темы не знает, поэтому под них есть свои
 * токены (--tf-scrim, --tf-on-photo).
 */

.tf-section {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-5);
  padding: var(--tf-sp-7) 0;
  border-top: var(--tf-border);
  /* якорь из меню не должен уезжать под шапку */
  scroll-margin-top: var(--tf-sp-5);
}
.tf-section__head { display: flex; flex-direction: column; gap: var(--tf-sp-2); }
.tf-section__head p { margin: 0; }
/* Шапка с действием справа: «Все клубы» рядом с заголовком витрины */
.tf-section__head--row {
  flex-direction: row;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--tf-sp-4);
}

/* ——— Первый экран: поиск слева, коллаж справа ——— */

.tf-hero {
  display: grid;
  gap: var(--tf-sp-6);
  padding: var(--tf-sp-7) 0 var(--tf-sp-6);
  align-items: center;
}
.tf-hero__text { display: flex; flex-direction: column; gap: var(--tf-sp-4); align-items: flex-start; }
.tf-hero__text h1 { margin: 0; }
.tf-hero__text p { margin: 0; }
.tf-hero--search .tf-hero__text { gap: var(--tf-sp-5); }

@media (min-width: 62em) {
  .tf-hero { grid-template-columns: 1.05fr .95fr; gap: var(--tf-sp-7); }
}

/*
 * Поиск по каталогу прямо на первом экране. Это GET-форма в `/clubs`, а не
 * отдельный поиск: два разных поиска по одному и тому же расходятся на первой
 * же правке, а адрес с фильтрами люди пересылают друг другу.
 */
.tf-search {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--tf-sp-3);
  width: 100%;
  padding: var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  box-shadow: var(--tf-shadow);
}
/* min-width:0 обязателен: иначе поле не даёт флексу сжаться и строка едет вправо */
.tf-search__field { display: flex; flex-direction: column; gap: var(--tf-sp-1); flex: 1 1 150px; min-width: 0; }
.tf-search__field--wide { flex: 2 1 200px; }
.tf-search__label {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-fog);
}
.tf-search__go { flex: 1 1 100%; }
@media (min-width: 48em) {
  .tf-search__go { flex: 0 0 auto; }
}

/* Города одной строкой: клик — это готовый запрос, а не подсказка */
.tf-pills { display: flex; flex-wrap: wrap; align-items: center; gap: var(--tf-sp-2); margin: 0; }
.tf-pill {
  display: inline-block;
  padding: 3px 12px;
  border: var(--tf-border);
  border-radius: var(--tf-radius-pill);
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
  text-decoration: none;
  transition: color .16s ease, border-color .16s ease, background-color .16s ease;
}
.tf-pill:hover { color: var(--tf-track); border-color: var(--tf-track); background: var(--tf-track-soft); }

/* Числа витрины. Показываются только когда их не стыдно назвать — это решает вью */
.tf-stats { display: flex; flex-wrap: wrap; gap: var(--tf-sp-6); list-style: none; margin: 0; padding: 0; }
.tf-stat { display: flex; flex-direction: column; gap: var(--tf-sp-1); }
.tf-stat__num {
  font-family: var(--tf-font-display);
  font-size: 36px;
  font-weight: 700;
  line-height: 1;
  color: var(--tf-ink);
  font-variant-numeric: tabular-nums;
}
.tf-stat__label { font-size: var(--tf-size-small); color: var(--tf-fog); }

/*
 * Коллаж: фотография задаёт настроение, телефон поверх неё показывает продукт.
 * Телефон вынесен за нижний край на треть — так он читается как отдельный
 * предмет, а не как часть снимка.
 */
.tf-collage { position: relative; padding-bottom: var(--tf-sp-6); }
.tf-collage__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--tf-radius-lg);
  box-shadow: var(--tf-shadow);
}
.tf-collage__phone { position: absolute; right: 4%; bottom: 0; width: 30%; max-width: 190px; min-width: 120px; }

/* ——— Рамки экранов: браузер и телефон ——— */

/*
 * Скриншот кабинета показывается фоном, а не `<img>`: тема у нас
 * переключается атрибутом `data-theme`, а `<picture>` так не умеет; два `<img>`
 * с display:none качали бы оба файла. Пути приходят переменными из вью
 * (--tf-shot / --tf-shot-dark), поэтому общий бандл не знает про чужой докрут.
 */
.tf-shot {
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  box-shadow: var(--tf-shadow);
  overflow: hidden;
}
.tf-shot__bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: var(--tf-sp-2) var(--tf-sp-3);
  background: var(--tf-surface);
  border-bottom: var(--tf-border);
}
.tf-shot__bar > span:not(.tf-shot__url) {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--tf-hair);
}
.tf-shot__url {
  flex: 1;
  margin-left: var(--tf-sp-2);
  padding: 2px var(--tf-sp-3);
  background: var(--tf-ground);
  border-radius: var(--tf-radius-pill);
  font-family: var(--tf-font-mono);
  font-size: 11px;
  color: var(--tf-fog);
}
.tf-shot__screen {
  display: block;
  background-image: var(--tf-shot);
  background-position: top center;
  background-size: 100% auto;
  background-repeat: no-repeat;
  background-color: var(--tf-surface-2);
}
.tf-shot--browser .tf-shot__screen { aspect-ratio: 16 / 10; }
/* Телефон: корпус — тот же чернильный тон в обеих темах, экран внутри */
.tf-shot--phone {
  padding: 6px;
  background: var(--tf-ink);
  border: none;
  border-radius: 20px;
}
.tf-shot--phone .tf-shot__screen { aspect-ratio: 39 / 78; border-radius: 15px; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tf-shot__screen { background-image: var(--tf-shot-dark, var(--tf-shot)); }
}
:root[data-theme="dark"] .tf-shot__screen { background-image: var(--tf-shot-dark, var(--tf-shot)); }

/* Скриншот и мокап переписки рядом */
.tf-showcase { display: grid; gap: var(--tf-sp-5); align-items: start; }
@media (min-width: 62em) {
  .tf-showcase { grid-template-columns: 1.45fr .8fr; }
}

/*
 * Переписка с ботом — мокап, а не снимок чужого Telegram: снимок означал бы
 * чужие имена и аватарки на нашей главной. Разметка на тех же токенах, поэтому
 * в тёмной теме мокап тёмный, а не светлое пятно.
 */
.tf-chat {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-3);
  padding: var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
}
.tf-chat__head {
  display: flex;
  align-items: center;
  gap: var(--tf-sp-2);
  padding-bottom: var(--tf-sp-2);
  border-bottom: var(--tf-border);
  font-family: var(--tf-font-display);
  font-weight: 600;
}
.tf-chat__msg {
  align-self: flex-start;
  max-width: 94%;
  padding: var(--tf-sp-3);
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  font-size: var(--tf-size-small);
}
.tf-chat__msg p { margin: 0 0 var(--tf-sp-1); }
.tf-chat__msg--own { align-self: flex-end; background: var(--tf-track-soft); border-color: var(--tf-track); }
.tf-chat__msg--react {
  align-self: center;
  max-width: none;
  padding: 0;
  background: none;
  border: none;
  font-family: var(--tf-font-mono);
  font-size: 12px;
  color: var(--tf-fog);
}
.tf-chat__time { display: block; text-align: right; font-family: var(--tf-font-mono); font-size: 11px; color: var(--tf-fog); }

/* ——— Карточки витрины ——— */

/* Сетка карточек: число колонок задаёт ширина, а не медиавыражения */
.tf-grid {
  display: grid;
  gap: var(--tf-sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}

.tf-feature {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-2);
  padding: var(--tf-sp-5);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
}
.tf-feature h3 { margin: 0; font-size: var(--tf-size-h4); }
.tf-feature p { margin: 0; color: var(--tf-ink-2); font-size: var(--tf-size-small); }
.tf-feature--accent { border-color: var(--tf-track); }

/* Клуб: знак, имя, город и направления — то, что человек читает глазами */
.tf-club,
.tf-person {
  display: flex;
  gap: var(--tf-sp-3);
  padding: var(--tf-sp-5);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.tf-club { flex-direction: column; }
.tf-club:hover,
.tf-person:hover { transform: translateY(-2px); border-color: var(--tf-track); box-shadow: var(--tf-shadow); }
.tf-club__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--tf-radius-lg);
  background: var(--tf-track-soft) center / cover no-repeat;
  color: var(--tf-track);
  font-family: var(--tf-font-display);
  font-size: 22px;
  font-weight: 700;
}
.tf-club__name { margin: 0; font-size: var(--tf-size-h4); }
.tf-club__about { margin: 0; color: var(--tf-ink-2); font-size: var(--tf-size-small); }
.tf-club p,
.tf-person p { margin: 0; }

.tf-person { align-items: flex-start; gap: var(--tf-sp-4); }
.tf-person__body { display: flex; flex-direction: column; gap: var(--tf-sp-1); min-width: 0; }
.tf-person__name { margin: 0; font-size: var(--tf-size-h4); }
.tf-person__headline { color: var(--tf-ink-2); font-size: var(--tf-size-small); }

/* Сбор: дата отдельным блоком — по ней глаз и ищет */
.tf-events {
  display: grid;
  gap: var(--tf-sp-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}
.tf-event {
  display: flex;
  gap: var(--tf-sp-4);
  padding: var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-left: 3px solid var(--tf-track);
  border-radius: var(--tf-radius-lg);
}
.tf-event__date {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: 52px;
}
.tf-event__day { font-family: var(--tf-font-display); font-size: 30px; font-weight: 700; line-height: 1; color: var(--tf-ink); }
.tf-event__month,
.tf-event__weekday { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--tf-fog); }
.tf-event__body { display: flex; flex-direction: column; gap: var(--tf-sp-1); min-width: 0; }
.tf-event__title { margin: 0; font-size: var(--tf-size-h4); }
.tf-event__club { font-size: var(--tf-size-small); }
.tf-event p { margin: 0; }

/* ——— Две колонки: текст и картинка ——— */

.tf-split { display: grid; gap: var(--tf-sp-6); align-items: center; }
.tf-split__text { display: flex; flex-direction: column; gap: var(--tf-sp-5); }
.tf-split__art { display: flex; flex-direction: column; gap: var(--tf-sp-4); }
.tf-split__photo {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  max-height: 560px;
  object-fit: cover;
  border-radius: var(--tf-radius-lg);
  box-shadow: var(--tf-shadow);
}
@media (min-width: 62em) {
  .tf-split { grid-template-columns: 1.1fr .9fr; }
  /* Обратный порядок только там, где колонки рядом: в столбик текст идёт первым */
  .tf-split--reverse .tf-split__art { order: -1; }
}

/* Шаги сценария: номер — тем же дисплейным шрифтом, что заголовки */
.tf-steps {
  display: grid;
  gap: var(--tf-sp-5);
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: tf-step;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
}
.tf-steps--column { grid-template-columns: 1fr; gap: var(--tf-sp-4); }
.tf-step { display: flex; flex-direction: column; gap: var(--tf-sp-2); }
.tf-step h3 { margin: 0; font-size: var(--tf-size-h4); }
.tf-step p { margin: 0; color: var(--tf-ink-2); font-size: var(--tf-size-small); }
.tf-step::before {
  counter-increment: tf-step;
  content: counter(tf-step);
  font-family: var(--tf-font-display);
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  color: var(--tf-track);
  border-bottom: 2px solid var(--tf-hair);
  padding-bottom: var(--tf-sp-2);
}
/* В столбик номер встаёт слева от текста: сверху он рвал бы список на карточки */
.tf-steps--column .tf-step { display: grid; grid-template-columns: auto 1fr; gap: var(--tf-sp-2) var(--tf-sp-4); align-items: baseline; }
.tf-steps--column .tf-step::before { grid-row: span 2; border-bottom: none; padding-bottom: 0; }

/* График недели рядом со скриншотом тренера */
.tf-week {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-3);
  padding: var(--tf-sp-5);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
}
.tf-week p { margin: 0; }
.tf-week__days {
  display: flex;
  justify-content: space-between;
  /* тот же зазор, что у столбиков, иначе подписи разъезжаются с графиком */
  gap: 2.2%;
  font-family: var(--tf-font-display);
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-fog);
}
.tf-week__days span { flex: 1; text-align: center; }

/* Крупный график недели: тот же элемент, что знак, только в рост экрана */
.tf-load--week { height: 132px; gap: 2.2%; }
.tf-load--week .tf-load__bar { width: 100%; border-radius: var(--tf-radius); }

/* ——— Полоса с фотографией: финальный призыв ——— */

.tf-band {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 340px;
  border-radius: var(--tf-radius-lg);
  overflow: hidden;
  /* фон и затемнение лежат под текстом, но внутри своего слоя */
  isolation: isolate;
}
.tf-band__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
/* Затемнение поверх фотографии: без него текст читается через раз, а не всегда */
.tf-band::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(100deg, var(--tf-scrim) 0%, var(--tf-scrim) 45%, var(--tf-scrim-soft) 100%);
}
.tf-band__text { display: flex; flex-direction: column; gap: var(--tf-sp-4); max-width: 640px; padding: var(--tf-sp-6); }
/*
 * Текст на фотографии светлый в ОБЕИХ темах: фотография темы не знает, и
 * «как в тёмной теме» здесь означало бы чёрный текст на чёрном снимке.
 */
.tf-band__text,
.tf-band__text h2,
.tf-band__text .tf-lede,
.tf-band__text .tf-meta { color: var(--tf-on-photo); }
.tf-band__text h2 { margin: 0; }
.tf-band__text p { margin: 0; }
.tf-band .btn-outline-secondary { color: var(--tf-on-photo); border-color: var(--tf-on-photo); }
.tf-band .btn-outline-secondary:hover { color: var(--tf-ink); background: var(--tf-on-photo); border-color: var(--tf-on-photo); }

/* Финальная секция без верхней линейки: полоса и так отделена собой */
.tf-section--last { border-top: none; }

/* Финальный призыв (оставлен для страниц, где полосы с фотографией нет) */
.tf-cta {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-4);
  align-items: flex-start;
  padding: var(--tf-sp-6);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-left: 3px solid var(--tf-track);
  border-radius: var(--tf-radius-lg);
}
.tf-cta h2 { margin: 0; }
.tf-cta p { margin: 0; }

/*
 * Плашка: статус беты и предупреждения. Не alert — она не про результат
 * действия, а про постоянное свойство сервиса.
 */
.tf-note {
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-2);
  padding: var(--tf-sp-4);
  background: var(--tf-warn-soft);
  border-left: 3px solid var(--tf-warn);
  border-radius: var(--tf-radius);
  color: var(--tf-ink-2);
  font-size: var(--tf-size-small);
}
.tf-note p { margin: 0; }
.tf-note--plain { background: var(--tf-surface); border-left-color: var(--tf-hair); }

/* ——————————————————————————— Появление при прокрутке ——————————————————————————— */

/*
 * Секция проявляется, когда до неё долистали (js/reveal.js вешает `is-in`).
 *
 * Прячем только при живом JS: класс `tf-js` ставит сам скрипт. Иначе
 * выключенный или упавший скрипт означал бы пустую белую страницу — цена
 * анимации не должна быть такой.
 *
 * `prefers-reduced-motion` выключает всё: для части людей движение на экране —
 * это тошнота и головная боль, а не украшение.
 */
.tf-js .tf-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .55s ease, transform .55s ease;
}
.tf-js .tf-reveal.is-in { opacity: 1; transform: none; }
.tf-js .tf-reveal--delay { transition-delay: .12s; }

/* Столбики недели вырастают, когда график попал на экран */
.tf-load--week .tf-load__bar { height: var(--tf-bar-h); }
.tf-js .tf-reveal .tf-load--week .tf-load__bar { height: 0; transition: height .7s cubic-bezier(.2, .7, .3, 1); }
.tf-js .tf-reveal.is-in .tf-load--week .tf-load__bar { height: var(--tf-bar-h); }
.tf-load--week .tf-load__bar:nth-child(2) { transition-delay: .06s; }
.tf-load--week .tf-load__bar:nth-child(3) { transition-delay: .12s; }
.tf-load--week .tf-load__bar:nth-child(4) { transition-delay: .18s; }
.tf-load--week .tf-load__bar:nth-child(5) { transition-delay: .24s; }
.tf-load--week .tf-load__bar:nth-child(6) { transition-delay: .3s; }
.tf-load--week .tf-load__bar:nth-child(7) { transition-delay: .36s; }

/* Телефон на первом экране чуть покачивается — единственное вечное движение */
@media (prefers-reduced-motion: no-preference) {
  .tf-collage__phone { animation: tf-float 7s ease-in-out infinite; }
}
@keyframes tf-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

@media (prefers-reduced-motion: reduce) {
  .tf-js .tf-reveal,
  .tf-js .tf-reveal.is-in { opacity: 1; transform: none; transition: none; }
  .tf-js .tf-reveal .tf-load--week .tf-load__bar { height: var(--tf-bar-h); transition: none; }
  .tf-club,
  .tf-person,
  .tf-pill { transition: none; }
}

/* На печати ничего не проявляется: страница печатается целиком или никак */
@media print {
  .tf-js .tf-reveal { opacity: 1; transform: none; }
  .tf-js .tf-reveal .tf-load--week .tf-load__bar { height: var(--tf-bar-h); }
}

/* ——————————————————————————— Юридический документ ——————————————————————————— */

/*
 * Длинный текст с нумерацией разделов. Ширина ограничена: политику читают
 * глазами, а не по диагонали, и строка в 120 знаков этому мешает.
 */
.tf-doc {
  max-width: 72ch;
  counter-reset: tf-doc-section;
}
.tf-doc > section { margin-top: var(--tf-sp-6); scroll-margin-top: var(--tf-sp-5); }
.tf-doc > section > h2 {
  font-size: var(--tf-size-h3);
  text-transform: none;
  letter-spacing: -.01em;
  margin: 0 0 var(--tf-sp-3);
}
.tf-doc > section > h2::before {
  counter-increment: tf-doc-section;
  content: counter(tf-doc-section) '. ';
  color: var(--tf-track);
}
.tf-doc h3 { font-size: var(--tf-size-h4); margin: var(--tf-sp-4) 0 var(--tf-sp-2); }
.tf-doc p { margin: 0 0 var(--tf-sp-3); }
.tf-doc ul, .tf-doc ol { margin: 0 0 var(--tf-sp-3); padding-left: var(--tf-sp-5); }
.tf-doc li { margin-bottom: var(--tf-sp-2); }
.tf-doc li::marker { color: var(--tf-fog); }
.tf-doc dl { margin: 0 0 var(--tf-sp-3); }
.tf-doc dt { font-weight: 700; }
.tf-doc dd { margin: 0 0 var(--tf-sp-2); color: var(--tf-ink-2); }

/* Оглавление документа */
.tf-toc {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tf-sp-2) var(--tf-sp-4);
  list-style: none;
  margin: 0;
  padding: var(--tf-sp-4);
  background: var(--tf-surface);
  border: var(--tf-border);
  border-radius: var(--tf-radius-lg);
  font-family: var(--tf-font-display);
  font-size: 13px;
}
.tf-toc a { color: var(--tf-ink-2); text-decoration: none; }
.tf-toc a:hover { color: var(--tf-track); text-decoration: underline; }

/* Реквизиты: пары «что — значение», данные моноширинным */
.tf-facts { margin: 0; display: grid; gap: var(--tf-sp-2); }
.tf-facts dt {
  font-family: var(--tf-font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tf-fog);
}
.tf-facts dd { margin: 0; color: var(--tf-ink); }
@media (min-width: 40em) {
  .tf-facts { grid-template-columns: max-content 1fr; column-gap: var(--tf-sp-5); align-items: baseline; }
}

/*
 * Сводка на панели кабинета (specs/cabinet-revision.md, «Панель — сводка, а не
 * меню разделов»): список «кто — когда — что» внутри карточки.
 *
 * Имя стоит первым и является ссылкой: тренер читает строку как «Петров,
 * пятница, темповый» и щёлкает по человеку, а не по дате. Дата — второй, но
 * своим шрифтом и цветом: она различает строки одного человека.
 */
.tf-sum {
  list-style: none;
  margin: 0 0 var(--tf-sp-3);
  padding: 0;
  display: grid;
  gap: var(--tf-sp-2);
}
.tf-sum__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  gap: 0 var(--tf-sp-2);
  align-items: baseline;
  border-top: var(--tf-border);
  padding-top: var(--tf-sp-2);
}
.tf-sum__row:first-child { border-top: 0; padding-top: 0; }
.tf-sum__who { font-weight: 600; }
.tf-sum__when {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  color: var(--tf-fog);
  white-space: nowrap;
}
/* Название занимает всю ширину строкой ниже: «1км+10х400/400+4х200/200+1км»
   рядом с именем не помещается ни на каком экране. */
.tf-sum__what {
  grid-column: 1 / -1;
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
  overflow-wrap: anywhere;
}
/* Итог недели крупно: единственное число, ради которого карточку открывают. */
.tf-sum__big {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-h2);
  line-height: 1.1;
  margin-bottom: var(--tf-sp-2);
}
.tf-sum__unit { font-size: var(--tf-size-body); color: var(--tf-fog); }

/*
 * Сегодняшнее занятие на панели ученика (specs/cabinet-revision.md, итерация 6).
 *
 * Название крупнее строки списка и стоит первым: человек открыл кабинет ради
 * него. Время — рядом моноширинным, как во всех сводках; шаги занятия печатает
 * общий `.tf-workout-outline`, чтобы «10х400/400» выглядело одинаково в плане,
 * в предпросмотре ИИ и здесь.
 */
.tf-today + .tf-today {
  border-top: var(--tf-border);
  margin-top: var(--tf-sp-3);
  padding-top: var(--tf-sp-3);
}
.tf-today__title {
  font-size: var(--tf-size-h4);
  font-weight: 600;
  margin-bottom: var(--tf-sp-1);
}
.tf-today__when {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  font-weight: 400;
  color: var(--tf-fog);
  margin-left: var(--tf-sp-2);
}
.tf-today__num {
  font-family: var(--tf-font-mono);
  color: var(--tf-ink-2);
  margin-bottom: var(--tf-sp-2);
}

/*
 * Список учеников как сводка (specs/cabinet-revision.md, «Список учеников —
 * две недели точками»).
 *
 * Строка раскрывается на месте обычным `<details>`: тренер обходит группу
 * сверху вниз, и переход на карточку и обратно ломал бы этот обход. Маркер
 * треугольника убран — его роль играет вся строка, она же и есть кнопка.
 */
.tf-roster {
  display: grid;
  gap: var(--tf-sp-2);
  margin-top: var(--tf-sp-4);
}
.tf-roster__item {
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  background: var(--tf-surface);
}
.tf-roster__item[open] { box-shadow: var(--tf-shadow); }
.tf-roster__row {
  display: grid;
  gap: var(--tf-sp-1) var(--tf-sp-3);
  align-items: center;
  padding: var(--tf-sp-3);
  cursor: pointer;
  list-style: none;
}
.tf-roster__row::-webkit-details-marker { display: none; }
.tf-roster__row:hover { background: var(--tf-surface-2); }
.tf-roster__name { font-weight: 600; }
.tf-roster__facts,
.tf-roster__race { font-size: var(--tf-size-small); color: var(--tf-ink-2); }
.tf-roster__num { display: block; }
.tf-roster__num + .tf-roster__num { color: var(--tf-fog); }

/*
 * Метка состояния — словами. Точки дают форму («подряд ли выпали дни»), но
 * ответ на вопрос «звонить ли» строка обязана давать текстом: то же требование,
 * что у плитки дня (итерация 1) — цвет вспомогательный, а не единственный.
 */
.tf-roster__flag {
  justify-self: start;
  font-size: var(--tf-size-small);
  font-weight: 600;
  padding: .1rem var(--tf-sp-2);
  border-radius: var(--tf-radius);
  white-space: nowrap;
}
.tf-roster__flag--ok { color: var(--tf-ok); background: var(--tf-ok-soft); }
.tf-roster__flag--silent { color: var(--tf-danger); background: var(--tf-danger-soft); }
.tf-roster__flag--new { color: var(--tf-ink-2); background: var(--tf-surface-2); }

/*
 * Как далась человеку неделя (specs/notifications.md, «Итерация 8»).
 *
 * Тона приглушённые и НЕ совпадают со светофором точек рядом: там речь о
 * выполнении плана, здесь — о самочувствии, и красное «тяжело» рядом с красным
 * «не отчитался» читалось бы как одна и та же беда. «Тяжело» — повод спросить,
 * а не провал, поэтому предупреждающий тон, а не опасный.
 */
.tf-roster__feel { justify-self: start; }
.tf-chip {
  display: inline-block;
  font-size: var(--tf-size-small);
  font-weight: 600;
  padding: .1rem var(--tf-sp-2);
  border-radius: var(--tf-radius);
  white-space: nowrap;
  color: var(--tf-ink-2);
  background: var(--tf-surface-2);
}
.tf-chip--easy { color: var(--tf-info); background: var(--tf-info-soft); }
.tf-chip--normal { color: var(--tf-ok); background: var(--tf-ok-soft); }
.tf-chip--hard { color: var(--tf-warn); background: var(--tf-warn-soft); }
.tf-chip--break { color: var(--tf-ink-2); background: var(--tf-surface-2); }

/*
 * Четырнадцать точек: две недели, старое слева, сегодня справа. Тренеру важно
 * не «79 %», а какой день выпал и подряд ли — поэтому день, а не процент.
 * Тона те же, что у календаря: точка — тот же светофор без цифр.
 */
.tf-roster__dots {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  align-items: center;
}
.tf-dot {
  width: .62rem;
  height: .62rem;
  border-radius: 50%;
  background: var(--tf-fog);
  flex: none;
}
.tf-dot--rest { background: transparent; box-shadow: inset 0 0 0 1px var(--tf-hair); }
.tf-dot.tf-tone--planned { background: var(--tf-info); }
.tf-dot.tf-tone--skipped { background: var(--tf-warn); }
.tf-dot.tf-tone--no_report { background: var(--tf-danger); }
.tf-dot.tf-tone--undated { background: var(--tf-fog); }
.tf-dot.tf-tone--done,
.tf-dot.tf-tone--done_on_target { background: var(--tf-ok); }
.tf-dot.tf-tone--done_under { background: var(--tf-danger); }
.tf-dot.tf-tone--done_over { background: var(--tf-info); }
/* Сегодня обведено: без опоры строка читается зеркально — «слева свежее». */
.tf-dot--today { box-shadow: 0 0 0 2px var(--tf-surface), 0 0 0 3px var(--tf-ink-2); }

.tf-roster__body {
  border-top: var(--tf-border);
  padding: var(--tf-sp-3);
}
.tf-roster__cols {
  display: grid;
  gap: var(--tf-sp-4);
}

/* Журнал тренера: заметки о человеке, которых не видит никто, кроме автора. */
.tf-notes {
  list-style: none;
  margin: 0 0 var(--tf-sp-3);
  padding: 0;
  display: grid;
  gap: var(--tf-sp-2);
}
.tf-notes__item {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  gap: var(--tf-sp-2);
  align-items: baseline;
  border-top: var(--tf-border);
  padding-top: var(--tf-sp-2);
}
.tf-notes__item:first-child { border-top: 0; padding-top: 0; }
.tf-notes__when {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  color: var(--tf-fog);
  white-space: nowrap;
}
.tf-notes__drop { padding: 0; font-size: var(--tf-size-small); color: var(--tf-fog); }

@media (min-width: 48em) {
  .tf-roster__row {
    grid-template-columns:
      minmax(8rem, 1fr) max-content minmax(0, 11rem) max-content max-content minmax(0, 9rem);
  }
  .tf-roster__cols { grid-template-columns: 1fr 1fr; }
}

/*
 * Перекличка на сборе (specs/events.md, «Сбор клуба: место встречи и
 * перекличка»). Список, а не таблица: строка отвечает на один вопрос — «был ли
 * человек», — и колонок для него не нужно.
 */
.tf-roll { margin: 0; }
.tf-roll__item {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--tf-sp-2);
  border-top: var(--tf-border);
  padding: var(--tf-sp-2) 0;
}
.tf-roll__item:first-child { border-top: 0; }
.tf-roll__item form { margin: 0; }
.tf-roll__name { font-weight: 600; }
.tf-roll__more { cursor: pointer; font-weight: 600; }

/* «Пришёл» — спокойное «всё хорошо», а не победа. */
.tf-roll__was {
  font-size: var(--tf-size-small);
  font-weight: 600;
  padding: .1rem var(--tf-sp-2);
  border-radius: var(--tf-radius);
  color: var(--tf-ok);
  background: var(--tf-ok-soft);
  white-space: nowrap;
}

/* ——————————————————————————— Подвал ——————————————————————————— */

.tf-footer {
  background: var(--tf-surface);
  border-top: var(--tf-border);
  color: var(--tf-fog);
  font-size: var(--tf-size-small);
  padding: var(--tf-sp-5) 0;
  margin-top: var(--tf-sp-7);
}
.tf-footer a { color: var(--tf-ink-2); }
.tf-footer a:hover { color: var(--tf-track); }
.tf-footer__inner {
  display: flex;
  justify-content: space-between;
  gap: var(--tf-sp-4);
  flex-wrap: wrap;
  align-items: center;
}
/* Ссылки на документы — на каждой странице, а не только там, где галочка */
.tf-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tf-sp-2) var(--tf-sp-4);
  font-family: var(--tf-font-display);
  font-size: 13px;
}
.tf-footer__links a { text-decoration: none; }
.tf-footer__links a:hover { text-decoration: underline; }

/* ——————————————————————————— Мобильная раскладка ——————————————————————————— */

/*
 * Кабинеты открывают с телефона (specs/pilot-launch.md, блок E), поэтому
 * правила ниже — не «адаптив по остаточному принципу», а часть системы:
 * панель действий, размер касания и поведение шапки описаны один раз здесь,
 * а не подбираются утилитами бутстрапа в каждом вью.
 */

/* Панель действий раздела: кнопки в ряд, на узком экране — в столбец */
.tf-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tf-sp-2);
  margin-bottom: var(--tf-sp-5);
}
.tf-actions form { margin: 0; }

/* Миниатюры загруженных скриншотов */
.tf-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--tf-sp-3);
}
.tf-thumb {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  background: var(--tf-surface-2);
}

/*
 * Скриншоты в списке — значок со счётчиком, не миниатюра: превью мы не делаем,
 * и два десятка строк означали бы два десятка полноразмерных картинок, каждая
 * через контроллер с проверкой прав. Картинки грузятся, только когда открыли.
 */
.tf-shots {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px var(--tf-sp-1);
  border-radius: var(--tf-radius);
  color: var(--tf-ink-2);
  text-decoration: none;
  vertical-align: middle;
}
.tf-shots:hover,
.tf-shots:focus-visible { color: var(--tf-track); background: var(--tf-surface-2); }
.tf-shots__icon { width: 18px; height: 18px; flex: 0 0 auto; }
.tf-shots__count {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-eyebrow);
  line-height: 1;
}

/*
 * Слайдер скриншотов (js/lightbox.js). Тёмная подложка в обеих темах: это
 * просмотр фотографии, а не часть страницы, и светлый фон вокруг скриншота
 * бьёт по глазам ровно там, где смотрят на цифры.
 */
.tf-lightbox-on { overflow: hidden; }
.tf-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1080;
  display: flex;
  flex-direction: column;
  gap: var(--tf-sp-2);
  padding: var(--tf-sp-3);
  background: rgba(9, 11, 14, .93);
  color: #E7EAED;
}
.tf-lightbox[hidden] { display: none; }
.tf-lightbox__bar {
  display: flex;
  align-items: center;
  gap: var(--tf-sp-3);
  flex: 0 0 auto;
}
.tf-lightbox__count {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  margin-right: auto;
}
.tf-lightbox__open {
  color: inherit;
  font-size: var(--tf-size-small);
  text-decoration: underline;
}
.tf-lightbox__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--tf-radius);
  background: rgba(255, 255, 255, .1);
  color: inherit;
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
}
.tf-lightbox__btn:hover { background: rgba(255, 255, 255, .2); }
.tf-lightbox__btn:focus-visible { outline: 2px solid #E7EAED; outline-offset: 2px; }
.tf-lightbox__btn[hidden] { display: none; }
.tf-lightbox__stage {
  display: flex;
  align-items: center;
  gap: var(--tf-sp-3);
  flex: 1 1 auto;
  min-height: 0;
}
.tf-lightbox__img {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
  max-height: 100%;
  margin: 0 auto;
  object-fit: contain;
}
.tf-lightbox__caption {
  flex: 0 0 auto;
  text-align: center;
  font-size: var(--tf-size-small);
  color: #AEB7C0;
}

@media (max-width: 35.99em) {
  /* На телефоне стрелки по краям съедают ширину картинки — уводим их вниз */
  .tf-lightbox__stage { position: relative; }
  .tf-lightbox__nav { position: absolute; bottom: 0; }
  .tf-lightbox__nav--prev { left: 0; }
  .tf-lightbox__nav--next { right: 0; }
}

/* Длинная ссылка (импорт, домен школы) не должна распирать страницу */
.tf-break { overflow-wrap: anywhere; }
code, .tf-doc a { overflow-wrap: anywhere; }

@media (max-width: 35.99em) {
  /* На 375 px кнопка в ряд из двух уже не читается — растягиваем на строку */
  .tf-actions > .btn,
  .tf-actions > form,
  .tf-actions > form > .btn { width: 100%; }
  /* Карточка формы на телефоне разворачивается во всю ширину: рамка вокруг
     экрана только отнимает место у полей. */
  .tf-form {
    border: 0;
    border-radius: 0;
    padding: 0;
    background: transparent;
  }
  /* Кнопка отправки прилипает к низу: длинную форму иначе долистывают до
     конца дважды — заполнить и найти «Сохранить». */
  .tf-form-actions {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin-top: var(--tf-sp-4);
    padding-bottom: calc(var(--tf-sp-3) + env(safe-area-inset-bottom, 0px));
    background: var(--tf-ground);
  }
  .tf-form-actions > .btn,
  .tf-form-actions > form,
  .tf-form-actions > form > .btn { width: 100%; }
  .tf-page-head { align-items: flex-start; }
  /* поля дат и времени в iOS сжимаются до нечитаемого — держим ширину строки */
  .form-control[type="date"],
  .form-control[type="time"] { width: 100%; }
}

/*
 * Палец — не курсор: 40 px это нижняя граница, при которой в кнопку попадают
 * с первого раза. Правило висит на pointer: coarse, чтобы не раздувать
 * плотные списки на десктопе.
 */
@media (pointer: coarse) {
  .btn {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .btn-sm { min-height: 36px; }
  /*
   * Иконки ActionColumn: сама иконка мелкая, площадь нажатия — нет. Только в
   * карточке: там ячейка действий отдельной строкой, и растягивать её нечему
   * помешать (в таблице то же правило раздуло бы каждую ссылку в клетке).
   */
  .tf-cards > tbody > tr > td:not([data-label]) > a {
    min-width: 40px;
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .tf-nav .nav-link { padding-top: var(--tf-sp-2); padding-bottom: var(--tf-sp-2); }
  .dropdown-item { padding-top: var(--tf-sp-2); padding-bottom: var(--tf-sp-2); }
  .form-check-input { width: 1.15em; height: 1.15em; margin-top: .2em; }
}

/*
 * Раскрытое меню: пункты и блок пользователя идут в столбец, а не жмутся в
 * строку. Блок пользователя отделён линией — это не пункт навигации.
 */
.tf-nav__user {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--tf-sp-2);
}
.tf-nav__user form { margin: 0; }

/*
 * Переключатель клуба (specs/membership-and-access.md, «Переключение
 * контекста»). `<details>`, а не бутстраповский dropdown: шапка — последнее
 * место, где стоит зависеть от загрузившегося JS.
 */
.tf-clubs { position: relative; }
.tf-clubs__current {
  display: inline-flex;
  align-items: center;
  gap: var(--tf-sp-2);
  padding: .25rem var(--tf-sp-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  background: var(--tf-surface);
  color: var(--tf-ink-2);
  font-size: var(--tf-size-small);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  max-width: 14rem;
}
.tf-clubs__current::-webkit-details-marker { display: none; }
.tf-clubs__current:hover { background: var(--tf-surface-2); }
.tf-clubs__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Сколько клубов всего — чтобы «их несколько» читалось до раскрытия. */
.tf-clubs__count {
  flex: none;
  font-size: var(--tf-size-small);
  color: var(--tf-fog);
  border-left: var(--tf-border);
  padding-left: var(--tf-sp-2);
}
.tf-clubs__menu {
  position: absolute;
  z-index: 20;
  right: 0;
  margin-top: var(--tf-sp-1);
  min-width: 15rem;
  padding: var(--tf-sp-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  background: var(--tf-surface);
  box-shadow: var(--tf-shadow);
}
.tf-clubs__empty { margin: 0 0 var(--tf-sp-2); }
.tf-clubs__form { margin: 0; }
.tf-clubs__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--tf-sp-1) var(--tf-sp-2);
  border: 0;
  border-radius: var(--tf-radius);
  background: none;
  color: var(--tf-ink);
  font: inherit;
  cursor: pointer;
}
.tf-clubs__item:hover { background: var(--tf-surface-2); }
.tf-clubs__all {
  display: block;
  margin-top: var(--tf-sp-2);
  padding-top: var(--tf-sp-2);
  border-top: var(--tf-border);
  font-size: var(--tf-size-small);
}

@media (max-width: 47.98em) {
  /* На узком экране выпадать некуда — список встаёт в поток шапки. */
  .tf-clubs__menu { position: static; box-shadow: none; min-width: 0; }
}

@media (max-width: 47.98em) {
  .tf-nav .navbar-collapse { padding-bottom: var(--tf-sp-2); }
  .tf-nav .dropdown-menu { border: 0; padding-left: var(--tf-sp-3); background: none; }
  .tf-nav__user {
    border-top: var(--tf-border);
    padding-top: var(--tf-sp-3);
    margin-top: var(--tf-sp-2);
  }
}

/* ——————————————————————————— Мелочи ——————————————————————————— */

/* фиксированная навигация больше не нужна — контент начинается сразу под ней */
main > .container,
main > .container-fluid { padding-top: var(--tf-sp-5); padding-bottom: var(--tf-sp-6); }

.not-set { color: var(--tf-fog); font-style: italic; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}
/* ————————————————— Календарь плана (specs/plan-calendar.md) ————————————————— */

/*
 * Одна разметка, две раскладки — как у карточных таблиц (`tf-cards`): в вёрстке
 * всегда «неделя → семь дней», а здесь решается, показать неделю строкой из
 * семи колонок (десктоп) или семью строками подряд (телефон). На 375 px
 * месячная сетка нечитаема при любой вёрстке: клетка выходит в 50 px.
 *
 * По умолчанию (мобильная раскладка) день — строка: подпись слева, карточки
 * справа. Шапка с днями недели в ней не нужна — день подписывает себя сам.
 */
.tf-cal { margin-bottom: var(--tf-sp-5); }
.tf-cal__head { display: none; }
.tf-cal__weeklabel {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-small);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
  background: var(--tf-surface);
  border-top: var(--tf-border);
  padding: var(--tf-sp-2) var(--tf-sp-3);
}
.tf-cal__day {
  display: grid;
  /* minmax(0, 1fr): название вроде «1км+10х400/400+4х200/200+1км» переносить
     негде, и без нуля в минимуме такой день растягивал бы всю сетку. */
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--tf-sp-2);
  align-items: start;
  border-top: var(--tf-border);
  padding: var(--tf-sp-2) var(--tf-sp-3);
}
.tf-cal__date {
  display: flex;
  align-items: baseline;
  gap: var(--tf-sp-1);
  color: var(--tf-ink-2);
}
.tf-cal__num { font-family: var(--tf-font-display); font-size: var(--tf-size-h4); font-weight: 700; }
.tf-cal__dayname { font-size: var(--tf-size-small); color: var(--tf-fog); }
.tf-cal__rest { color: var(--tf-fog); }

/* Метка старта в клетке дня (specs/race-goals.md): забег — не тренировка, у
   него нет статуса и отчёта, поэтому это подпись, а не карточка. Акцентная
   кромка слева отличает его от тренировки: цвет тот же, что у «запланирована»
   (--tf-info), нового в палитру не заводим. */
.tf-cal__mark {
  font-size: var(--tf-size-small);
  color: var(--tf-ink);
  background: var(--tf-info-soft);
  border-left: 3px solid var(--tf-info);
  border-radius: var(--tf-radius);
  padding: var(--tf-sp-1) var(--tf-sp-2);
  margin-bottom: var(--tf-sp-1);
  overflow-wrap: anywhere;
}

/* Карточка тренировки: ссылка, поэтому подчёркивание и цвет сбрасываем явно. */
.tf-cal__card {
  display: block;
  text-decoration: none;
  color: var(--tf-ink);
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-left: 3px solid var(--tf-hair);
  border-radius: var(--tf-radius);
  padding: var(--tf-sp-2);
  margin-bottom: var(--tf-sp-1);
  /* Площадь под палец: то же требование, что к кнопкам в кабинетах. */
  min-height: 2.75rem;
  overflow: hidden;
}
.tf-cal__card:hover { border-color: var(--tf-fog); color: var(--tf-ink); }
.tf-cal__card:focus-visible { outline: 0; box-shadow: var(--tf-focus); }
.tf-cal__time {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
  margin-right: var(--tf-sp-2);
}
.tf-cal__sport {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tf-fog);
  margin-right: var(--tf-sp-2);
}
/*
 * Цифры плитки (specs/cabinet-revision.md, «Плитка дня несёт цифры, а не только
 * цвет»): факт своей строкой и крупно, план — приглушённой строкой под ним.
 * До этого на плитке стоял только плановый итог мелкой подписью, а «сделал ли
 * человек задание» говорил один цвет кромки; отсюда и обязательная легенда из
 * семи образцов под календарём.
 *
 * Моноширинный шрифт здесь не украшение: в колонке из семи плиток числа должны
 * вставать друг под другом, иначе «8.2» и «11.4» гуляют по ширине и столбик
 * перестаёт читаться с расстояния.
 */
.tf-cal__fact {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-small);
  color: var(--tf-ink);
  margin-top: var(--tf-sp-1);
  line-height: 1.3;
}
/* Само число — носитель светофора по своей метрике; классы тона общие со
   списком (`.tf-fact--*` ниже), красить одинаково обязаны оба экрана. */
.tf-cal__factnum { font-weight: 600; }
/* Темп — производная величина, светофор по ней не считают: подпись, а не
   второе крупное число. */
.tf-cal__pace {
  font-size: var(--tf-size-eyebrow);
  color: var(--tf-ink-2);
  margin-left: var(--tf-sp-2);
}
/* Слово вместо цифр, когда цифр нет («Без отчёта», «Пропущена», «Выполнена»):
   без него состояние остаётся только цветом — а это и есть то, что итерация
   чинит. Шрифт не моноширинный: слово в столбик чисел не встаёт. */
.tf-cal__state {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
}
/* Плановый итог — ориентир, а не результат, поэтому приглушён и подписан
   словом «план»: одинокое число в клетке читается как присланный отчёт
   (specs/structured-training.md, «Светофор план/факт»). */
.tf-cal__plan {
  font-family: var(--tf-font-mono);
  font-size: var(--tf-size-eyebrow);
  color: var(--tf-fog);
  line-height: 1.3;
}
/*
 * Название — единственное, что задаёт тренировку («1км+10х400/400+4х200/200+1км»),
 * и в нём не бывает пробелов. В строке дня места хватает, поэтому там ломаем
 * по любому символу; в клетке месяца ломать нельзя (клетка в 1/7 экрана даст
 * четыре строки), поэтому там — многоточие, а полный текст уезжает в подсказку
 * карточки (`title`).
 */
.tf-cal__title { font-size: var(--tf-size-small); overflow-wrap: anywhere; }

/* Сегодня и прошедшее — те же приёмы, что в списке: подсветка одна на экран,
   прошедшее приглушается текстом, а не подложкой. */
.tf-cal__day--today { background: var(--tf-track-soft); }
.tf-cal__day--past .tf-cal__title,
.tf-cal__day--past .tf-cal__date { color: var(--tf-ink-2); }
.tf-cal__day--other { opacity: .55; }
.tf-cal__day--weekend .tf-cal__dayname { color: var(--tf-track); }

/*
 * Итог недели (specs/cabinet-revision.md, «Колонка итогов недели»): на широком
 * экране — восьмая колонка сетки, на телефоне — полоска под семью днями недели.
 * Разметка одна, как и у всего остального в календаре; решает раскладку CSS.
 *
 * Цвета у итога нет: светофор считается по состоявшейся тренировке, а неделя
 * бывает будущей — покрашенный по тому же правилу итог кричал бы «недобрал» там,
 * где тренер только планирует (см. `PlanCalendar::renderTotals()`).
 */
.tf-cal__total {
  border-top: var(--tf-border);
  background: var(--tf-surface);
  padding: var(--tf-sp-2) var(--tf-sp-3);
}
.tf-cal__totalhead {
  font-family: var(--tf-font-display);
  font-size: var(--tf-size-eyebrow);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
  margin-bottom: var(--tf-sp-1);
}
/* Подпись слева, число справа: на телефоне полоска идёт в ширину экрана, и
   числа должны встать в столбик у правого края, а не плавать за подписями. */
.tf-cal__totalrow {
  display: flex;
  justify-content: space-between;
  gap: var(--tf-sp-2);
  font-size: var(--tf-size-small);
  line-height: 1.4;
}
.tf-cal__totalkey { color: var(--tf-fog); }
/* Тем же моноширинным, что цифры плиток: итог складывает именно их, и два
   разных начертания одного километража читаются как две разные величины. */
.tf-cal__totalval { font-family: var(--tf-font-mono); color: var(--tf-ink); }
.tf-cal__totalsports {
  font-size: var(--tf-size-eyebrow);
  color: var(--tf-fog);
  margin-top: var(--tf-sp-1);
}

@media (min-width: 62em) {
  /* Месяц целиком: семь колонок, неделя — строка сетки. */
  .tf-cal__head,
  .tf-cal__week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 1px;
  }
  /* Восьмая колонка появляется вместе с итогом — и в шапке тоже, иначе дни
     съезжают под чужие подписи. Ширина фиксированная: итог не должен отбирать
     место у дней, ему хватает двух чисел в строке. */
  .tf-cal--totals .tf-cal__head,
  .tf-cal--totals .tf-cal__week {
    grid-template-columns: repeat(7, minmax(0, 1fr)) minmax(0, 9rem);
  }
  .tf-cal__dow {
    font-family: var(--tf-font-display);
    font-size: var(--tf-size-eyebrow);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--tf-ink-2);
    padding: var(--tf-sp-1) var(--tf-sp-2);
  }
  /* Подпись недели в сетке не нужна: недели читаются строками. Скрываем её
     здесь, а не в PHP, — разметка обязана остаться одна. */
  .tf-cal__weeklabel { display: none; }
  .tf-cal__day {
    display: block;
    border: var(--tf-border);
    min-height: 6.5rem;
    padding: var(--tf-sp-2);
  }
  .tf-cal__date { display: block; }
  .tf-cal__dayname { display: none; }
  /* В сетке итог — такая же клетка, как день: та же рамка и та же высота,
     иначе восьмая колонка выглядит приклеенной сбоку. */
  .tf-cal__total {
    border: var(--tf-border);
    min-height: 6.5rem;
    padding: var(--tf-sp-2);
  }
  .tf-cal__totalrow { display: block; }
  .tf-cal__totalkey {
    display: block;
    font-size: var(--tf-size-eyebrow);
  }
  .tf-cal__card { border-left-width: 3px; }
  .tf-cal__title {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/*
 * Светофор (`common\services\TrainingOutcome`) — цветная кромка слева у карточки
 * календаря, а не заливка всей клетки: в дне бывает две тренировки, и две
 * заливки рядом спорят с подсветкой «сегодня». Тон дублируется подписью в
 * `title`, поэтому цвет здесь — ускорение чтения, а не единственный носитель
 * смысла.
 *
 * Тоны навешиваются только на карточку (`.tf-cal__card`): у таблиц
 * `--bs-table-bg` прозрачный, и та же заливка на `tr` покрасила бы строку
 * целиком, отобрав заметность у «сегодня».
 */
.tf-cal__card.tf-tone--planned { border-left-color: var(--tf-info); }
.tf-cal__card.tf-tone--skipped { border-left-color: var(--tf-warn); background: var(--tf-warn-soft); }
.tf-cal__card.tf-tone--no_report { border-left-color: var(--tf-danger); }
.tf-cal__card.tf-tone--no_report .tf-cal__title { color: var(--tf-danger); }
.tf-cal__card.tf-tone--undated { border-left-color: var(--tf-fog); }
/*
 * Светофор план/факт (specs/structured-training.md, «Светофор план/факт»):
 * три состояния поверх «выполнена», семь состояний светофора живут на тех же
 * четырёх токенах — новых цветов не заводим, различает вторая ось. Насыщенная
 * кромка без фона значит «отчёта ещё нет» (`planned`, `no_report`); та же
 * кромка плюс мягкий фон значит «отчёт есть» (`done_over`, `done_under`,
 * `done_on_target`). Внутри тона фон — предупреждение (в допуске — успех
 * `--tf-ok`, недобрал — тревога `--tf-danger`, перебрал — нейтральная
 * информация `--tf-info`, пропущена — предупреждение `--tf-warn`).
 * `done` («выполнена», сравнивать нечем) — особый случай: сильный сигнал
 * кромки принадлежит проверенному состоянию, поэтому у «просто выполнена»
 * кромка приглушена до собственного мягкого тона (сливается с фоном), а не
 * убрана совсем — иначе плоская заливка без края читалась бы как ошибка или
 * отключённая строка.
 */
.tf-cal__card.tf-tone--done_on_target { border-left-color: var(--tf-ok); background: var(--tf-ok-soft); }
.tf-cal__card.tf-tone--done { border-left-color: var(--tf-ok-soft); background: var(--tf-ok-soft); }
.tf-cal__card.tf-tone--done_under { border-left-color: var(--tf-danger); background: var(--tf-danger-soft); }
.tf-cal__card.tf-tone--done_over { border-left-color: var(--tf-info); background: var(--tf-info-soft); }

/*
 * В списке из всех тонов помечаем один — «прошло, а отчёта нет»: остальные там
 * уже сказаны словом (пилюля статуса), а этот и есть единственное состояние,
 * требующее действия. Кромка бордюром, а не box-shadow: тень у ячейки занята
 * бутстраповской подложкой строки (`--bs-table-accent-bg`), и перебивать её
 * значит потерять подсветку «сегодня».
 */
.table tr.tf-tone--no_report > *:first-child { border-left: 3px solid var(--tf-danger); }

/*
 * Светофор план/факт в списке — на самом числе, а не на строке
 * (specs/structured-training.md, «Светофор план/факт»). Строке этот тон отдать
 * нельзя: её подложка занята подсветкой «сегодня», а кромкой «недобрал»
 * (danger, без фона) не отличается от «без отчёта». На числе тон стоит там,
 * где и цифры, и различает метрики: недобрать километры и перебрать время —
 * разные вещи. Токены те же четыре, что у карточки календаря: в допуске —
 * успех, недобрал — тревога, перебрал — нейтральная информация. Подпись
 * состояния приходит подсказкой title из common\widgets\FactPlan.
 */
/* Вторая ось, как у карточки: полужирный говорит «цифры сверены с заданием»,
   цвет — в какую сторону. Без веса «перебрал» пропадал бы вовсе: --tf-info
   (#3C5170) почти совпадает со второстепенным текстом --tf-ink-2 (#3F4750),
   и число выглядело бы просто ненакрашенным. */
.tf-fact--on-target,
.tf-fact--under,
.tf-fact--over { font-weight: 600; }
.tf-fact--on-target { color: var(--tf-ok); }
.tf-fact--under { color: var(--tf-danger); }
.tf-fact--over { color: var(--tf-info); }
/* Прошедшее приглушает текст целой строкой (`.tf-row--past > *`), поэтому тон
   повторён внутри неё: иначе прошедшая неделя теряла бы светофор целиком. */
.tf-row--past .tf-fact--on-target { color: var(--tf-ok); }
.tf-row--past .tf-fact--under { color: var(--tf-danger); }
.tf-row--past .tf-fact--over { color: var(--tf-info); }

/*
 * Легенда светофора: цвет сам по себе ничего не объясняет, поэтому под
 * календарём стоит расшифровка. Образцы красятся теми же токенами, что кромка
 * карточки, — но своим правилом: тоны сознательно привязаны к `.tf-cal__card`.
 *
 * С тех пор как плитка печатает факт и план числами (specs/cabinet-revision.md,
 * итерация 1), легенда свёрнута в `<details>`: календарь читается без неё, но
 * цвет остался, и новому тренеру расшифровка всё ещё нужна. Стрелку `summary`
 * не прячем — она и есть подсказка, что блок раскрывается.
 */
.tf-legend-box { margin-bottom: var(--tf-sp-4); }
.tf-legend-box > summary {
  cursor: pointer;
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
  /* Площадь под палец: та же величина, что у кнопок в кабинетах. */
  padding: var(--tf-sp-1) 0;
}
.tf-legend-box > summary:focus-visible { outline: 0; box-shadow: var(--tf-focus); }
.tf-legend-box[open] > summary { margin-bottom: var(--tf-sp-2); }
.tf-legend-box .tf-legend { margin-bottom: 0; }

.tf-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tf-sp-3);
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
  margin-bottom: var(--tf-sp-4);
}
.tf-legend__item { display: inline-flex; align-items: center; gap: var(--tf-sp-1); }
.tf-legend__swatch {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: var(--tf-border);
  border-left-width: 3px;
  border-radius: var(--tf-radius);
}
.tf-legend__swatch.tf-tone--planned { border-left-color: var(--tf-info); }
.tf-legend__swatch.tf-tone--skipped { border-left-color: var(--tf-warn); background: var(--tf-warn-soft); }
.tf-legend__swatch.tf-tone--no_report { border-left-color: var(--tf-danger); }
.tf-legend__swatch.tf-tone--done_on_target { border-left-color: var(--tf-ok); background: var(--tf-ok-soft); }
.tf-legend__swatch.tf-tone--done { border-left-color: var(--tf-ok-soft); background: var(--tf-ok-soft); }
.tf-legend__swatch.tf-tone--done_under { border-left-color: var(--tf-danger); background: var(--tf-danger-soft); }
.tf-legend__swatch.tf-tone--done_over { border-left-color: var(--tf-info); background: var(--tf-info-soft); }

/* ——— Перетаскивание (js/dnd.js) ——— */

/* Пока тащим — ни выделения текста, ни прокрутки под пальцем. */
.tf-dragging, .tf-dragging body { user-select: none; touch-action: none; }
/* Источник переноса браузер не тащит сам: его нативный drag ссылки отбирал
   указатель у нашего (см. `dragstart` в js/dnd.js). Атрибут `draggable="false"`
   говорит то же, но Safari слушается именно это свойство. */
[data-tf-move], [data-tf-move] a, [data-tf-move] img,
[data-tf-template], [data-tf-template] img { -webkit-user-drag: none; }
.tf-dragging .tf-cal__card { cursor: grabbing; }
/* Тень под курсором: подписи хватает, миниатюру карточки не рисуем. */
.tf-drag-ghost {
  position: fixed;
  z-index: 1080;
  pointer-events: none;
  transform: translate(-50%, -140%);
  max-width: 14rem;
  padding: var(--tf-sp-1) var(--tf-sp-2);
  background: var(--tf-ink);
  color: var(--tf-surface-2);
  border-radius: var(--tf-radius);
  font-size: var(--tf-size-small);
  box-shadow: var(--tf-shadow);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tf-drag-src { opacity: .4; }
/* Цель — рамкой акцента, а не заливкой: заливка спорит с тоном карточек внутри. */
.tf-drag-over { outline: 2px solid var(--tf-track); outline-offset: -2px; }
.tf-drag-over.tf-cal__day--other { opacity: 1; }
/* Строка списка как цель: outline на `tr` браузеры рисуют неохотно. */
tr.tf-drag-over > * { background-image: linear-gradient(var(--tf-track-soft), var(--tf-track-soft)); }

/* Пустой день в списке — цель переноса и форма недели; строка тихая, чтобы
   недели с двумя тренировками не выглядели полупустыми. */
.tf-day-empty > * { color: var(--tf-fog); }

/* Старт в списке недели (specs/race-goals.md): забег — не тренировка, поэтому
   строка без статуса и кнопок, но заметная — ради него неделя и существует.
   Цвет тот же, что у метки в календаре: «запланировано». */
.tf-row-race > * { color: var(--tf-info); font-weight: 600; }
.tf-row-race { --bs-table-accent-bg: var(--tf-info-soft); }

/* Действия над неделей целиком в колонке итога (specs/cabinet-revision.md,
   «Скорость планирования»). Свёрнуты: в календаре шесть недель, и шесть
   открытых форм перевесили бы итоги, ради которых колонка заведена. */
.tf-weekops {
  margin-top: var(--tf-sp-2);
  border-top: var(--tf-border);
  padding-top: var(--tf-sp-1);
  font-size: var(--tf-size-eyebrow);
}
.tf-weekops > summary {
  cursor: pointer;
  color: var(--tf-fog);
  font-family: var(--tf-font-display);
  letter-spacing: .06em;
  text-transform: uppercase;
}
/* В колонку шириной 9rem кнопки рядом не встают — столбиком. */
.tf-weekops__form {
  display: grid;
  gap: var(--tf-sp-1);
  margin-top: var(--tf-sp-1);
}
.tf-weekops__form .btn { --bs-btn-padding-y: 1px; }

/* Повтор тренировки — блок под формой правки. */
.tf-repeat {
  margin-top: var(--tf-sp-5);
  border-top: var(--tf-border);
  padding-top: var(--tf-sp-3);
}
.tf-repeat__form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tf-sp-2);
}
.tf-repeat__until {
  display: flex;
  align-items: center;
  gap: var(--tf-sp-1);
  color: var(--tf-ink-2);
}
.tf-repeat__form .form-select,
.tf-repeat__form .form-control { width: auto; }

/* Ящик библиотеки рядом с календарём (specs/cabinet-revision.md, итерация 7б):
   заготовки, которые тащат прямо в день. Свёрнут — нужен не при каждом
   открытии календаря. */
.tf-drawer {
  border: var(--tf-border);
  background: var(--tf-surface);
  border-radius: var(--tf-radius);
  padding: var(--tf-sp-2) var(--tf-sp-3);
  margin-bottom: var(--tf-sp-3);
}
.tf-drawer > summary {
  cursor: pointer;
  font-family: var(--tf-font-display);
  color: var(--tf-ink-2);
}
.tf-drawer__body { margin-top: var(--tf-sp-2); }
.tf-drawer__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tf-sp-1);
}
/* Заготовка — «кубик», который берут рукой: у неё вид предмета, а не ссылки,
   иначе перетаскивание не приходит в голову. Ссылкой она при этом остаётся. */
.tf-drawer__item {
  display: inline-block;
  max-width: 18rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  border: var(--tf-border);
  background: var(--tf-surface-2);
  border-radius: var(--tf-radius);
  padding: var(--tf-sp-1) var(--tf-sp-2);
  font-size: var(--tf-size-small);
  color: var(--tf-ink);
  text-decoration: none;
  cursor: grab;
}
.tf-drawer__item:hover { border-color: var(--tf-track); color: var(--tf-track-deep); }

/* Меню по правому клику на дне (js/daymenu.js). Лежит в body и позиционируется
   по курсору, поэтому fixed: внутри сетки календаря оно обрезалось бы
   переполнением клетки. */
.tf-daymenu {
  position: fixed;
  z-index: 1080;
  min-width: 12rem;
  padding: var(--tf-sp-1) 0;
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow);
}
.tf-daymenu__item {
  display: block;
  width: 100%;
  border: 0;
  background: none;
  text-align: left;
  padding: var(--tf-sp-1) var(--tf-sp-3);
  font-size: var(--tf-size-small);
  color: var(--tf-ink);
  text-decoration: none;
  cursor: pointer;
}
.tf-daymenu__item:hover { background: var(--tf-surface); color: var(--tf-track-deep); }

/* Призрак копии отличается от призрака переноса: человек должен видеть, что
   получится, до того как отпустит. */
.tf-drag-ghost--copy::after {
  content: ' + копия';
  color: var(--tf-ok);
}

/* --- Карточка тренировки: план и факт строка в строку --------------------
   specs/cabinet-revision.md, «Карточка тренировки». Сетка, а не таблица:
   клетки держат поля ввода, а таблица с инпутами на телефоне разъезжается по
   ширине. Подписи колонок стоят один раз сверху — «Планируется» и «Факт»
   повторять у каждой строки незачем. */
.tf-planfact {
  display: grid;
  gap: var(--tf-sp-1) var(--tf-sp-2);
  align-items: center;
}
.tf-planfact__row {
  display: grid;
  grid-template-columns: minmax(7rem, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--tf-sp-2);
  align-items: center;
}
.tf-planfact__row--head {
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
}
.tf-planfact__label {
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
}
.tf-planfact__cell { min-width: 0; }
/* Поле внутри клетки занимает её целиком: два числа одной строки обязаны
   стоять на одной вертикали, иначе сравнивать их глазом не выходит. */
.tf-planfact__cell .form-control { width: 100%; }
.tf-planfact__cell .help-block { font-size: var(--tf-size-small); }
/* Факт — только чтение у тренера, и выглядеть он должен как значение, а не как
   поле, которое почему-то не нажимается. */
.tf-planfact__cell--fact {
  font-variant-numeric: tabular-nums;
  color: var(--tf-ink);
}
.tf-planfact__report {
  margin-top: var(--tf-sp-2);
  font-size: var(--tf-size-small);
}
@media (max-width: 30rem) {
  /* На телефоне подпись уезжает над парой чисел: три колонки в 320 px дают
     столбец шириной в два символа. */
  .tf-planfact__row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .tf-planfact__label { grid-column: 1 / -1; }
  .tf-planfact__row--head .tf-planfact__label { display: none; }
}

/* --- Неделя группы: строка на ученика ------------------------------------
   specs/cabinet-revision.md, «Группы и календарь группы». Та же сетка, что у
   календаря плана, но развёрнутая: там семь дней одного человека, здесь семь
   дней у каждого. Заголовок колонки поэтому один на всю таблицу, а имя стоит
   в первой клетке строки. */
.tf-weekgrid {
  display: grid;
  gap: var(--tf-sp-1);
  overflow-x: auto;
}
.tf-weekgrid__row {
  display: grid;
  grid-template-columns: minmax(9rem, 1.4fr) repeat(7, minmax(5.5rem, 1fr)) minmax(7rem, 1fr);
  gap: var(--tf-sp-1);
  align-items: stretch;
  min-width: 52rem;
}
.tf-weekgrid__row--head {
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
}
.tf-weekgrid__name {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--tf-sp-1) var(--tf-sp-2);
  font-size: var(--tf-size-small);
}
.tf-weekgrid__cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--tf-sp-1);
  background: var(--tf-surface-2);
  border: var(--tf-border);
  border-radius: var(--tf-radius);
  min-height: 2.5rem;
}
.tf-weekgrid__row--head .tf-weekgrid__cell {
  background: none;
  border: 0;
  min-height: 0;
  padding-bottom: 0;
}
/* Сегодня обведено, а не залито: заливка спорила бы с тоном тренировки внутри. */
.tf-weekgrid__cell--today { border-color: var(--tf-track); }
.tf-weekgrid__item {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--tf-size-small);
  border-radius: var(--tf-radius);
  padding: 1px var(--tf-sp-1);
  text-decoration: none;
  color: inherit;
}
.tf-weekgrid__total {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--tf-sp-1) var(--tf-sp-2);
  font-size: var(--tf-size-small);
  font-variant-numeric: tabular-nums;
}

/* Раздача над сеткой: поля в строку, длинные — во всю ширину. */
.tf-assign {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--tf-sp-2);
  align-items: end;
  margin-bottom: var(--tf-sp-2);
}
.tf-assign__field {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--tf-size-small);
  color: var(--tf-ink-2);
}
.tf-assign__field--wide { grid-column: 1 / -1; }

/* Состав — галочками в несколько колонок: список учеников бывает длинным, а
   выбор из него делается глазами, а не прокруткой. */
.tf-checks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--tf-sp-1) var(--tf-sp-3);
  margin-bottom: var(--tf-sp-2);
}
.tf-checks__item {
  display: flex;
  align-items: center;
  gap: var(--tf-sp-1);
  font-size: var(--tf-size-small);
}
.tf-actions--wrap { flex-wrap: wrap; }

/* Страница клуба (specs/communities.md). Обложка — единственная крупная картинка
   в продукте, и высоту ей задаём здесь, а не пропорциями файла: клуб загрузит
   что угодно, от панорамы до скриншота, а шапка обязана остаться шапкой. */
.tf-club__cover {
  margin: 0 calc(-1 * var(--tf-sp-3)) var(--tf-sp-4);
  overflow: hidden;
  border-radius: var(--tf-radius);
  background: var(--tf-track-soft);
}
.tf-club__cover img {
  display: block;
  width: 100%;
  height: clamp(9rem, 28vw, 18rem);
  object-fit: cover;
}
.tf-club__logo {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: var(--tf-radius);
  background: var(--tf-track-soft);
}
