/* ==========================================================================
   TOURZONE — дизайн-система «Табло»
   Единственный стиль на весь продукт. Меняя переменные в :root,
   можно перекрасить и перенабрать всё приложение целиком.
   ========================================================================== */

/* --- 1. Токены ---------------------------------------------------------- */

:root {
  /* Бумага и чернила */
  --paper: #ffffff;
  --surface: #f7f7f2;
  --surface-2: #efefe6;
  /* Поле контента справа от рельсы и под топбаром. Рельса, топбар и панели —
     бумага, и втроём они стыковались одним волоском: каркас читался одним
     белым пятном, а панель на нём не выглядела панелью. Поле на волосок темнее
     бумаги — ровно настолько, чтобы карточка списка на нём читалась карточкой.
     С подложкой (.tz-note, .tz-vd--out) поле сошлось почти в тон, и держатся
     эти блоки теперь на своей рамке, а не на заливке. */
  --canvas: #f6f6f1;
  --ink: #16170f;
  --ink-2: #4c4e40;
  --ink-3: #6c6e5f;
  --rule: #e4e4da;
  --rule-2: #cccebf;
  /* Граница интерактивного элемента: поле, кнопка, чип. Отдельный токен, а не
     --rule-2, потому что по WCAG 1.4.11 контур управления обязан давать 3:1 к
     фону, а разделителю-волоску это ни к чему. */
  --rule-ctl: #8a8c7c;

  /* Акцент — теннисный мяч */
  --ball: #d7f205;
  --ball-soft: #f0fca6;
  --ball-wash: #f8fddc;
  --ball-deep: #7d8f00;
  --ball-ink: #333d00;

  /* Покрытия */
  --clay: #c2542c;
  --hard: #2b6aae;
  --grass: #2f7d4f;
  --carpet: #6d59a6;
  --surface-color: var(--rule-2); /* подменяется инлайном на карточке */

  /* Статусы */
  --ok: #2c7a4d;
  --ok-wash: #e8f4ec;
  --warn: #8a5d0d;
  --warn-wash: #fbf1dd;
  --err: #b93526;
  --err-wash: #fbeae7;

  /* Шрифты */
  --font-ui: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, Arial, sans-serif;
  --font-num: ui-monospace, "SF Mono", "JetBrains Mono", "Roboto Mono", Menlo,
    Consolas, monospace;

  /* Типографическая шкала */
  --t-3xs: 10px;
  --t-2xs: 11px;
  --t-xs: 12px;
  --t-s: 13px;
  --t-m: 15px;
  --t-l: 18px;
  --t-xl: 23px;
  --t-2xl: 30px;
  --t-3xl: 40px;

  /* Отступы (шаг 4) */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;
  --s8: 64px;

  /* Радиусы: данные — острые, управление — скруглённое */
  --r-data: 0px;
  --r-ctl: 6px;
  --r-box: 10px;
  --r-pill: 999px;

  /* Тени — ровно две на весь продукт */
  --sh-pop: 0 8px 28px rgba(22, 23, 15, 0.13);
  --sh-lift: 0 1px 0 var(--rule-2);

  /* Длительности */
  --d-fast: 110ms;
  --d-slow: 240ms;

  /* Геометрия каркаса */
  --rail: 244px;
  --measure: 1160px;
}

/* --- 2. Сброс и база ---------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--t-m);
  line-height: 1.5;
  font-feature-settings: "cv05", "ss01";
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.16;
}

h1 {
  font-size: var(--t-2xl);
}
h2 {
  font-size: var(--t-xl);
}
h3 {
  font-size: var(--t-l);
}
h4 {
  font-size: var(--t-m);
}

p {
  margin: 0 0 var(--s3);
  max-width: 68ch;
}

a {
  color: inherit;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 3px;
}

/* Отклик на наведение даёт цвет мяча, а не подчёркивание: подчёркивание и так
   стоит у ссылок в тексте, а лишняя черта под именем игрока в карточке матча
   только рябила.

   :where(:not(...)) не добавляет веса — правило остаётся силы a:hover. Поэтому
   всё, что красит себя само и лежит ниже по файлу (пункт рельсы, страница
   пагинатора, чип, «показать ещё»), выигрывает само собой, без исключений.
   В скобках только те, чей цвет объявлен одним классом: их вес меньше, порядок
   бы им не помог, а зелёный текст на их фоне читался бы хуже, а не лучше. */
a:hover:where(:not(.tz-btn, .tz-tag, .tz-brand, .tz-me__row, .tz-chat__item, .skip-link)) {
  color: var(--ball-deep);
  text-decoration-color: currentColor;
}

ul,
ol {
  margin: 0;
  padding: 0;
}

li {
  list-style: none;
}

img,
svg {
  display: block;
  max-width: 100%;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 2px;
}

::selection {
  background: var(--ball);
  color: var(--ink);
}

/* Числа везде набираются моноширинным с табличными цифрами */
.num,
.tz-table td .num,
time.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.muted {
  color: var(--ink-3);
}
.sub {
  color: var(--ink-2);
}
.small {
  font-size: var(--t-s);
}
.tiny {
  font-size: var(--t-xs);
}
.nowrap {
  white-space: nowrap;
}

/* Обрезка длинных имён — обязательна во всех списках */
.clip {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.clip-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: var(--s3);
  top: -60px;
  z-index: 90;
  background: var(--ink);
  color: var(--paper);
  padding: var(--s2) var(--s4);
  border-radius: var(--r-ctl);
  transition: top var(--d-fast);
}

.skip-link:focus {
  top: var(--s3);
}

/* --- 3. Каркас: рельса + полотно ---------------------------------------- */

.tz-app {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100vh;
}

.tz-rail {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  padding: var(--s5) var(--s4);
  border-right: 1px solid var(--rule);
  background: var(--paper);
}

.tz-brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  text-decoration: none;
}

.tz-brand__mark {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 7px;
  background: var(--ball);
  position: relative;
  overflow: hidden;
}

/* «Швы» теннисного мяча — те же две дуги, что в favicon.svg: эллипсы
   вынесены за края квадрата, наружу торчащее срезает overflow. Геометрия
   пересчитана из иконки 32×32 в масштабе 26/32. */
.tz-brand__mark::before,
.tz-brand__mark::after {
  content: "";
  position: absolute;
  box-sizing: border-box;
  width: 22.75px;
  height: 32.5px;
  border: 1.8px solid var(--ink);
  border-radius: 50%;
}

.tz-brand__mark::before {
  left: 17.9px;
  top: -9.8px;
}

.tz-brand__mark::after {
  left: -14.6px;
  top: 3.3px;
}

.tz-brand__word {
  font-size: var(--t-m);
  font-weight: 650;
  letter-spacing: 0.06em;
}

.tz-nav {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  flex: 1;
  overflow-y: auto;
  margin: 0 calc(var(--s2) * -1);
  padding: 0 var(--s2);
}

.tz-nav__title {
  font-size: var(--t-2xs);
  font-weight: 600;
  color: var(--ink-3);
  padding: 0 var(--s2) var(--s2);
}

/* Пункты раздела стоят вплотную: подсветка текущего смыкалась бы с наведённым
   соседом в одну плашку. Два пикселя — ровно чтобы скруглённые углы читались,
   и не настолько, чтобы раздел развалился на отдельные строки. Правило общее
   для рельсы сайта и админки — разметка навигации у них одна. */
.tz-nav ul {
  display: grid;
  gap: 2px;
}

.tz-nav__link {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s2);
  border-radius: var(--r-ctl);
  font-size: var(--t-s);
  text-decoration: none;
  color: var(--ink-2);
  transition: background var(--d-fast), color var(--d-fast);
}

.tz-nav__link svg {
  width: 17px;
  height: 17px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
}

.tz-nav__link:hover {
  background: var(--surface);
  color: var(--ink);
}

.tz-nav__link[aria-current="page"] {
  background: var(--ink);
  color: var(--paper);
  font-weight: 550;
}

.tz-nav__link--locked {
  color: var(--ink-3);
  cursor: not-allowed;
}

.tz-nav__count {
  margin-left: auto;
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  background: var(--ball);
  color: var(--ink);
  border-radius: var(--r-pill);
  padding: 1px 7px;
}

.tz-nav__link[aria-current="page"] .tz-nav__count {
  background: var(--ball);
  color: var(--ink);
}

/* Карточка пользователя внизу рельсы */
.tz-me {
  border-top: 1px solid var(--rule);
  padding-top: var(--s4);
  display: grid;
  gap: var(--s3);
}

.tz-me__row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  text-decoration: none;
  min-width: 0;
}

.tz-avatar {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  display: grid;
  place-items: center;
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--ink-2);
}

.tz-avatar--live {
  background: var(--ball);
  color: var(--ink);
}

/* Аватар пользователя загружен им самим и может быть любой пропорции: без
   cover широкая картинка растянулась бы в овал. */
img.tz-avatar {
  object-fit: cover;
}

/* Крупное превью в форме правки профиля: там картинку оценивают, а не узнают
   себя в углу экрана. */
.tz-avatar--lg {
  width: 64px;
  height: 64px;
  font-size: var(--t-s);
}

/* --- 4. Топбар и шапка страницы ----------------------------------------- */

.tz-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--canvas);
}

.tz-top {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(8px);
}

/* Бургер нужен только там, где рельса выезжает поверх контента (≤860px). На
   десктопе она стоит на месте, и кнопка переключала бы data-open вхолостую.
   Селектор с .tz-btn — не украшение: базовое правило кнопки идёт ниже по
   файлу с той же силой и одиночный .tz-burger перебивало бы своим display. */
.tz-btn.tz-burger {
  display: none;
}

.tz-scope {
  display: flex;
  gap: var(--s2);
  align-items: center;
  flex-wrap: wrap;
}

.tz-seg {
  display: inline-flex;
  background: var(--surface);
  border-radius: var(--r-ctl);
  padding: 2px;
}

.tz-seg a,
.tz-seg button {
  border: 0;
  background: transparent;
  padding: 4px 11px;
  border-radius: 4px;
  font-size: var(--t-xs);
  font-weight: 550;
  color: var(--ink-2);
  text-decoration: none;
  cursor: pointer;
}

/* Выбранный сегмент — тот же чёрный акцент, что у активного пункта рельсы
   (.tz-nav__link[aria-current="page"]): «где я сейчас» по всему интерфейсу
   читается одним и тем же способом. */
.tz-seg [aria-current="true"],
.tz-seg [aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
  font-weight: 550;
}

.tz-top__right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* Шапка на десктопе — один ряд: логотип уже есть в рельсе, ящик не нужен, а
   подписи групп только повторяли бы вслух то, что видно по самим сегментам.
   Всё это включается ниже, на узком брейкпоинте. */
.tz-top__brand {
  display: none;
}

.tz-btn.tz-top__filters {
  display: none;
}

.tz-scope__t {
  display: none;
}

.tz-scrim {
  display: none;
}

/* display:contents — чтобы обёртки, нужные только мобильному ящику, на
   десктопе не создавали лишний уровень: сегменты остаются прямыми детьми
   .tz-top и .tz-scope и раскладываются ровно как раньше. */
.tz-top__ctl,
.tz-scope__g {
  display: contents;
}

/* Ритм полотна задаёт сетка с gap, а не собственные margin у блоков. Так
   расстояние между блоками верхнего уровня описано в одном месте и не зависит
   от того, что за блок стоит рядом: раньше «полоска по вашей карточке» и форма
   фильтров слипались, потому что .ball-box { margin: 0 } объявлен ниже
   утилиты .mb-5 и молча её перебивал.
   minmax(0, 1fr) — чтобы широкое содержимое (полоса календаря, таблица)
   прокручивалось внутри себя, а не растягивало страницу. */
.tz-page {
  padding: var(--s6) var(--s5) var(--s8);
  width: 100%;
  max-width: calc(var(--measure) + var(--s5) * 2);
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s5);
  align-content: start;
}

/* Свои вертикальные margin блоков верхнего уровня к gap прибавлялись бы. Гасим
   их здесь, а не удаляем у самих компонентов: те же .tz-head и .tz-tabs стоят
   и внутри вкладок, и в админке, где сетки с gap нет. Селектор с main — чтобы
   правило не зависело от того, объявлена утилита .mb-5 выше или ниже по файлу. */
main.tz-page > * {
  margin-top: 0;
  margin-bottom: 0;
}

.tz-page--wide {
  max-width: none;
}

/* Надстрочник с заголовком — один блок полотна: между собой у них свой шаг,
   вдвое меньше межблочного, иначе надстрочник читался бы как отдельная фраза,
   а не как подпись над h1. */
.tz-pagehead {
  display: grid;
  gap: var(--s3);
}

/* .tz-head несёт свой margin ради админки — там полотно не сетка. Здесь шаг
   даёт gap, и margin прибавился бы к нему. */
.tz-pagehead > .tz-head {
  margin-bottom: 0;
}

/* Надстрочник — не капслок и не «A · B · C», а точка состояния + фраза */
.tz-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-xs);
  color: var(--ink-2);
}

.tz-eyebrow__dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--ball);
  flex: none;
}

.tz-eyebrow--guest .tz-eyebrow__dot {
  background: var(--rule-2);
}

.tz-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-bottom: var(--s5);
}

.tz-head__lead {
  color: var(--ink-2);
  font-size: var(--t-m);
  margin: var(--s2) 0 0;
}

/* --- 5. Кнопки и поля ---------------------------------------------------- */

.tz-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  border: 1px solid var(--rule-ctl);
  background: var(--paper);
  color: var(--ink);
  border-radius: var(--r-ctl);
  padding: 7px 14px;
  font-size: var(--t-s);
  font-weight: 550;
  line-height: 1.3;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--d-fast), border-color var(--d-fast);
}

.tz-btn:hover {
  background: var(--surface);
}

.tz-btn svg {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.7;
}

.tz-btn--primary {
  background: var(--ball);
  border-color: var(--ball);
  color: var(--ink);
}

.tz-btn--primary:hover {
  background: var(--ball-deep);
  border-color: var(--ball-deep);
}

.tz-btn--dark {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.tz-btn--dark:hover {
  background: #000;
}

.tz-btn--ghost {
  border-color: transparent;
  background: transparent;
  color: var(--ink-2);
}

.tz-btn--ghost:hover {
  background: var(--surface);
  color: var(--ink);
}

.tz-btn--danger {
  border-color: var(--err);
  color: var(--err);
}

.tz-btn--danger:hover {
  background: var(--err-wash);
}

.tz-btn--sm {
  padding: 4px 9px;
  font-size: var(--t-xs);
}

.tz-btn--icon {
  padding: 6px;
  width: 30px;
  height: 30px;
}

.tz-btn[disabled],
.tz-btn[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

.tz-field {
  display: grid;
  gap: 5px;
  min-width: 0;
}

.tz-field > label {
  font-size: var(--t-xs);
  font-weight: 550;
  color: var(--ink-2);
}

.tz-input,
.tz-select {
  width: 100%;
  border: 1px solid var(--rule-ctl);
  background: var(--paper);
  border-radius: var(--r-ctl);
  padding: 7px 10px;
  font-size: var(--t-s);
}

/* Обычный <select> — как закрытый csel: та же рамка, отступы и «галка»
   цвета --ink-3 вместо системной стрелки. Список раскрывает браузер, его
   вид не наш, но в спокойном состоянии оба поля неразличимы. */
.tz-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 28px;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' fill='none' stroke='%236f7162' stroke-width='1.5'%3E%3Cpath d='M1.6 3.4 5 6.8l3.4-3.4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 9px;
}

.tz-select:disabled {
  color: var(--ink-3);
  background-color: var(--surface);
  cursor: default;
}

.tz-input::placeholder {
  color: var(--ink-3);
}

.tz-input:focus,
.tz-select:focus {
  outline: 2px solid var(--ink);
  outline-offset: -1px;
}

.tz-input--search {
  padding-left: 32px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f7162' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 10px center;
  background-size: 15px;
}

.tz-hint {
  font-size: var(--t-xs);
  color: var(--ink-3);
}

.tz-error-text {
  font-size: var(--t-xs);
  color: var(--err);
}

.tz-input[aria-invalid="true"] {
  border-color: var(--err);
}

/* Поле выбора файла (аватар в профиле). Нативная кнопка внутри инпута не
   наследует от .tz-input ничего, и системный серый прямоугольник рядом с
   нашими контролами читается как чужой элемент — красим её отдельно. */
.tz-input[type="file"] {
  padding: 6px 8px;
  cursor: pointer;
}

.tz-input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 4px 10px;
  border: 1px solid var(--rule-ctl);
  border-radius: var(--r-ctl);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-xs);
  cursor: pointer;
}

.tz-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-s);
  cursor: pointer;
}

.tz-switch input {
  appearance: none;
  width: 34px;
  height: 20px;
  border-radius: var(--r-pill);
  background: var(--rule-ctl);
  position: relative;
  cursor: pointer;
  transition: background var(--d-fast);
  margin: 0;
}

.tz-switch input::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: var(--r-pill);
  background: var(--paper);
  transition: transform var(--d-fast);
}

.tz-switch input:checked {
  background: var(--ball-deep);
}

.tz-switch input:checked::after {
  transform: translateX(14px);
}

/* --- 6. Чипы, бейджи, фильтры ------------------------------------------- */

.tz-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.tz-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--rule-ctl);
  border-radius: var(--r-pill);
  padding: 4px 11px;
  font-size: var(--t-xs);
  font-weight: 550;
  background: var(--paper);
  color: var(--ink-2);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.tz-chip:hover {
  border-color: var(--ink-3);
  color: var(--ink);
}

.tz-chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.tz-chip--clear {
  border-style: dashed;
  color: var(--ink-3);
}

.tz-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 4px;
  padding: 2px 7px;
  font-size: var(--t-2xs);
  font-weight: 600;
  background: var(--surface-2);
  color: var(--ink-2);
  white-space: nowrap;
}

.tz-tag--ok {
  background: var(--ok-wash);
  color: var(--ok);
}
.tz-tag--warn {
  background: var(--warn-wash);
  color: var(--warn);
}
.tz-tag--err {
  background: var(--err-wash);
  color: var(--err);
}
.tz-tag--ball {
  background: var(--ball);
  color: var(--ink);
}

/* Покрытие: точка цвета корта + подпись */
.tz-surface {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--t-xs);
  color: var(--ink-2);
  white-space: nowrap;
}

.tz-surface::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--surface-color);
  flex: none;
}

.s-clay {
  --surface-color: var(--clay);
}
.s-hard {
  --surface-color: var(--hard);
}
.s-grass {
  --surface-color: var(--grass);
}
.s-carpet {
  --surface-color: var(--carpet);
}

/* --- 7. Панель фильтров -------------------------------------------------- */

.tz-filters {
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  padding: var(--s4);
  display: grid;
  gap: var(--s4);
  background: var(--paper);
  margin-bottom: var(--s5);
}

.tz-filters__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: var(--s3);
}

.tz-filters__foot {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  border-top: 1px solid var(--rule);
  padding-top: var(--s3);
}

.tz-filters__foot .tz-btn:last-child {
  margin-left: auto;
}

/* Со скриптом фильтр применяется сам, и «Найти» становится кнопкой, которая
   ничего не меняет. Прячем — но только её: чипы категорий тоже сабмиты.
   Без скрипта класса has-js нет, кнопка на месте, форма уходит как обычно.
   Вправо тогда отъезжает «Сбросить всё» — единственная ссылка-кнопка в
   подвале фильтров. */
.has-js .tz-filters__foot .tz-btn[type="submit"] {
  display: none;
}

.has-js .tz-filters__foot > a.tz-btn:last-of-type {
  margin-left: auto;
}

/* Ожидание ответа — тонкая полоса поверху. Форму фильтров не гасим: она лежит
   внутри #content, и человек продолжает набирать в поле, — приглушается только
   сама выдача. И полоса, и приглушение выезжают с задержкой, иначе быстрый
   ответ давал бы мигание на каждой галке. */
#content[aria-busy="true"]::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 95; /* выше липкой шапки (90) */
  background: linear-gradient(90deg, transparent, var(--ball-deep), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: tz-progress 0.9s linear 150ms infinite;
  opacity: 0;
}

/* Выдача (#tzlist — общая обёртка списка на турнирах, матчах и игроках)
   бледнеет, но остаётся на месте: страница не прыгает, а видно, что цифры под
   курсором уже несвежие. Переход объявлен только здесь, в состоянии ожидания:
   обратно яркость возвращается мгновенно, иначе подменённый список ещё
   полсекунды доходил бы до полной. */
#content[aria-busy="true"] #tzlist {
  opacity: 0.5;
  transition: opacity var(--d-fast) linear 150ms;
}

@keyframes tz-progress {
  from {
    background-position: -40% 0;
    opacity: 1;
  }
  to {
    background-position: 140% 0;
    opacity: 1;
  }
}

/* Показать полосу без движения. Просто animation: none оставляло бы её
   невидимой: видимость дают кадры, а opacity в покое — 0. Поэтому полоса
   зажигается через fill-mode, и задержка против мигания при этом сохраняется —
   глобальный сброс ниже по файлу режет длительность, но не задержку. */
@keyframes tz-progress-hold {
  to {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  #content[aria-busy="true"]::before {
    background: var(--ball-deep);
    animation: tz-progress-hold 1ms linear 150ms forwards;
  }
}

/* --- 8. Кастомный селект и живой поиск ---------------------------------- */

.csel {
  position: relative;
}

.csel__toggle {
  width: 100%;
  text-align: left;
  border: 1px solid var(--rule-ctl);
  background: var(--paper);
  border-radius: var(--r-ctl);
  padding: 7px 28px 7px 10px;
  font-size: var(--t-s);
  cursor: pointer;
  position: relative;
}

.csel__toggle::after {
  content: "";
  position: absolute;
  right: 10px;
  top: 50%;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--ink-3);
  border-bottom: 1.5px solid var(--ink-3);
  transform: translateY(-70%) rotate(45deg);
}

.csel__panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  min-width: 240px;
  background: var(--paper);
  border: 1px solid var(--rule-2);
  border-radius: var(--r-box);
  box-shadow: var(--sh-pop);
  padding: var(--s2);
  display: none;
  max-height: 320px;
  overflow: auto;
}

.csel[data-open="true"] .csel__panel {
  display: block;
}

/* Без скрипта раскрывать панель нечем — показываем список сразу, а кнопку
   с текущим выбором убираем: она ничего не открывает. Через @media, а не через
   класс has-js: скрипт отложенный (defer), класс приезжает после разбора
   страницы, и все панели успели бы мигнуть раскрытыми. */
@media (scripting: none) {
  .csel__toggle {
    display: none;
  }

  .csel__panel {
    display: block;
    position: static;
    min-width: 0;
    max-height: 220px;
    box-shadow: none;
    border-color: var(--rule-ctl);
  }
}

.csel__search {
  position: sticky;
  top: 0;
  background: var(--paper);
  padding-bottom: var(--s2);
}

.csel__group {
  font-size: var(--t-2xs);
  font-weight: 650;
  color: var(--ink-3);
  padding: var(--s2) var(--s2) 4px;
}

.csel__opt {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 6px var(--s2);
  border-radius: 5px;
  font-size: var(--t-s);
  cursor: pointer;
}

.csel__opt:hover {
  background: var(--surface);
}

.csel__opt input {
  accent-color: var(--ball-deep);
}

.csel__opt .num {
  margin-left: auto;
  color: var(--ink-3);
  font-size: var(--t-xs);
}

/* --- 9. Календарь-фильтр с тепловой картой ------------------------------ */

.tz-cal {
  display: grid;
  gap: var(--s2);
}

.tz-cal__strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 3px;
  align-items: end;
  height: 46px;
}

.tz-cal__day {
  background: var(--surface-2);
  border: 0;
  border-radius: 2px 2px 0 0;
  padding: 0;
  cursor: pointer;
  min-height: 3px;
  transition: background var(--d-fast);
}

.tz-cal__day[data-heat="1"] {
  background: var(--ball-wash);
}
.tz-cal__day[data-heat="2"] {
  background: var(--ball-soft);
}
.tz-cal__day[data-heat="3"] {
  background: var(--ball);
}
.tz-cal__day[data-heat="4"] {
  background: var(--ball-deep);
}

.tz-cal__day[aria-pressed="true"] {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}

.tz-cal__axis {
  display: flex;
  justify-content: space-between;
  font-size: var(--t-2xs);
  color: var(--ink-3);
  font-family: var(--font-num);
}

/* --- 10. ВЕРДИКТ О ДОПУСКЕ — сигнатурный компонент ---------------------- */
/*
   Читается за долю секунды по трём сигналам, в порядке важности:
   1) «лесенка» — где ты падаешь относительно двух отсечек;
   2) короткий код (Основа / Квала / Мимо);
   3) число — запас или недобор в позициях рейтинга.
   Цветом выделяется только собственная метка игрока: сами сегменты
   лесенки нейтральные. Поэтому сотня карточек в списке не превращается
   в светофор.
*/

.tz-verdict {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
}

.tz-ladder {
  display: grid;
  grid-template-columns: 34px 22px 16px;
  gap: 2px;
  align-items: stretch;
  height: 26px;
  flex: none;
}

.tz-ladder__seg {
  background: var(--surface-2);
  border-radius: 1px;
  position: relative;
}

.tz-ladder__seg--qual {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--rule);
}

.tz-ladder__seg--out {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--rule);
}

/* метка игрока */
.tz-ladder__pin {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 3px;
  background: var(--ink);
  border-radius: var(--r-pill);
  box-shadow: 0 0 0 2px var(--paper);
}

.v-main .tz-ladder__seg--main {
  background: var(--ball);
}
.v-qual .tz-ladder__seg--qual {
  background: var(--ball-soft);
  box-shadow: none;
}
.v-out .tz-ladder__seg--out {
  background: var(--surface);
}

.tz-verdict__text {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.tz-verdict__code {
  font-size: var(--t-s);
  font-weight: 650;
  line-height: 1.15;
}

.v-out .tz-verdict__code {
  color: var(--ink-3);
  font-weight: 550;
}

.tz-verdict__math {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  color: var(--ink-3);
  white-space: nowrap;
}

/* Раскрываемая арифметика */
.tz-verdict__more {
  border: 0;
  background: transparent;
  padding: 2px;
  cursor: pointer;
  color: var(--ink-3);
  border-radius: 4px;
}

.tz-verdict__more:hover {
  background: var(--surface);
  color: var(--ink);
}

/* Раскрытый расчёт лежит внутри строки турнира, а по строке растянута ссылка:
   без своего слоя клик по расчёту уводил бы на турнир. */
.tz-math {
  position: relative;
  z-index: 1;
  margin-top: var(--s3);
  border-top: 1px dashed var(--rule-2);
  padding-top: var(--s3);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
  gap: var(--s3);
}

.tz-math__cell dt {
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-math__cell dd {
  margin: 2px 0 0;
  font-family: var(--font-num);
  font-size: var(--t-s);
  color: var(--ink);
}

/* Прогноз круга и очков */
.tz-forecast {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-num);
  font-size: var(--t-xs);
  color: var(--ink-2);
  background: var(--surface);
  border-radius: var(--r-pill);
  padding: 3px 9px;
}

/* Сводка допуска над списком турниров: по чьей карточке считаем и как выдача
   разложилась по вердикту. Числа — ссылки-фильтры: они же единственное место,
   где эта раскладка есть, поэтому дублировать её сегментом в фильтрах нечем. */
.tz-sum {
  display: flex;
  align-items: center;
  gap: var(--s5);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--ball-soft);
  background: var(--ball-wash);
  border-radius: var(--r-ctl);
}

.tz-sum__me {
  display: grid;
  gap: 1px;
}

.tz-sum__me b {
  font-size: var(--t-s);
}

.tz-sum__me span {
  font-size: var(--t-2xs);
  color: var(--ink-2);
}

.tz-sum__n {
  display: grid;
  gap: 0;
  line-height: 1.1;
  color: inherit;
  text-decoration: none;
  border-bottom: 2px solid transparent;
}

.tz-sum__n b {
  font-size: var(--t-xl);
  font-weight: 650;
  letter-spacing: -0.03em;
}

.tz-sum__n span {
  font-size: var(--t-2xs);
  color: var(--ink-2);
}

.tz-sum__n--out b {
  color: var(--ink-3);
}

a.tz-sum__n:hover {
  border-bottom-color: var(--rule-ctl);
}

a.tz-sum__n[aria-current="true"] {
  border-bottom-color: var(--ink);
}

/* --- 11. Карточка турнира ------------------------------------------------ */

.tz-list {
  display: grid;
  gap: 0;
  isolation: isolate;
  padding: 0 var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  background: var(--paper);
}

/* Боковой отступ держит сама карточка: у строк своего нет. Из-за этого и
   разделительный волосок, и подсветка наведения обрываются, не доходя до
   рамки, — список склеен, но не упирается в собственный край. */
.tz-list > .tz-tour,
.tz-list > .tz-match {
  padding-left: 0;
  padding-right: 0;
}

/* Нижняя строка упирается в рамку карточки — своя линия была бы второй. */
.tz-list > *:last-child {
  border-bottom: 0;
}

/* Продолжение того же списка (скрытый хвост «показать ещё») идёт вторым
   контейнером — его верхнюю линию гасим, иначе на стыке было бы две. */
.tz-list + .tz-list {
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* Пока хвост раскрыт, обе карточки должны читаться одной: у верхней гасим
   нижние скругления и возвращаем линию под последней строкой — на стыке иначе
   получился бы разрыв. */
.tz-list:has(+ .tz-list:not([hidden])) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.tz-list:has(+ .tz-list:not([hidden])) > *:last-child {
  border-bottom: 1px solid var(--rule);
}

/* Внутри flush-панели список прижат к краям, как таблица: рамку, фон и
   скругление держит панель, поэтому своей карточки у списка тут нет, а боковой
   отступ снова уходит строкам. */
.tz-panel--flush > .tz-list {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.tz-panel--flush > .tz-list > .tz-tour,
.tz-panel--flush > .tz-list > .tz-match {
  padding-left: var(--s3);
  padding-right: var(--s3);
}

.tz-panel--flush > .tz-list:last-child > :last-child {
  border-bottom: 0;
}

.tz-tour {
  display: grid;
  grid-template-columns: 60px minmax(0, 1fr) auto;
  gap: var(--s4);
  padding: var(--s4) var(--s3);
  border-bottom: 1px solid var(--rule);
  align-items: start;
  position: relative;
  transition: background var(--d-fast);
}

/* Наведение — не фон строки, а отдельная плашка под ней: фоном строка красилась
   бы целиком, вплотную к разделителям и к рамке карточки.

   Геометрия у плашки одна на все четыре стороны — 8px: столько же от её края
   до содержимого, сколько от её края до ближайшей линии. По вертикали ближайшая
   линия — разделитель, по горизонтали — рамка карточки, и до неё плашка
   дотягивается, выходя за разделитель в отступ карточки. Отступ по бокам взят
   оттуда, а не поджатием содержимого: двигать строку к центру ради него не
   нужно. */
.tz-list > .tz-tour::before,
.tz-list > .tz-match::before {
  content: "";
  position: absolute;
  inset: var(--s2) calc(var(--s2) * -1);
  z-index: -1;
  border-radius: var(--r-ctl);
  background: none;
  transition: background var(--d-fast);
  pointer-events: none;
}

/* В flush-панели своего отступа у карточки нет — вылезать плашке некуда, зато
   боковой отступ снова у строк. Половина этого отступа со всех сторон и даёт ту
   же ровную рамку вокруг плашки. */
.tz-panel--flush > .tz-list > .tz-tour::before,
.tz-panel--flush > .tz-list > .tz-match::before {
  inset: calc(var(--s3) / 2);
}

.tz-list > .tz-tour:hover::before,
.tz-list > .tz-match--link:hover::before {
  background: var(--surface);
}

.tz-tour__date {
  font-family: var(--font-num);
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 4px;
  text-align: center;
  padding-top: 2px;
}

.tz-tour__day {
  font-size: var(--t-l);
  font-weight: 600;
  line-height: 1;
}

.tz-tour__mon {
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-tour__body {
  min-width: 0;
}

/* Кликается вся строка, а не одно название: ссылка растянута по карточке. Слой
   у неё нулевой — выше подсветки наведения (-1) и выше обычного содержимого,
   но ниже колонок со своими кнопками (.tz-tour__side, .tz-tour__extra,
   .tz-vcol): их клики остаются своими. */
.tz-tour__open {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.tz-tour__name {
  position: relative;
  z-index: 1;
  font-size: var(--t-m);
  font-weight: 600;
  text-decoration: none;
}

.tz-tour__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  margin-top: 5px;
  font-size: var(--t-xs);
  color: var(--ink-2);
}

.tz-tour__side {
  display: grid;
  gap: var(--s2);
  justify-items: end;
  position: relative;
  z-index: 1;
}

.tz-tour__cat {
  font-family: var(--font-num);
  font-size: var(--t-s);
  font-weight: 650;
  border: 1px solid var(--ink);
  border-radius: 3px;
  padding: 1px 6px;
}

.tz-tour__extra {
  grid-column: 2 / -1;
  position: relative;
  z-index: 1;
}

/* Строка с колонкой вердикта — список турниров по своему рейтингу. Вердикт
   стоит справа колонкой фиксированной ширины, а не полосой под карточкой:
   строка не растёт в высоту, и глаз идёт по одной вертикали Основа / Квала /
   Мимо. Цвет несёт только левая планка колонки, поэтому сотня строк не
   превращается в светофор. Категория переезжает к названию — правое место
   занято вердиктом. */
.tz-tour--v {
  grid-template-columns: 60px minmax(0, 1fr) 216px;
  align-items: center;
  padding: var(--s3);
}

.tz-tour__title {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

.tz-tour--v .tz-tour__cat {
  font-size: var(--t-2xs);
  padding: 0 5px;
  flex: none;
}

.tz-tour--v .tz-tour__meta {
  margin-top: 4px;
}

.tz-tour--v .tz-math {
  grid-column: 1 / -1;
}

.tz-vcol {
  display: grid;
  position: relative;
  z-index: 1;
  gap: 2px;
  padding-left: var(--s3);
  border-left: 3px solid var(--rule-2);
}

.tz-vcol--main {
  border-color: var(--ball-deep);
}

/* Игрок уже в официальном листе ITF — это не проекция, и планка другая */
.tz-vcol--in {
  border-color: var(--ok);
}

.tz-vcol--qual {
  border-color: var(--ball);
}

.tz-vcol__code {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--t-s);
  font-weight: 650;
  line-height: 1.2;
}

.tz-vcol--out .tz-vcol__code {
  color: var(--ink-3);
  font-weight: 550;
}

/* Запас одним числом. Текст плашки темнее прототипа (--ink-2 вместо --ink-3):
   11px на утопленном фоне не добирали до AA, см. contrast_test.go. */
.tz-vcol__gap {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  font-weight: 600;
  white-space: nowrap;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--surface-2);
  color: var(--ink-2);
}

.tz-vcol__gap--plus {
  background: var(--ball-wash);
  color: var(--ball-ink);
}

.tz-vcol__nums {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-vcol__fc {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  color: var(--ink-2);
}

/* Карточка того же турнира в планировщике и в наблюдаемом — тот же класс,
   отличается только контейнером */
.tz-list--compact .tz-tour {
  padding: var(--s3);
  grid-template-columns: 48px minmax(0, 1fr) auto;
}

/* --- 12. Таблицы --------------------------------------------------------- */

.tz-tablewrap {
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  /* Бумага под таблицей нужна явно: таблица часто лежит прямо на поле раздела,
     а не внутри панели, и без фона строки красились бы полем (--canvas). */
  background: var(--paper);
}

.tz-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-s);
}

.tz-table th {
  text-align: left;
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--ink-2);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  white-space: nowrap;
  position: sticky;
  top: 0;
}

.tz-table td {
  padding: var(--s3);
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}

.tz-table tbody tr:last-child td {
  border-bottom: 0;
}

.tz-table tbody tr:hover {
  background: var(--surface);
}

.tz-table tr[data-me="true"] {
  background: var(--ball-wash);
}

.tz-table tr[data-me="true"] td:first-child {
  box-shadow: inset 3px 0 0 var(--ball-deep);
}

.tz-table td.right,
.tz-table th.right {
  text-align: right;
}

.tz-table--fixed {
  table-layout: fixed;
}

/* Движение в рейтинге рядом с номером: сама стрелка приходит символом, класс
   отвечает только за размер и цвет направления. */
.tz-delta {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
}

.tz-delta.up {
  color: var(--ok);
}

.tz-delta.dn {
  color: var(--err);
}

/* --- 13. Панели, боксы, сетки -------------------------------------------- */

.tz-panel {
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  padding: var(--s5);
  background: var(--paper);
}

.tz-panel--flush {
  padding: 0;
  overflow: hidden;
}

.tz-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}

.tz-panel__body {
  padding: var(--s4);
}

.tz-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--s5);
  align-items: start;
}

.tz-cols--even {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.tz-stack {
  display: grid;
  gap: var(--s4);
}

.tz-stack--tight {
  gap: var(--s2);
}

.tz-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}

.tz-kpi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  overflow: hidden;
}

.tz-kpi__cell {
  background: var(--paper);
  padding: var(--s4);
}

.tz-kpi__label {
  font-size: var(--t-xs);
  color: var(--ink-3);
}

.tz-kpi__value {
  font-family: var(--font-num);
  font-size: var(--t-xl);
  letter-spacing: -0.03em;
  margin-top: 2px;
}

.tz-kpi__delta {
  font-size: var(--t-xs);
  font-family: var(--font-num);
}

.tz-kpi__delta.up {
  color: var(--ok);
}
.tz-kpi__delta.down {
  color: var(--err);
}

/* --- 14. Вкладки и подменю ----------------------------------------------- */

.tz-tabs {
  display: flex;
  gap: var(--s5);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--s5);
  overflow-x: auto;
}

.tz-tabs a,
.tz-tabs button {
  border: 0;
  background: transparent;
  padding: 0 0 var(--s3);
  font-size: var(--t-s);
  font-weight: 550;
  color: var(--ink-3);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.tz-tabs [aria-current="true"],
.tz-tabs [aria-selected="true"] {
  color: var(--ink);
  border-bottom-color: var(--ball-deep);
}

.tz-tabs .tz-tag {
  font-size: var(--t-3xs);
}

/* --- 15. Состояния: пусто / загрузка / ошибка ---------------------------- */

.tz-empty {
  border: 1px dashed var(--rule-2);
  border-radius: var(--r-box);
  padding: var(--s7) var(--s5);
  text-align: center;
  display: grid;
  gap: var(--s3);
  justify-items: center;
}

.tz-empty h3 {
  font-size: var(--t-l);
}

.tz-empty p {
  color: var(--ink-2);
  max-width: 44ch;
  margin: 0;
}

.tz-note {
  border-left: 3px solid var(--rule-2);
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
  font-size: var(--t-s);
  color: var(--ink-2);
}

.tz-note--err {
  border-left-color: var(--err);
  background: var(--err-wash);
  color: var(--err);
}

.tz-note--warn {
  border-left-color: var(--warn);
  background: var(--warn-wash);
  color: var(--warn);
}

.tz-note--ok {
  border-left-color: var(--ok);
  background: var(--ok-wash);
  color: var(--ok);
}

/* Скелет загрузки. display:block обязателен — это <span>, а высота у
   строчного элемента не применяется, и полоса схлопнулась бы в ничто. */
.tz-skel {
  display: block;
  background: linear-gradient(
    90deg,
    var(--surface) 0%,
    var(--surface-2) 50%,
    var(--surface) 100%
  );
  background-size: 200% 100%;
  animation: tz-shimmer 1.3s linear infinite;
  border-radius: 3px;
  height: 12px;
}

@keyframes tz-shimmer {
  to {
    background-position: -200% 0;
  }
}

.tz-spinner {
  display: inline-block;
  vertical-align: -3px;
  width: 16px;
  height: 16px;
  border: 2px solid var(--rule-2);
  border-top-color: var(--ink);
  border-radius: var(--r-pill);
  animation: tz-spin 0.7s linear infinite;
}

@keyframes tz-spin {
  to {
    transform: rotate(360deg);
  }
}

/* --- 16. Пагинация ------------------------------------------------------- */

.tz-pager {
  display: flex;
  align-items: center;
  gap: var(--s2);
  justify-content: center;
  margin-top: var(--s5);
  flex-wrap: wrap;
}

.tz-pager a,
.tz-pager span {
  min-width: 32px;
  height: 32px;
  display: inline-grid;
  place-items: center;
  padding: 0 var(--s2);
  border-radius: var(--r-ctl);
  font-family: var(--font-num);
  font-size: var(--t-s);
  text-decoration: none;
  color: var(--ink-2);
}

.tz-pager a:hover {
  background: var(--surface);
}

.tz-pager [aria-current="page"] {
  background: var(--ink);
  color: var(--paper);
}

.tz-pager__total {
  margin-left: var(--s3);
  color: var(--ink-3);
  font-size: var(--t-xs);
}

/* --- 17. Модалка и попап ------------------------------------------------- */

.tz-modal {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: none;
  place-items: center;
  padding: var(--s4);
  background: rgba(22, 23, 15, 0.42);
}

.tz-modal[data-open="true"] {
  display: grid;
}

.tz-modal__box {
  background: var(--paper);
  border-radius: var(--r-box);
  box-shadow: var(--sh-pop);
  width: min(560px, 100%);
  max-height: 88vh;
  overflow: auto;
}

.tz-modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--rule);
}

.tz-modal__body {
  padding: var(--s5);
}

.tz-modal__foot {
  display: flex;
  gap: var(--s2);
  justify-content: flex-end;
  padding: var(--s3) var(--s5) var(--s5);
}

/* Тост — единственный на документ, живёт в углу и всплывает по data-открытию.
   Успех и ошибка различаются полосой слева: цвет читается и без текста. */
.tz-toast {
  position: fixed;
  left: 50%;
  bottom: var(--s5);
  z-index: 90;
  transform: translate(-50%, 12px);
  max-width: min(420px, calc(100vw - 2 * var(--s4)));
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink-3);
  border-radius: var(--r-ctl);
  background: var(--paper);
  box-shadow: var(--sh-pop);
  font-size: var(--t-s);
  color: var(--ink);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.16s ease, transform 0.16s ease;
}

.tz-toast[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.tz-toast[data-kind="ok"] {
  border-left-color: var(--ok);
}

.tz-toast[data-kind="err"] {
  border-left-color: var(--err);
}

@media (prefers-reduced-motion: reduce) {
  .tz-toast {
    transition: none;
  }
}

/* --- 18. Матч ------------------------------------------------------------ */

/* Карточка матча: имена по краям крупно, между ними счёт по сетам одним числом,
   под ним геймы, турнир с кругом и датой — подвалом. Вся карточка — ссылка на
   матч: растянутый tz-match__open лежит поверх, поэтому ссылки внутри подняты
   над ним z-index'ом.

   В ленте живёт по правилам строки турнира (.tz-tour): своей рамки нет, боков
   нет, соседей делит одна линия снизу — список читается сплошным полотном, а не
   россыпью коробок. */
/* Сетка карточки: имя — счёт — имя, под ними подпись и метки. Обёртки-строки
   у имён нет: колонка счёта растягивается на обе строки и встаёт по центру всей
   карточки. Со строкой счёт всегда сидел выше середины — подвал такая же часть
   карточки, и это было видно. */
.tz-match {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  column-gap: var(--s4);
  padding: var(--s4) var(--s3);
  border-bottom: 1px solid var(--rule);
  transition: background var(--d-fast);
}

/* Растягиваем счёт только когда подвал есть: без него вторая строка карточке
   не нужна, а пустая забрала бы себе межстрочный шаг. Браузер без :has()
   просто оставит счёт по центру строки имён — так было до этого. */
.tz-match:has(.tz-match__foot, .tz-match__tags--foot) {
  row-gap: var(--s3);
}

.tz-match:has(.tz-match__foot, .tz-match__tags--foot) .tz-match__mid {
  grid-row: 1 / span 2;
}

/* Карточкой-ссылкой не может быть сам <article>: внутри свои ссылки, а
   вкладывать их друг в друга нельзя. Ссылка лежит поверх карточки и вне потока:
   в сетке она иначе заняла бы собой ячейку. */
.tz-match__open {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.tz-match__open:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* Карточка на самой странице матча: рамку и фон даёт панель вокруг. */
.tz-match--flat {
  padding: 0;
  border: 0;
  background: none;
}

.tz-match__side {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.tz-match__side--b {
  justify-items: end;
  text-align: right;
}

.tz-match__name {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  position: relative;
  z-index: 2;
  font-size: var(--t-l);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-decoration: none;
}

/* Имя игрока подчёркивать нечем: карточка кликабельна целиком, отклик даёт
   цвет. Селектор с элементом — чтобы перебить и приглушённый цвет проигравшего
   (.tz-match__side--lose весит столько же, сколько два класса). */
a.tz-match__name:hover {
  color: var(--ball-deep);
}

.tz-match__side--win .tz-match__name {
  font-weight: 650;
}

.tz-match__side--lose .tz-match__name {
  color: var(--ink-3);
  font-weight: 550;
}

.tz-match__tags {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  font-size: var(--t-xs);
  color: var(--ink-2);
}

.tz-match__dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--ball-deep);
  flex: none;
}

.tz-match__mid {
  grid-column: 2;
  align-self: center;
  display: grid;
  justify-items: center;
  gap: 3px;
  padding: 0 var(--s2);
}

.tz-match__big {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-2xl);
  font-weight: 650;
  line-height: 1;
  letter-spacing: -0.04em;
}

/* Двоеточие — разделитель, а не цифра: гасим, чтобы читались числа */
.tz-match__big i {
  font-style: normal;
  color: var(--rule-2);
  padding: 0 1px;
}

.tz-match__big--none {
  font-size: var(--t-xl);
  color: var(--ink-3);
}

/* Подвал отделён воздухом, а не линией: единственная линия в ленте — та, что
   делит матчи. Вторая, вполовину уже, спорила бы с ней за смысл. */
.tz-match__foot {
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
  font-size: var(--t-xs);
  color: var(--ink-2);
}

/* Метки исхода и покрытия — правая часть подвала, своей ячейкой у внешнего
   края. Место задано явно: подпись слева может и не прийти, а метки всё равно
   должны остаться справа, а не переехать под первое имя. */
.tz-match__tags--foot {
  grid-column: 3;
  justify-self: end;
}

.tz-match__foot a {
  position: relative;
  z-index: 2;
  color: var(--ink);
  font-weight: 600;
}

.tz-score {
  display: flex;
  gap: var(--s2);
  font-family: var(--font-num);
  font-size: var(--t-s);
  font-variant-numeric: tabular-nums;
}

.tz-match__mid .tz-score {
  font-size: var(--t-xs);
  color: var(--ink-2);
  white-space: nowrap;
}

/* Финал внутри строки турнира — та же карточка, но мельче: строка списка не
   должна вырастать в пол-экрана из-за одного матча. */
.tz-tour__extra .tz-match__name {
  font-size: var(--t-m);
}

.tz-tour__extra .tz-match__big {
  font-size: var(--t-xl);
}

.tz-score b {
  font-weight: 650;
}

.tz-score sup {
  font-size: 9px;
  color: var(--ink-3);
}

/* --- 19. Профиль игрока -------------------------------------------------- */

.tz-hero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s5);
  align-items: center;
  padding: var(--s5) 0 var(--s5);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--s5);
}

.tz-hero__portrait {
  width: 72px;
  height: 72px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  display: grid;
  place-items: center;
  font-size: var(--t-xl);
  font-weight: 600;
  color: var(--ink-2);
}

/* Фото вместо инициалов (аватар из аккаунта провайдера). Без overflow
   квадратная картинка вылезла бы за круг. */
.tz-hero__portrait--img {
  overflow: hidden;
}

.tz-hero__portrait--img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.tz-ranks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--s4);
}

.tz-rank__label {
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-rank__value {
  font-family: var(--font-num);
  font-size: var(--t-l);
  letter-spacing: -0.03em;
}

/* Полоса W/L */
.tz-bar {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  overflow: hidden;
  display: flex;
}

.tz-bar__fill {
  background: var(--ball-deep);
  height: 100%;
}

.tz-form {
  display: inline-flex;
  gap: 3px;
}

.tz-form span {
  width: 16px;
  height: 16px;
  border-radius: 3px;
  display: grid;
  place-items: center;
  font-size: var(--t-3xs);
  font-weight: 700;
  font-family: var(--font-num);
  background: var(--surface-2);
  color: var(--ink-3);
}

.tz-form .w {
  background: var(--ball);
  color: var(--ink);
}

.tz-form .l {
  background: var(--surface-2);
  color: var(--ink-3);
}

/* Наблюдение: отмеченная звезда закрашена. В списке из 25 строк состояние
   должно читаться сразу, а не по наведению. */
[data-watch][aria-pressed="true"] svg {
  fill: var(--ball);
  stroke: var(--ball-deep);
}

[data-watch][aria-busy="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* --- 20. Планировщик: недельная лента ------------------------------------ */

.tz-week {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: var(--s4);
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--rule);
}

.tz-week__label {
  font-family: var(--font-num);
  font-size: var(--t-xs);
  color: var(--ink-3);
  position: sticky;
  top: 96px;
  align-self: start;
}

.tz-week__num {
  font-size: var(--t-l);
  color: var(--ink);
  display: block;
}

.tz-pick {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-xs);
  cursor: pointer;
}

/* В карточке турнира отметка стоит в строке метаданных — правое место занято
   колонкой вердикта. Обводки у неё нет: строка отмечается кликом в любое
   место, а плашка обещала бы, что попасть надо именно в неё. */
.tz-tour__meta .tz-pick {
  margin-left: auto;
  white-space: nowrap;
}

/* Строка турнира в плане отмечается целиком — курсор про это и говорит.
   Только в плане: в поиске турниров у строки такого действия нет. */
.tz-week .tz-tour {
  cursor: pointer;
}

.tz-pick input {
  accent-color: var(--ball-deep);
  width: 16px;
  height: 16px;
}

/* Пропущенная неделя не исчезает — гасим её, чтобы решение было видно и
   обратимо: скрытая неделя читалась бы как «турниров нет». */
.tz-week[data-skipped="true"] .tz-list {
  opacity: 0.4;
}

.tz-week[data-skipped="true"] [data-week-skip] {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

/* --- 21. Помощник -------------------------------------------------------- */

.tz-assistant {
  position: fixed;
  right: var(--s5);
  bottom: var(--s5);
  z-index: 60;
}

.tz-assistant__btn {
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-pill);
  padding: 10px 16px;
  font-size: var(--t-s);
  font-weight: 550;
  cursor: pointer;
  box-shadow: var(--sh-pop);
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
}

/* Лента диалога. Реплики — не «пузыри мессенджера», а плитки одной сетки:
   помощник здесь инструмент фильтрации, а не собеседник. */
.tz-chat {
  display: grid;
  gap: var(--s2);
  max-height: 44vh;
  overflow: auto;
}

.tz-chat__msg {
  padding: var(--s2) var(--s3);
  border: 1px solid var(--rule);
  border-radius: var(--r-ctl);
  background: var(--surface);
  font-size: var(--t-s);
  max-width: 92%;
  white-space: pre-wrap;
}

.tz-chat__msg--user {
  justify-self: end;
  border-color: var(--ball-soft);
  background: var(--ball-wash);
}

/* Ответ помощника — базовый пузырь у левого края; модификатор нужен, чтобы
   реплики бота отличались от пользовательских не только положением. */
.tz-chat__msg--bot {
  justify-self: start;
}

.tz-chat__msg--typing {
  color: var(--ink-3);
}

.tz-chat__msg--err {
  border-color: var(--err);
  background: var(--err-wash);
  color: var(--err);
}

/* Визитка результата: заголовок, строки-ссылки, кнопка «открыть выдачу» */
.tz-chat__card {
  max-width: 100%;
  padding: 0;
  overflow: hidden;
}

.tz-chat__title {
  padding: var(--s2) var(--s3);
  font-weight: 600;
}

.tz-chat__sub {
  padding: 0 var(--s3) var(--s2);
  color: var(--ink-2);
  font-size: var(--t-xs);
}

.tz-chat__item {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--rule);
  color: inherit;
  text-decoration: none;
  font-size: var(--t-xs);
}

a.tz-chat__item:hover {
  background: var(--paper);
}

.tz-chat__item .tz-chat__meta {
  color: var(--ink-3);
}

.tz-chat__badge {
  margin-left: auto;
  flex: none;
  padding: 1px var(--s2);
  border-radius: var(--r-pill);
  background: var(--ball);
  color: var(--ball-ink);
  font-size: var(--t-3xs);
  font-weight: 600;
}

.tz-chat__open {
  display: block;
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--rule);
  background: var(--paper);
  font-size: var(--t-xs);
  font-weight: 550;
}

/* --- 22. Админка --------------------------------------------------------- */
/* Своя вёрстка, но те же токены: рельса темнее, плотность выше,
   радиусы почти отсутствуют — это инструмент, а не витрина. */

.adm {
  --rail: 216px;
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100vh;
}

.adm .tz-rail {
  background: var(--ink);
  border-right: 0;
  color: var(--surface-2);
}

.adm .tz-rail .tz-brand__word,
.adm .tz-rail .tz-nav__title {
  color: var(--surface-2);
}

.adm .tz-nav__link {
  color: #b9bbac;
  font-size: var(--t-xs);
  padding: 6px var(--s2);
}

.adm .tz-nav__link:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--paper);
}

.adm .tz-nav__link[aria-current="page"] {
  background: var(--ball);
  color: var(--ink);
}

.adm .tz-me {
  border-top-color: rgba(255, 255, 255, 0.14);
  color: #b9bbac;
}

.adm-top {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--rule);
  position: sticky;
  top: 0;
  background: var(--paper);
  z-index: 40;
}

.adm-page {
  padding: var(--s5);
}

.adm-page h1 {
  font-size: var(--t-xl);
}

/* Заголовок обычной панели в админке мельче, чем на гостевых страницах:
   в прототипе admin-*.html это fs-m, а не полный h3. */
.adm .tz-panel > h3,
.adm .tz-panel > .tz-row > h3 {
  font-size: var(--t-m);
}

.adm .tz-table {
  font-size: var(--t-xs);
}

.adm .tz-table td,
.adm .tz-table th {
  padding: 7px var(--s3);
}

.adm-badge {
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  padding: 1px 6px;
  border-radius: 3px;
  background: var(--surface-2);
  color: var(--ink-2);
}

.adm-badge--ok {
  background: var(--ok-wash);
  color: var(--ok);
}
.adm-badge--err {
  background: var(--err-wash);
  color: var(--err);
}
.adm-badge--warn {
  background: var(--warn-wash);
  color: var(--warn);
}
.adm-badge--run {
  background: var(--ball);
  color: var(--ink);
}

/* Консоль / логи */
.adm-console {
  background: #101208;
  color: #d6dcc4;
  font-family: var(--font-num);
  font-size: var(--t-xs);
  line-height: 1.65;
  padding: var(--s4);
  border-radius: var(--r-ctl);
  max-height: 520px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.adm-console .ln-err {
  color: #f08c7c;
}
.adm-console .ln-ok {
  color: var(--ball);
}
.adm-console .ln-warn {
  color: #e8c46a;
}
.adm-console .ln-dim {
  color: #7c8168;
}

.adm-bar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s4);
  background: var(--ball-wash);
  border: 1px solid var(--ball-soft);
  border-radius: var(--r-ctl);
  font-size: var(--t-xs);
  margin-bottom: var(--s4);
}

/* Полоса прогресса (ширина задаётся инлайном) */
.tz-progress {
  height: 5px;
  background: var(--surface-2);
  border-radius: var(--r-pill);
  overflow: hidden;
  min-width: 72px;
}

.tz-progress > i {
  display: block;
  height: 100%;
  background: var(--ball-deep);
}

/* --- 23. Секции демонстрации состояний ---------------------------------- */
/* Используются только в прототипах: показать «гость» и «привязанный»
   рядом на одной странице. В продакшене состояние одно. */

.demo {
  border-top: 2px solid var(--ink);
  margin-top: var(--s8);
  padding-top: var(--s3);
}

.demo:first-of-type {
  margin-top: 0;
}

.demo__label {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  background: var(--ink);
  color: var(--paper);
  font-size: var(--t-2xs);
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 0 0 var(--r-ctl) var(--r-ctl);
  margin: calc(var(--s3) * -1) 0 var(--s5);
}

/* --- 24. Адаптив --------------------------------------------------------- */

@media (max-width: 1024px) {
  .tz-cols {
    grid-template-columns: minmax(0, 1fr);
  }
  .tz-hero {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .tz-hero > .tz-row:last-child {
    grid-column: 1 / -1;
  }
}

@media (max-width: 860px) {
  .tz-app,
  .adm {
    grid-template-columns: minmax(0, 1fr);
  }

  .tz-rail {
    position: fixed;
    left: 0;
    z-index: 70;
    width: 274px;
    /* Низкий экран в альбомной ориентации: меню длиннее высоты окна, и без
       прокрутки внутри рельсы нижние пункты недостижимы. */
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    transition: transform var(--d-slow);
    box-shadow: var(--sh-pop);
  }

  .tz-rail[data-open="true"] {
    transform: none;
  }

  .tz-btn.tz-burger {
    display: inline-flex;
  }

  /* Логотип возвращается в шапку: рельса уехала за край, и без него на экране
     не остаётся ничего, что называет проект. */
  .tz-top__brand {
    display: block;
    margin-right: auto;
    min-width: 0;
  }

  .tz-btn.tz-top__filters {
    display: inline-flex;
    flex: none;
  }

  /* Переключатели тура и ширины в один ряд с бургером не помещаются — девять
     контролов в строке и разворачивали шапку в три этажа. Здесь они уходят в
     ящик, который выезжает из-под шапки по кнопке «Фильтры». */
  .tz-top__ctl {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: grid;
    gap: var(--s4);
    padding: var(--s4) var(--s5) var(--s5);
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
    box-shadow: var(--sh-pop);
    /* Не «шторка сверху вниз», а короткий сдвиг с проявлением: шапка
       полупрозрачная, и выезжающий из-под неё блок просвечивал бы насквозь. */
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    transition: transform var(--d-fast), opacity var(--d-fast),
      visibility var(--d-fast);
  }

  .tz-top__ctl[data-open="true"] {
    transform: none;
    opacity: 1;
    visibility: visible;
  }

  .tz-scope,
  .tz-scope__g {
    display: grid;
    gap: var(--s2);
    justify-items: start;
  }

  .tz-scope__g {
    gap: 6px;
  }

  /* В строке шапки группы различались местом; столбиком — только подписью. */
  .tz-scope__t {
    display: block;
    margin: 0;
    font-size: var(--t-xs);
    color: var(--ink-2);
  }

  .tz-top__ctl .tz-seg {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
  }

  /* Ширина контента на телефоне ничего не меняет: полотно и так во весь
     экран, все три положения выглядят одинаково. В ящик такое не кладут. */
  .tz-top__right {
    display: none;
  }

  /* Затемнение под рельсой: z-index между контентом и рельсой (70), так что
     любой клик мимо неё приходит сюда. */
  .tz-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 60;
    background: rgba(22, 23, 15, 0.4);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--d-slow), visibility var(--d-slow);
  }

  .tz-scrim[data-open="true"] {
    opacity: 1;
    visibility: visible;
  }
}

/* Без скриптов ящик не открыть — там переключатели остаются в шапке и просто
   переносятся по строкам. Проверка через @media, а не через .has-js: скрипт
   отложенный, и класс приезжает уже после первой отрисовки — контролы успели
   бы моргнуть на своём месте. */
@media (max-width: 860px) and (scripting: none) {
  .tz-top__ctl {
    position: static;
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    transform: none;
    opacity: 1;
    visibility: visible;
  }

  .tz-btn.tz-top__filters {
    display: none;
  }
}

@media (max-width: 860px) {

  .tz-page,
  .adm-page {
    padding: var(--s5) var(--s4) var(--s7);
  }

  /* На узком экране боковой отступ карточки списка ужимаем: свои поля уже
     держит страница, а строке нужна ширина. */
  .tz-list {
    padding: 0 var(--s3);
  }

  .tz-list > .tz-tour::before,
  .tz-list > .tz-match::before {
    inset: calc(var(--s3) / 2) calc(var(--s3) / -2);
  }

  /* На узком экране строка турнира сжимается по вертикали до шага матча —
     иначе рамка вокруг плашки наведения перестаёт быть ровной. */
  .tz-tour {
    grid-template-columns: 48px minmax(0, 1fr);
    padding-top: var(--s3);
    padding-bottom: var(--s3);
  }

  .tz-tour__side {
    grid-column: 2;
    justify-items: start;
  }

  /* Матч на узком экране: имена и счёт мельче, но схема та же — имя, счёт,
     имя. Ломать её на строки нельзя: пропадёт «кто кого». */
  .tz-match {
    padding: var(--s3);
    column-gap: var(--s2);
  }

  .tz-match__name {
    font-size: var(--t-m);
  }

  .tz-match__big {
    font-size: var(--t-xl);
  }

  /* Колонка вердикта на узком экране встаёт под строку: 216px справа съели бы
     название, а вердикт без названия бесполезен. */
  .tz-tour--v {
    align-items: start;
  }

  .tz-tour--v .tz-vcol {
    grid-column: 2;
    margin-top: var(--s2);
  }

  .tz-week {
    grid-template-columns: minmax(0, 1fr);
  }

  .tz-week__label {
    position: static;
  }
}

@media (max-width: 700px) {
  /* Сравнение — горизонтальная лента с прилипанием: четыре колонки, сжатые
     до ширины телефона, перестают быть сравнением, а перенос строкой рвёт
     сопоставление значений. */
  .tz-cmp {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    padding-bottom: var(--s2);
  }

  .tz-cmp > * {
    flex: 0 0 78%;
    scroll-snap-align: start;
  }
}

@media (max-width: 560px) {
  :root {
    --t-2xl: 25px;
    --t-3xl: 30px;
  }

  .tz-hero {
    grid-template-columns: minmax(0, 1fr);
  }

  .tz-filters__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .tz-ladder {
    grid-template-columns: 28px 18px 13px;
  }
}

/* --- 25. Печать и доступность ------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

@media print {
  .tz-rail,
  .tz-top,
  .tz-assistant,
  .tz-scrim,
  .tz-pager {
    display: none;
  }
  .tz-app {
    grid-template-columns: 1fr;
  }
  /* На бумаге поле контента — это сама бумага: тонировать её принтеру нечем. */
  .tz-main {
    background: none;
  }
}

/* --- 26. Утилиты ---------------------------------------------------------
   Небольшой закрытый набор: только то, что реально повторяется в разметке.
   Inline-стили в страницах остаются исключительно для динамических значений
   (ширина полосы прогресса, позиция метки на лесенке, цвет-переменная).
   ------------------------------------------------------------------------ */

/* Атрибут hidden сильнее любого display у компонента: панели и консоли админки
   прячутся до первого ответа сервера именно им, без класса-дубликата. */
[hidden] { display: none !important; }

.m-0 { margin: 0; }
.mt-4px { margin-top: 4px; }
.mt-5px { margin-top: 5px; }
.mt-6px { margin-top: 6px; }
.mb-4px { margin-bottom: 4px; }
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mt-5 { margin-top: var(--s5); }
.mt-6 { margin-top: var(--s6); }
.mt-7 { margin-top: var(--s7); }
.mb-2 { margin-bottom: var(--s2); }
.mb-3 { margin-bottom: var(--s3); }
.mb-4 { margin-bottom: var(--s4); }
.mb-5 { margin-bottom: var(--s5); }
.mb-8 { margin-bottom: var(--s8); }
.ml-auto { margin-left: auto; }
.mx-auto { margin-left: auto; margin-right: auto; }
.p-5 { padding: var(--s5); }
.pt-4 { padding-top: var(--s4); }
.pb-0 { padding-bottom: 0; }

.fs-s { font-size: var(--t-s); }
.fs-m { font-size: var(--t-m); }
.fs-l { font-size: var(--t-l); }
.fs-xl { font-size: var(--t-xl); }
.fs-2xl { font-size: var(--t-2xl); font-weight: 600; }
.fs-3xl { font-size: var(--t-3xl); font-weight: 600; letter-spacing: -0.03em; }
.fw-55 { font-weight: 550; }
.c-ok { color: var(--ok); }
.c-err { color: var(--err); }
.c-ball { color: var(--ball-ink); }
.c-rail-dim { color: #9a9c8c; }
.as-prose { font-family: var(--font-ui); font-size: var(--t-xs); color: var(--ink-2); }

.jc-between { justify-content: space-between; }
.jc-center { justify-content: center; }
.jc-end { justify-content: flex-end; }
.ai-start { align-items: flex-start; }
.ai-end { align-items: flex-end; }
.ai-stretch { align-items: stretch; }
.as-end { align-self: flex-end; }
.fw-nowrap { flex-wrap: nowrap; }
.flex-1 { flex: 1; }
.gap-2 { gap: var(--s2); }
.span-all { grid-column: 1 / -1; }
.minw-0 { min-width: 0; }
.w-full { width: 100%; }
.flip { transform: rotate(180deg); }
.inline-mid { display: inline-block; vertical-align: middle; }
.h-26 { height: 26px; }
.w-52 { width: 52px; height: 20px; }

.bt-0 { border-top: 0; }
.bb-0 { border-bottom: 0; }
.bt-rule { border-top: 1px solid var(--rule); }
.bg-surface { background: var(--surface); border-color: var(--rule); }
.boxed { border: 1px solid var(--rule); border-radius: var(--r-box); padding: var(--s5); background: var(--paper); }
.flush-table { border: 0; border-radius: 0; }
.btn-tall { padding: 11px; }
.legend-pad { padding: 0 6px; font-weight: 600; }

.mw-150 { max-width: 150px; }
.mw-160 { max-width: 160px; }
.mw-180 { max-width: 180px; }
.mw-190 { max-width: 190px; }
.mw-200 { max-width: 200px; }
.mw-280 { max-width: 280px; }
.mw-300 { max-width: 300px; }
.mw-420 { max-width: 420px; }
.mw-460 { max-width: 460px; }
.mw-560 { max-width: 560px; }
.mw-24ch { max-width: 24ch; }
.mw-26ch { max-width: 26ch; }
.mw-28ch { max-width: 28ch; }
.mw-34ch { max-width: 34ch; }
.mw-52ch { max-width: 52ch; }

/* Именованные мелкие блоки, которые встречаются больше одного раза */
.ball-box {
  border: 1px solid var(--ball-soft);
  background: var(--ball-wash);
  border-radius: var(--r-ctl);
  padding: var(--s3) var(--s4);
}

/* Плашкой бывает и <fieldset> — у него свои поля от браузера. Гасим их точечно:
   общий margin: 0 на всём классе перебивал бы утилиты отступа у соседей. */
fieldset.ball-box {
  margin: 0;
}

.bubble {
  background: var(--surface);
  padding: var(--s3);
  border-radius: var(--r-ctl);
}

.final-block {
  border-left: 2px solid var(--ball);
  padding-left: var(--s3);
}

.nav-lock {
  margin-left: auto;
  width: 14px;
  height: 14px;
  color: var(--ink-3);
}

.sg-head {
  border-bottom: 1px solid var(--rule);
  padding-bottom: var(--s5);
  margin-bottom: var(--s6);
}

.sg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: var(--s3);
}

.sg-swatch {
  height: 56px;
  border-radius: var(--r-ctl);
  box-shadow: inset 0 0 0 1px var(--rule);
}

/* Страницы-заглушки: 404 и 403 */
.tz-center {
  min-height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  text-align: center;
  gap: var(--s4);
}

.tz-center--520 { max-width: 520px; }
.tz-center--560 { max-width: 560px; }

.tz-panel--tourhead {
  border-color: var(--rule);
  background: linear-gradient(100deg, var(--ball-wash), var(--paper) 62%);
}

.tz-panel--add {
  border-style: dashed;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--s3);
  text-align: center;
}

/* Колонки сравнения. Адаптив у них свой (см. раздел 24): ронять колонки друг
   под друга нельзя — сравнение живёт ровно тем, что значения стоят рядом. */
.tz-cmp .tz-panel {
  min-width: 0;
}

.tz-summary {
  padding: var(--s3) var(--s4);
  cursor: pointer;
  background: var(--surface);
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-s);
  font-weight: 550;
}

.nav-lock svg { width: 14px; height: 14px; }

/* --- Заявочный лист турнира ---------------------------------------------
   Одна таблица на всю панель: секции сетки (основа / резерв / квалификация)
   — полноширинные строки-заголовки внутри той же <tbody>, а не отдельные
   таблицы. Так номера, колонки и отсечка стоят в одной сетке колонок. */

.tz-entries .tz-panel__head { flex-wrap: wrap; }
.tz-entries .tz-panel__body { padding-bottom: 0; }

/* Подсказка режима — над таблицей, в тех же отступах, что и шапка. Сверху
   отступ обязателен: у .tz-panel__body здесь снят padding-bottom (чтобы
   таблица шла вплотную к краю панели), а тело кончается абзацем
   .tz-spec__note про размеры сеток. Без margin-top подложка подсказки
   упиралась в этот абзац вплотную, и два разных блока читались как один
   слипшийся. --s4 = тот самый padding, который у тела снят. */
.tz-entries [data-entry-pane] > .tz-note { margin: var(--s4) var(--s4) var(--s3); }

/* Строка-разделитель секции. Прижата к краям панели: это полоса, а не ячейка. */
.tz-table--entries tr.sec th {
  position: static;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: normal;
}

.tz-table--entries tr.sec .lbl { font-weight: 700; color: var(--ink); }

.tz-table--entries tr.sec .cnt {
  margin-left: var(--s2);
  color: var(--ink-2);
  text-transform: none;
  letter-spacing: 0;
}

.tz-table--entries tr.sec .sub {
  margin-left: var(--s3);
  color: var(--ink-2);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

/* Отсечка основы — единственная линия, которую видно издалека. */
.tz-table--entries tr.sec--qual th {
  border-top: 2px solid var(--ball-deep);
  background: var(--ball-wash);
}

/* «Затесавшийся»: место в списке занимает, но сыграет на другом турнире недели. */
.tz-table--entries tr.pass td { opacity: 0.55; }
.tz-table--entries tr.pass:hover td { opacity: 1; }

/* Заглушка места без игрока (резерв организатора, добор квалы). */
.tz-table--entries tr.ghost td {
  color: var(--ink-2);
  font-style: italic;
}

.tz-table--entries tr.ghost .dot,
.tz-table--entries tr.pass .dot { color: var(--ink-3, var(--ink-2)); }

.tz-table--entries tr.empty td {
  padding: var(--s5) var(--s4);
  text-align: center;
}

/* Карточка сводки под замком: значение открывается после входа. */
.tz-kpi__cell--lock .tz-kpi__value { opacity: 0.6; }

/* --- 26. Ширина полотна, меню пользователя, служебная плашка ------------- */

/* Режим ширины. Серверный дефолт приезжает в data-align на <html>, клиент
   переопределяет его тем же атрибутом (см. tourzone.js), поэтому выбор
   пользователя переживает переход между страницами. */
html[data-align="full"] {
  --measure: 100%;
}

html[data-align="left"] .tz-page,
html[data-align="left"] .adm-page {
  margin-left: 0;
  margin-right: auto;
}

/* Плашка состояния парсера над содержимым страницы. Видна только админам,
   поэтому намеренно неяркая: это служебная информация, а не часть продукта. */
.padm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s4);
  padding: var(--s2) var(--s3);
  border: 1px dashed var(--rule-2);
  border-radius: var(--r-ctl);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--t-xs);
}

.padm-tag {
  font-size: var(--t-3xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Элемент плашки: «подпись: значение». .lbl гасим, значение — обычным цветом,
   чтобы в потоке одинаковых пар глаз цеплялся за данные, а не за подписи. */
.padm-i {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}

.padm-i .lbl {
  color: var(--ink-3);
}

.padm-i b {
  font-weight: 550;
  color: var(--ink);
}

/* Точка у задачи, которая выполняется прямо сейчас. */
.padm-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
  animation: padm-pulse 1.4s ease-in-out infinite;
}

@keyframes padm-pulse {
  50% {
    opacity: 0.3;
  }
}

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

.padm-a {
  margin-left: auto;
}

/* Иконки (ui.Icon): инлайновый SVG, цвет — модификатором или currentColor */
.tz-ic {
  flex: none;
  vertical-align: -2px;
}

.tz-ic--muted {
  color: var(--ink-3);
}
.tz-ic--accent {
  color: var(--ball-deep);
}
.tz-ic--hard {
  color: var(--hard);
}
.tz-ic--clay {
  color: var(--clay);
}
.tz-ic--dark {
  color: var(--ink);
}
.tz-ic--win {
  color: var(--ok);
}

/* Флаг страны: настоящий флаг плюс код IOC рядом.
   Флаг — эмодзи из региональных индикаторов, нарисованное своим шрифтом
   (assets/flags.woff2): системного шрифта с флагами нет ни в Windows, ни в
   части Linux, и там пара индикаторов показалась бы двумя буквами в рамке.
   Шрифт подключается только на диапазон U+1F1E6–1F1FF, поэтому остальной
   текст его не тянет. Код IOC остаётся: 16-пиксельный флаг у половины стран
   неразличим, а на печати цвета вообще нет.

   Шрифт: Twemoji Country Flags (глифы Twemoji, © Twitter и участники,
   CC-BY 4.0) — см. assets/flags.woff2.txt. */
@font-face {
  font-family: "TZ Country Flags";
  src: url("flags.woff2") format("woff2");
  font-display: swap;
  unicode-range: U+1F1E6-1F1FF;
}

.tz-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  flex: none;
}

.tz-flag__ico {
  font-family: "TZ Country Flags", "Twemoji Country Flags", "Noto Color Emoji",
    "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
  font-size: 14px;
  line-height: 1;
  letter-spacing: 0;
}

.tz-flag__box {
  display: inline-block;
  padding: 0 4px;
  border: 1px solid var(--rule-2);
  border-radius: 3px;
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--font-num);
  font-size: var(--t-3xs);
  line-height: 1.5;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* --- 27. Страница турнира: афиша и спецификация -------------------------- */
/* Полотно страницы одного турнира: сверху афиша (единственный h1 страницы —
   у layout.Page.Heading здесь пусто), под ней спецификация строками
   «подпись раздела слева — содержимое справа». Разделов может быть два или
   семь: ритм не меняется, потому что колонок всегда ровно две. */

.tz-tourhero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--s4) var(--s5);
  padding-bottom: var(--s5);
  border-bottom: 2px solid var(--ink);
}

.tz-tourhero__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s3);
}

.tz-tourhero h1 {
  margin: 0;
  font-size: var(--t-3xl);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.tz-tourhero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  margin: var(--s3) 0 0;
  color: var(--ink-2);
  font-size: var(--t-s);
}

/* Разделитель фактов — псевдоэлементом следующего факта, а не отдельным
   узлом: при переносе строки точка уезжает вместе со своим фактом и не
   висит в конце предыдущей. */
.tz-tourhero__meta > span + span::before {
  content: "Â·";
  margin-right: var(--s3);
  color: var(--ink-3);
}

.tz-tourhero__key {
  margin: var(--s2) 0 0;
  font-family: var(--font-num);
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-tourhero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

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

   Числа набраны крупнее подписей и одним моноширинным кеглем: ряд читается
   как строка приборной панели, где важен порядок величин, а не проза. */
.tz-vd {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s5);
  margin-top: var(--s5);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--ball-soft);
  border-radius: var(--r-ctl);
  background: var(--ball-wash);
}

/* Не проходишь — полоса гаснет до нейтральной: мячовый фон читается как
   «всё хорошо», и обещать им отказ нельзя. */
.tz-vd--out {
  border-color: var(--rule);
  background: var(--surface);
}

/* Исход отбит слева цветной чертой, а не рамкой: он часть той же строки,
   но отвечает на другой вопрос, чем числа справа. */
.tz-vd__code {
  display: grid;
  gap: 1px;
  padding-left: var(--s3);
  border-left: 3px solid var(--ball-deep);
}

.tz-vd--out .tz-vd__code {
  border-left-color: var(--rule-2);
}

.tz-vd__code b {
  font-size: var(--t-m);
  font-weight: 650;
}

.tz-vd__code span {
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-vd__n {
  display: grid;
  gap: 1px;
}

.tz-vd__n b {
  font-family: var(--font-num);
  font-size: var(--t-l);
  font-weight: 600;
  letter-spacing: -0.02em;
}

.tz-vd__n span {
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}

/* Распорка отжимает кнопку к правому краю; при переносе она схлопывается
   в ноль и кнопка встаёт сразу за последним числом. */
.tz-vd__sp {
  flex: 1 1 auto;
}

/* Полотно спецификации: отбивка от афиши турнира. Собственных отступов
   строкам не даём — ритм держат их разделительные линии. */
.tz-spec {
  margin-top: var(--s6);
}

/* Строка спецификации. Линия снизу, а не рамка вокруг: разделов много, и
   семь рамок подряд читаются как семь разных страниц. */
.tz-spec__row {
  display: grid;
  grid-template-columns: 216px minmax(0, 1fr);
  gap: var(--s5);
  padding: var(--s6) 0;
  border-bottom: 1px solid var(--rule);
}

.tz-spec__row:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

/* Подпись липнет к верху: у длинного раздела (заявочный лист на 140 строк)
   иначе теряется, о чём читаешь. Отступ — высота топбара плюс воздух. */
.tz-spec__label {
  position: sticky;
  top: 72px;
  align-self: start;
}

.tz-spec__label h2 {
  margin: 0;
  font-size: var(--t-l);
  line-height: 1.2;
}

.tz-spec__label p {
  margin: var(--s2) 0 0;
  color: var(--ink-3);
  font-size: var(--t-xs);
  line-height: 1.45;
}

/* Переключатель под подписью (сетка юниоров): это выбор того, о чём весь
   раздел, поэтому стоит у подписи, а не в ряду управления над таблицей. */
.tz-spec__label .tz-seg {
  margin-top: var(--s3);
}

.tz-spec__body {
  min-width: 0;
}

.tz-spec__cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4) var(--s5);
  align-items: start;
}

/* Размеры сеток — панель, а не таблица: строк в ней две, и число мест само по
   себе ничего не решает — «24 места» в основе значит «18 прямой допуск, 6 под
   организатора и квалу». Поэтому в ячейке кроме числа стоит разбор, а панель
   идёт на всю ширину раздела: в половину колонки такой текст не помещался.
   Ячейки стоят в ряд и складываются в колонку, когда ряду тесно. Разделители
   рисует сама сетка (gap на цвете линии) — тогда линия ложится и между
   колонками, и между строками, без правил на :last-child под каждый случай.
   Рамка и шапка повторяют .tz-tablewrap/.tz-table th — стоящие друг под другом
   блоки должны читаться как один материал. */
.tz-draws {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: var(--r-box);
  overflow: hidden;
}

.tz-draws__h {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--ink-2);
}

.tz-draws__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1px;
  background: var(--rule);
}

/* Подпись и число — одной строкой, разбор под ними во всю ширину ячейки:
   число остаётся крупной точкой входа, а текст читается как его расшифровка. */
.tz-draws__cell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--s2) var(--s3);
  padding: var(--s3);
  background: var(--paper);
}

.tz-draws__t {
  margin: 0;
  font-size: var(--t-s);
}

.tz-draws__n {
  margin: 0;
  font-size: var(--t-2xl);
  line-height: 1;
  font-weight: 550;
}

.tz-draws__x {
  grid-column: 1 / -1;
  margin: 0;
  max-width: 46ch;
  font-size: var(--t-xs);
  line-height: 1.45;
  color: var(--ink-2);
}

/* Легенда знака «≈» — одна на панель, а не подпись у каждого числа. */
.tz-draws__foot {
  margin: 0;
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--rule);
  background: var(--surface);
  font-size: var(--t-2xs);
  color: var(--ink-3);
}

.tz-spec__note {
  margin: var(--s3) 0 0;
  color: var(--ink-3);
  font-size: var(--t-xs);
}

/* Список «характеристика → значение»: подпись слева, значение по правому
   краю, чтобы числа стояли столбиком и сравнивались взглядом. */
.tz-deflist {
  margin: 0;
}

.tz-deflist > div {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
}

.tz-deflist > div:last-child {
  border-bottom: 0;
}

.tz-deflist dt {
  color: var(--ink-3);
  font-size: var(--t-s);
}

.tz-deflist dd {
  margin: 0;
  text-align: right;
  font-size: var(--t-s);
  font-weight: 550;
}

/* Широкий вариант — для фактшита организатора. Там значение не число, а проза
   (адрес, расписание, условия брони): по правому краю она бы рвалась, поэтому
   значение встаёт под подписью и занимает всю ширину раздела. */
.tz-deflist--wide > div {
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
}

.tz-deflist--wide dd {
  text-align: left;
  font-weight: 400;
  overflow-wrap: anywhere;
}

/* Календарь турнира — лента засечек в порядке наступления. Сетка, а не
   список: сроки сравнивают друг с другом («снятие раньше или позже
   квалификации»), и колонками это делается взглядом. Промежутки в 1px на
   общем фоне-линейке дают сплошную решётку без двойных границ. */
.tz-dates {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  margin: 0;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-ctl);
  overflow: hidden;
}

.tz-dates > div {
  display: grid;
  gap: 2px;
  padding: var(--s3);
  background: var(--paper);
}

.tz-dates dt {
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
}

.tz-dates dd {
  margin: 0;
  font-family: var(--font-num);
  font-size: var(--t-s);
  font-weight: 600;
}

.tz-dates dd small {
  font-family: var(--font-ui);
  font-weight: 400;
  color: var(--ink-3);
}

/* Прошедший срок гаснет, ближайший подсвечивается: из пяти дат в работе
   всегда ровно одна, и глаз должен находить её без чтения. */
.tz-dates > div.is-past {
  background: var(--surface);
}

.tz-dates > div.is-past dd {
  color: var(--ink-3);
}

.tz-dates > div.is-next {
  background: var(--ball-wash);
}

/* Люди и сервисы карточками. Списком «подпись → значение» это не читалось:
   контакт роли и цена услуги короткие и самостоятельные, их проглядывают, а
   не читают строку за строкой. auto-fill, а не auto-fit: две карточки на
   широком экране должны остаться двумя, а не растянуться на всю ширину. */
.tz-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--s3);
}

.tz-card {
  display: grid;
  align-content: start;
  gap: 3px;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r-ctl);
  background: var(--paper);
}

.tz-card__role {
  margin: 0;
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
}

.tz-card__name {
  margin: 0;
  font-size: var(--t-s);
  font-weight: 600;
}

/* Почта и адрес приходят одной строкой без пробелов — рвём их по любому
   символу, иначе карточка растягивает всю решётку под свою ширину. */
.tz-card__line {
  margin: 0;
  font-size: var(--t-xs);
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* Свободный текст организатора об отеле («Additional Information»). У
   большинства это одна строка, у отдельных — полтора экрана, где повторён весь
   прайс, а в конце дописан второй отель. Длинный сворачиваем: текст нужен —
   по нему бронируют, — но разворачивать его всем не за что. */
.tz-fold {
  border-top: 1px solid var(--rule);
  margin-top: var(--s3);
}

.tz-fold > .tz-summary {
  padding: var(--s3) 0;
  background: none;
}

.tz-fold__body {
  padding-bottom: var(--s3);
}

/* Разбор заметок организатора (scripts/tz_hotel_ai.py): код брони, ссылка на
   неё, доплаты, которых нет в прайсе. Стоит внутри карточки отеля, но в своей
   рамке и с меткой: организатор за эти строки не отвечал, они собраны машиной
   из его текста, и первоисточник лежит тут же ниже под раскрытием. */
.tz-hai {
  margin-top: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--rule);
  border-radius: var(--r-ctl);
  background: var(--surface);
}

.tz-hai__head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0 0 var(--s2);
  color: var(--ink-2);
  font-size: var(--t-xs);
  font-weight: 550;
}

.tz-hai__code,
.tz-hai__price {
  font-family: var(--font-num);
}

/* Код брони — то единственное, что из этого блока переписывают руками в письмо
   отелю. Мячом, чтобы находился взглядом без перечитывания абзаца. */
.tz-hai__code {
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--ball);
  letter-spacing: 0.04em;
}

.tz-hai__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

@media (max-width: 1024px) {
  /* Две колонки внутри раздела на планшете схлопываются: список
     характеристик в половину ширины ужимается до переносов в каждой строке. */
  .tz-spec__cols {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 860px) {
  .tz-tourhero {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  /* Подпись раздела встаёт над содержимым и перестаёт липнуть: на телефоне
     она съела бы треть экрана, который и так листают. */
  .tz-spec__row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s3);
    padding: var(--s5) 0;
  }

  .tz-spec__label {
    position: static;
  }
}

/* --- 28. Профиль: «кто вы» одной формой ---------------------------------- */
/* Аватар, ФИО, пол, год и страна выезда — одна форма с одной кнопкой. Раньше
   тут стояли три формы подряд, каждая со своим «Сохранить»: человек правит
   «себя» один раз, а нажимал трижды и трижды перезагружал страницу. */
.tz-ident {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
}

/* Поля внутри — своей сеткой: ФИО и страна занимают всю ширину (.span-all),
   пол и год встают парой. Ниже 560px пара распадается сама. */
.tz-ident__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s4);
  align-content: start;
}

@media (max-width: 700px) {
  .tz-ident {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
  }

  .tz-ident__fields {
    width: 100%;
  }
}

/* Сама картинка и есть кнопка: клик по ней открывает выбор файла, файл можно
   бросить на неё мышью. Настоящий input[type=file] остаётся в разметке, но
   спрятан визуально — системная кнопка «Выберите файл» была единственным
   нерисованным элементом на экране, а клавиатуре и скринридеру нужно
   нормальное поле с подписью, а не картинка с обработчиком. */
.tz-ava {
  display: grid;
  justify-items: center;
  gap: var(--s2);
  width: 196px;
}

.tz-ava__frame {
  position: relative;
  width: 104px;
  height: 104px;
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: border-color var(--d-fast), background var(--d-fast);
}

.tz-ava__frame:hover {
  border-color: var(--rule-ctl);
}

/* Обводка фокуса — на рамке, а не на спрятанном поле: иначе при табуляции
   фокус «пропадал» бы в никуда. */
.tz-ava__frame:focus-within {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.tz-ava__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Инициалы — тот же мяч, что в рельсе: пустой аватар обязан читаться как
   осознанное состояние, а не как «картинка не загрузилась». */
.tz-ava__ini {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  font-size: var(--t-2xl);
  font-weight: 600;
  color: var(--ink);
  background: var(--ball);
}

/* Вуаль поверх картинки. Всегда видна снизу узкой полосой (иначе про
   кликабельность аватара никто не догадается), на hover заливает круг. */
.tz-ava__veil {
  position: absolute;
  inset: auto 0 0 0;
  display: grid;
  place-items: center;
  gap: 2px;
  padding: 5px 4px 6px;
  background: rgba(22, 23, 15, 0.62);
  color: #fff;
  font-size: var(--t-2xs);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  transition: inset var(--d-fast), background var(--d-fast);
}

.tz-ava__veil svg {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.7;
  display: none;
}

.tz-ava__frame:hover .tz-ava__veil,
.tz-ava__frame:focus-within .tz-ava__veil {
  inset: 0;
  align-content: center;
  background: rgba(22, 23, 15, 0.55);
}

.tz-ava__frame:hover .tz-ava__veil svg,
.tz-ava__frame:focus-within .tz-ava__veil svg {
  display: block;
}

/* Перетаскивание. Пунктир и подложка «мяча» — тот же язык, что у .tz-empty:
   пунктирная рамка в системе означает «сюда что-то положат». */
.tz-ava[data-state="drag"] .tz-ava__frame {
  border: 2px dashed var(--ball-deep);
  background: var(--ball-wash);
}

.tz-ava[data-state="drag"] .tz-ava__veil {
  inset: 0;
  align-content: center;
  background: rgba(22, 23, 15, 0.35);
}

.tz-ava[data-state="err"] .tz-ava__frame {
  border-color: var(--err);
}

.tz-ava__acts {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
  justify-content: center;
}

.tz-ava__state {
  margin: 0;
  text-align: center;
  font-size: var(--t-xs);
  color: var(--ink-2);
}

.tz-ava__state b {
  font-weight: 600;
  color: var(--ink);
}

/* Метка происхождения значения. У каждого поля два источника: карточка ITF
   (или профиль входа) и правка руками. Без метки пустой «Год рождения» при
   заполненном годе в шапке читается как поломка, и человек вписывает год
   руками, плодя ещё одну правку. Метка стоит в строке с подписью поля,
   поэтому label у таких полей становится flex-строкой. */
.tz-field--src > label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  justify-content: space-between;
}

.tz-auto {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--t-3xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  padding: 1px 8px;
}

/* Правка руками — акцентом: это единственное на экране, за что отвечает
   пользователь, а не парсер. */
.tz-auto--mine {
  color: var(--ball-ink);
  background: var(--ball-wash);
  border-color: var(--ball-soft);
}

/* Кнопка «вернуть автоматическое» живёт внутри метки: возврат — это не
   отдельное действие формы, а отмена конкретной правки. Она просто чистит
   поле, а пустое поле сервер уже сегодня понимает как «взять из карточки». */
.tz-auto button {
  border: 0;
  background: none;
  padding: 0 0 0 2px;
  margin: 0;
  cursor: pointer;
  color: inherit;
  display: grid;
  place-items: center;
}

.tz-auto button svg {
  width: 11px;
  height: 11px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2.2;
}

.tz-auto button:hover {
  color: var(--err);
}

/* Сегменты на радиокнопках: в системе .tz-seg собран из ссылок и кнопок, здесь
   нужна форма. Вид один в один — правила повторяют геометрию для нового тега. */
.tz-seg--pick label {
  position: relative;
  padding: 5px 11px;
  font-size: var(--t-xs);
  font-weight: 550;
  color: var(--ink-2);
  cursor: pointer;
  border-right: 1px solid var(--rule);
  white-space: nowrap;
}

.tz-seg--pick label:last-child {
  border-right: 0;
}

.tz-seg--pick input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.tz-seg--pick label:hover {
  background: var(--surface);
}

.tz-seg--pick label:has(input:checked) {
  background: var(--ink);
  color: var(--paper);
}

.tz-seg--pick label:has(input:focus-visible) {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* Подвал формы: кнопка и состояние правки в одной строке, отбитой линейкой.
   «Сохранить» обязан быть виден вместе с ответом на вопрос «а есть ли что
   сохранять». */
.tz-formbar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  border-top: 1px solid var(--rule);
  margin-top: var(--s5);
  padding-top: var(--s4);
}

/* Внутри .tz-ident подвал — такой же ряд сетки, и собственный отступ сложился
   бы с её gap в двойную дыру. */
.tz-ident > .tz-formbar {
  margin-top: 0;
}

.tz-formbar__state {
  margin: 0;
  font-size: var(--t-xs);
  color: var(--ink-3);
}

/* Пока правок нет — кнопка погашена, надпись говорит почему. Только со
   скриптом: он же и снимает блокировку по первому вводу, а без него правку
   стало бы не отправить вовсе. */
.has-js [data-dirty="false"] .tz-formbar .tz-btn--primary {
  opacity: 0.45;
  pointer-events: none;
}

[data-dirty="true"] .tz-formbar__state {
  color: var(--warn);
  font-weight: 550;
}

/* Строка способа входа: монограмма, название с деталью, справа состояние или
   действие. Рядом из .tz-row с абзацем-подписью почта Google визуально
   прилипала к следующей строке, а не к своей. */
.tz-prov {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
}

.tz-prov:last-of-type {
  border-bottom: 0;
}

/* Монограмма вместо фирменного значка: рисовать чужие логотипы в одну краску
   — врать про бренд, а тащить их цветными в монохромную систему нечем.
   Буква в квадрате читается однозначно и ничего не обещает. */
.tz-prov__ico {
  width: 30px;
  height: 30px;
  flex: none;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
  border-radius: var(--r-ctl);
  background: var(--surface);
  font-size: var(--t-s);
  font-weight: 650;
  color: var(--ink-2);
}

.tz-prov__ico svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.7;
}

.tz-prov__name {
  margin: 0;
  font-size: var(--t-s);
  font-weight: 550;
}

.tz-prov__sub {
  margin: 0;
  font-size: var(--t-xs);
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Выход уходит вниз панели и отбивается линейкой: он не должен стоять в одном
   ряду со списком привязок. */
.tz-prov__foot {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  border-top: 1px solid var(--rule);
  margin-top: var(--s4);
  padding-top: var(--s4);
}

/* Карточка ITF внутри панели. */
.tz-idcard {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
}

.tz-idcard__name {
  margin: 0;
  font-size: var(--t-m);
  font-weight: 600;
}

.tz-idcard__meta {
  margin: 2px 0 0;
  font-size: var(--t-xs);
  color: var(--ink-3);
}
