/* Компоненты EPOCH.
   ========================================================================
   Всё живёт в слое components — значит, утилиты из tokens.css перебивают
   их без !important и без гонки специфичности.

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

@layer components {

/* ------------------------------------------------------------- кнопки */

.btn{
  display:inline-flex; align-items:center; justify-content:center;
  gap:var(--sp-2);
  padding:10px var(--sp-5);
  min-height:40px;                      /* палец попадает */
  border:1px solid transparent;
  border-radius:var(--r-md);
  font-family:var(--font-body);
  font-size:var(--fs-sm); font-weight:600;
  line-height:1; white-space:nowrap; cursor:pointer;
  transition:background var(--dur-1) var(--ease-out),
             border-color var(--dur-1) var(--ease-out),
             color var(--dur-1) var(--ease-out);
}
.btn > svg{ width:18px; height:18px; flex:none;
            stroke:currentColor; fill:none; stroke-width:1.8 }

.btn--primary{ background:var(--accent); color:var(--text-on-accent) }
.btn--primary:hover{ background:var(--accent-hover) }
.btn--primary:active{ background:var(--accent); transform:translateY(1px) }

.btn--secondary{ background:var(--bg-elevated); color:var(--text);
                 border-color:var(--stroke) }
.btn--secondary:hover{ border-color:var(--stroke-strong) }

.btn--ghost{ background:transparent; color:var(--text-muted);
             border-color:var(--stroke) }
.btn--ghost:hover{ color:var(--text); border-color:var(--stroke-strong) }

.btn--danger{ background:var(--danger-soft); color:var(--danger);
              border-color:var(--danger-line) }
.btn--danger:hover{ background:var(--danger-line) }

.btn--sm{ min-height:32px; padding:6px var(--sp-3); font-size:var(--fs-xs) }
.btn--lg{ min-height:48px; padding:14px var(--sp-6); font-size:var(--fs-md) }
.btn--block{ width:100% }

/* Отключено — два разных случая, и разводить их приходится.
   У настоящей <button disabled> нажатия блокирует сам браузер, поэтому
   pointer-events ей не нужен — и не нужен как раз потому, что он гасит
   курсор: с ним `cursor:not-allowed` не показывается вовсе, элемент
   перестаёт принимать события и берёт курсор родителя.
   А `aria-disabled` браузеру ничего не говорит, там события гасить надо. */
.btn[disabled]{ opacity:.45; cursor:not-allowed }
.btn[aria-disabled="true"]{ opacity:.45; cursor:not-allowed;
                            pointer-events:none }

/* Загрузка: кнопка не меняет ширину, иначе макет прыгает под курсором.
   Та же метка у кнопки-иконки — она тоже умеет ждать, и заводить ей
   вторую такую же значило бы позволить им разойтись. */
.btn[data-loading], .icon-btn[data-loading]{
  color:transparent; pointer-events:none; position:relative;
}
.btn[data-loading]::after,
.icon-btn[data-loading]::after{
  content:""; position:absolute; width:16px; height:16px;
  border:2px solid currentColor; border-top-color:transparent;
  border-radius:50%; animation:spin .7s linear infinite;
  color:var(--text-on-accent);
}
.btn--ghost[data-loading]::after,
.btn--secondary[data-loading]::after,
.icon-btn[data-loading]::after{ color:var(--text) }
@keyframes spin{ to{ transform:rotate(360deg) } }

.icon-btn{
  display:inline-grid; place-items:center; width:40px; height:40px;
  border:1px solid var(--stroke); border-radius:var(--r-md);
  background:var(--bg-panel); color:var(--text-muted); cursor:pointer;
  transition:color var(--dur-1) var(--ease-out),
             border-color var(--dur-1) var(--ease-out);
}
.icon-btn:hover{ color:var(--text); border-color:var(--stroke-strong) }
.icon-btn[aria-pressed="true"]{ color:var(--accent-ink);
  border-color:var(--accent-line); background:var(--accent-soft) }
.icon-btn > svg{ width:18px; height:18px; stroke:currentColor; fill:none;
                 stroke-width:1.8 }

/* -------------------------------------------------------------- поля */

.field{ display:grid; gap:var(--sp-2) }
.field > label{ font-size:var(--fs-sm); font-weight:500; color:var(--text) }
.field .hint{ font-size:var(--fs-xs); color:var(--text-faint) }
.field .err{ font-size:var(--fs-xs); color:var(--danger) }

.input{
  width:100%; min-height:42px;
  padding:10px var(--sp-3);
  background:var(--bg-sunken);
  border:1px solid var(--stroke); border-radius:var(--r-md);
  color:var(--text); font:inherit; font-size:var(--fs-field);
  transition:border-color var(--dur-1) var(--ease-out);
}
.input::placeholder{ color:var(--text-faint) }
.input:hover{ border-color:var(--stroke-strong) }
/* Рамка подсвечивается в дополнение к общему кольцу, а не вместо
   него. Прежде здесь стоял outline:none — поле оставалось единственным
   элементом без кольца, да ещё и на :focus, то есть подсвечивалось от
   мыши, ровно от чего общее правило уходило. */
.input:focus-visible{ border-color:var(--accent-ink) }
.input[aria-invalid="true"]{ border-color:var(--danger) }
.input[disabled]{ opacity:.5; cursor:not-allowed }
select.input{ cursor:pointer;
  /* стрелка своя: системная в тёмной теме выглядит инородно */
  appearance:none; padding-right:var(--sp-7);
  background-image:linear-gradient(45deg,transparent 50%,var(--text-faint) 50%),
                   linear-gradient(135deg,var(--text-faint) 50%,transparent 50%);
  background-position:calc(100% - 18px) 19px, calc(100% - 13px) 19px;
  background-size:5px 5px, 5px 5px; background-repeat:no-repeat }

.input-group{ position:relative; display:flex; align-items:center }
.input-group > svg{ position:absolute; left:var(--sp-3); width:16px; height:16px;
  stroke:var(--text-faint); fill:none; stroke-width:1.8; pointer-events:none }
.input-group .input{ padding-left:var(--sp-8) }
.input-group .in-action{ position:absolute; right:var(--sp-2) }

.check{
  display:flex; align-items:center; gap:var(--sp-2);
  font-size:var(--fs-sm); color:var(--text-muted);
  cursor:pointer; padding:var(--sp-1) 0; min-height:32px;
}
.check:hover{ color:var(--text) }
.check input{ width:16px; height:16px; accent-color:var(--accent-ink); flex:none }

/* -------------------------------------------------------- поверхности */

.panel{
  background:var(--bg-panel);
  border:1px solid var(--stroke);
  border-radius:var(--r-lg);
}
.panel__pad{ padding:var(--sp-5) }
.panel__head{
  padding:var(--sp-4) var(--sp-5);
  border-bottom:1px solid var(--stroke);
  display:flex; align-items:center; gap:var(--sp-3);
}
.panel__head h3{ flex:1 }

/* --------------------------------------------------------- индикаторы */

.badge{
  display:inline-flex; align-items:center; gap:6px;
  padding:4px var(--sp-2);
  border:1px solid var(--stroke); border-radius:var(--r-sm);
  background:var(--bg-elevated); color:var(--text-muted);
  font-size:var(--fs-xs); font-weight:600;
  letter-spacing:var(--tracking-caps); text-transform:uppercase;
  white-space:nowrap;
}
.badge__dot{ width:6px; height:6px; border-radius:50%; background:currentColor;
             flex:none }
.badge--online{ color:var(--online); background:var(--online-soft);
                border-color:var(--online-line) }
.badge--offline{ color:var(--danger); background:var(--danger-soft);
                 border-color:var(--danger-line) }
.badge--full{ color:var(--accent-ink); background:var(--accent-soft);
              border-color:var(--accent-line) }
/* «Не знаем» — полноправное состояние, а не ошибка. Штриховка отличает его
   от офлайна: офлайн — это факт, unknown — отсутствие факта. */
.badge--unknown{
  color:var(--unknown); border-color:var(--stroke);
  background:repeating-linear-gradient(-45deg,
    var(--unknown-soft) 0 4px, transparent 4px 8px);
}
.badge--online .badge__dot{ animation:blip 2.4s var(--ease-in-out) infinite }
@keyframes blip{ 0%,100%{ opacity:1 } 50%{ opacity:.3 } }

/* Полоса заполнения сервера. */
.meter{ height:3px; border-radius:2px; background:var(--stroke);
        overflow:hidden }
.meter > i{ display:block; height:100%; background:var(--online);
            border-radius:2px; transition:width var(--dur-2) var(--ease-out) }
.meter--hot > i{ background:var(--accent) }
.meter--unknown > i{ background:var(--unknown) }

/* След числа: спарклайн за 24 часа. Рисуется как svg, данные из БД. */
.spark{ display:block; width:100%; height:24px; overflow:visible }
.spark path{ fill:none; stroke:var(--online); stroke-width:1.5;
             vector-effect:non-scaling-stroke }
.spark--unknown path{ stroke:var(--unknown) }

/* ------------------------------------------------------------ таблицы */

.table{ width:100%; border-collapse:collapse; font-size:var(--fs-sm) }
.table th{
  text-align:left; padding:var(--sp-3);
  font-size:var(--fs-xs); font-weight:600;
  letter-spacing:var(--tracking-caps); text-transform:uppercase;
  color:var(--text-faint); border-bottom:1px solid var(--stroke);
  white-space:nowrap;
}
.table td{ padding:var(--sp-3); border-bottom:1px solid var(--stroke);
           color:var(--text) }
.table tr:last-child td{ border-bottom:0 }
.table tbody tr:hover{ background:var(--wash-1) }
.table .num{ text-align:right }
/* На узком экране таблица прокручивается внутри себя, а не растягивает
   страницу. */
.table-scroll{ overflow-x:auto; -webkit-overflow-scrolling:touch }

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

.overlay{
  position:fixed; inset:0; z-index:var(--z-overlay);
  background:var(--scrim);
  display:grid; place-items:center; padding:var(--sp-5);
  animation:fade var(--dur-2) var(--ease-out);
}
@keyframes fade{ from{ opacity:0 } }

.modal{
  width:min(440px,100%);
  /* Выше экрана окно быть не может. Без этого длинное содержимое
     вырастало за край: сам оверлей закреплён (position:fixed) и не
     прокручивается, прокручивать было нечего, и нижняя часть окна
     оказывалась недостижимой.

     dvh, а не vh: на телефоне адресная строка съезжает, и vh считается
     по её отсутствию — окно вылезало ровно на её высоту. */
  max-height:calc(100dvh - 2 * var(--sp-5));
  /* Колонка нужна, чтобы шапка и подвал остались на местах, а
     прокручивалось только содержимое. */
  display:flex; flex-direction:column;
  background:var(--bg-elevated);
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-3);
  animation:rise var(--dur-2) var(--ease-out);
}
/* Только прозрачность и небольшой сдвиг самого окна. Трансформации на
   контейнере, внутри которого живёт чужой iframe (капча), Chromium
   использует как повод не доставлять туда нажатия — на это уже наступали. */
@keyframes rise{ from{ opacity:0; transform:translateY(8px) } }
.modal--has-frame{ animation:fade var(--dur-2) var(--ease-out) }

.modal__head{ padding:var(--sp-5); border-bottom:1px solid var(--stroke);
              display:flex; align-items:center; gap:var(--sp-3);
              flex:none }
.modal__head h3{ flex:1 }
/* Прокрутка — здесь, у содержимого, а не у страницы под окном.
   min-height:0 обязателен: без него элемент flex не даёт себя ужать
   ниже содержимого, max-height у окна не срабатывает, и прокрутка не
   появляется вовсе. overscroll-behavior не пускает прокрутку дальше,
   на страницу, когда список докручен до конца. */
.modal__body{ padding:var(--sp-5); display:grid; gap:var(--sp-4);
              flex:1; min-height:0; overflow-y:auto;
              overscroll-behavior:contain }
.modal__foot{ flex:none;
              padding:var(--sp-4) var(--sp-5); border-top:1px solid var(--stroke);
              display:flex; gap:var(--sp-2); justify-content:flex-end;
              background:var(--bg-panel);
              border-radius:0 0 var(--r-lg) var(--r-lg) }

/* -------------------------------------------------------------- тосты */

.toasts{ position:fixed; left:50%; bottom:var(--sp-6);
         transform:translateX(-50%); z-index:var(--z-toast);
         display:grid; gap:var(--sp-2); pointer-events:none;
         width:min(420px,calc(100vw - var(--sp-8))) }
.toast{
  display:flex; align-items:center; gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-4);
  background:var(--bg-elevated); border:1px solid var(--stroke-strong);
  border-radius:var(--r-md); box-shadow:var(--sh-2);
  color:var(--text); font-size:var(--fs-sm); font-weight:500;
  animation:toast-in var(--dur-2) var(--ease-out);
}
@keyframes toast-in{ from{ opacity:0; transform:translateY(10px) } }
.toast--ok{ border-color:var(--online-line) }
.toast--ok .toast__dot{ background:var(--online) }
.toast--err{ border-color:var(--danger-line) }
.toast--err .toast__dot{ background:var(--danger) }
.toast__dot{ width:8px; height:8px; border-radius:50%;
             background:var(--text-faint); flex:none }

/* ---------------------------------------------------------- скелетоны */

.skel{
  background:linear-gradient(90deg,
    var(--bg-panel) 25%, var(--bg-elevated) 37%, var(--bg-panel) 63%);
  background-size:400% 100%;
  animation:shimmer 1.4s linear infinite;
  border-radius:var(--r-md);
}
@keyframes shimmer{ from{ background-position:100% 0 }
                    to{ background-position:-100% 0 } }
.skel--text{ height:12px }
.skel--title{ height:20px; width:60% }
.skel--card{ height:196px; border-radius:var(--r-lg) }

/* ------------------------------------------------------- состояния --- */

.state{
  display:grid; justify-items:center; gap:var(--sp-3);
  padding:var(--sp-9) var(--sp-5); text-align:center;
  background:var(--bg-panel); border:1px solid var(--stroke);
  border-radius:var(--r-lg);
}
.state > svg{ width:32px; height:32px; stroke:var(--text-faint); fill:none;
              stroke-width:1.5 }
.state h3{ color:var(--text) }
.state p{ color:var(--text-muted); font-size:var(--fs-sm); max-width:46ch }
.state--error > svg{ stroke:var(--danger) }
.state--locked > svg{ stroke:var(--accent-ink) }

/* Полоса «данные устарели»: не ошибка, но и не свежая правда. */
.stale-note{
  display:flex; align-items:center; gap:var(--sp-2);
  padding:var(--sp-2) var(--sp-3);
  border:1px solid var(--stroke); border-radius:var(--r-md);
  background:repeating-linear-gradient(-45deg,
    var(--unknown-soft) 0 5px, transparent 5px 10px);
  color:var(--unknown); font-size:var(--fs-xs); font-weight:600;
}

/* --------------------------------------------------------- копирование */

.copy-row{
  display:flex; align-items:center; gap:var(--sp-2);
  padding:10px var(--sp-3);
  background:var(--bg-sunken); border:1px solid var(--stroke);
  border-radius:var(--r-md);
}
.copy-row code{ flex:1; color:var(--accent-ink); overflow-x:auto;
                white-space:nowrap; scrollbar-width:none }
.copy-row code::-webkit-scrollbar{ display:none }

/* ------------------------------------------------------------ вкладки */

.tabs{ display:flex; gap:var(--sp-1); border-bottom:1px solid var(--stroke);
       overflow-x:auto; scrollbar-width:none }
.tabs::-webkit-scrollbar{ display:none }
.tab{
  padding:var(--sp-3) var(--sp-4); white-space:nowrap; cursor:pointer;
  color:var(--text-muted); font-size:var(--fs-sm); font-weight:600;
  border-bottom:2px solid transparent; margin-bottom:-1px;
  transition:color var(--dur-1) var(--ease-out);
}
.tab:hover{ color:var(--text) }
.tab[aria-selected="true"]{ color:var(--accent-ink); border-bottom-color:var(--accent-ink) }
.tab:active{ transform:translateY(1px) }
.tab[disabled]{ opacity:.5; cursor:not-allowed }
.tab[aria-disabled="true"]{ opacity:.5; cursor:not-allowed;
                           pointer-events:none }

/* ====================================================== состояния

   Инвентаризация показала: «нажатие» было описано у двух компонентов из
   семнадцати, «загрузка» — у одного, «отключено» — у двух. При этом JS
   ставит `data-loading` на кнопки в семи местах и `disabled` — в
   дюжине: разметка состояния объявляла, а стилей у них не было.

   Здесь собраны недостающие. Правила одни и те же для всего, что
   нажимается, и заданы одинаково — иначе через месяц у каждого нового
   компонента снова окажется свой набор.

   Три правила на весь проект:

   * НАЖАТИЕ — сдвиг на пиксель вниз. Не тень и не смена цвета: сдвиг
     читается на любом фоне и не спорит с наведением.
   * ОТКЛЮЧЕНО — половинная непрозрачность и курсор «нельзя». Событий не
     принимает: отключённая кнопка, которая всё-таки нажимается, хуже
     отсутствующей.
   * ЗАГРУЗКА — не отключение. Элемент остаётся видимым и на месте,
     содержимое гаснет, поверх крутится метка. Подменять кнопку словом
     «Ждите» нельзя: пропадает то, на что человек только что нажал.
*/

.icon-btn:active{ transform:translateY(1px) }
.icon-btn[disabled]{ opacity:.5; cursor:not-allowed }
.icon-btn[aria-disabled="true"]{ opacity:.5; cursor:not-allowed;
                           pointer-events:none }
.chip:active{ transform:translateY(1px) }
.chip[disabled]{ opacity:.5; cursor:not-allowed }
.chip[aria-disabled="true"]{ opacity:.5; cursor:not-allowed;
                           pointer-events:none }

.check:active{ transform:translateY(1px) }
/* Отключён сам переключатель — гаснет вся строка вместе с подписью:
   подпись без переключателя выглядит как обычный текст. */
.check:has(input[disabled]), .check[aria-disabled="true"]{
  opacity:.5; cursor:not-allowed;
}

/* Поле в ожидании ответа сервера: проверка ника, поиск. Не запираем
   ввод — человек может дописывать, пока идёт проверка предыдущего. */
.input[data-loading]{ background-image:none; opacity:.7 }
.input[readonly]{ color:var(--text-muted); cursor:default }

@media (prefers-reduced-motion:reduce){
  .btn:active, .icon-btn:active, .chip:active,
  .check:active, .tab:active{ transform:none }
  .btn[data-loading]::after,
  .icon-btn[data-loading]::after{ animation-duration:2s }
  /* Метку не убираем совсем: без неё непонятно, идёт ли ещё работа.
     Медленное вращение читается и не мельтешит. */
}

} /* @layer components */

/* Карточка документа: вся плитка — ссылка, поэтому реагировать должна
   целиком, а не только строчка «читать». */
@layer components {
  .doc-card{ height:100%; transition:border-color var(--dur-1) var(--ease-out),
             transform var(--dur-1) var(--ease-out) }
  .doc-card:hover{ border-color:var(--stroke-strong); transform:translateY(-2px) }
  .doc-card__go{ color:var(--accent-ink); font-size:var(--fs-sm); margin-top:auto }
}

/* ======================================================= панель фильтров
   Общая полоса для разделов со списками. Раньше каждый раздел собирал её
   сам из .row и инлайновых стилей — и они разъезжались между разделами. */
@layer components {
  .toolbar{
    display:flex; align-items:center; flex-wrap:wrap;
    gap:var(--sp-3);
    padding:var(--sp-3);
    background:var(--bg-panel);
    border:1px solid var(--stroke);
    border-radius:var(--r-lg);
  }
  .toolbar__search{ flex:1 1 240px; min-width:0 }
  .toolbar__count{
    margin-left:auto; padding-left:var(--sp-2);
    color:var(--text-faint); font-size:var(--fs-sm);
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  @media (max-width:640px){
    .toolbar__count{ margin-left:0; width:100% }
  }

  /* ------------------------------------------------- выпадающий список
     Свой, а не <select>: системный список рисует операционная система, и
     на Windows он выпадает белым прямоугольником посреди тёмной страницы.
     Разметка — кнопка и меню, поведение описано в ui.js. */
  .dd{ position:relative }
  .dd__btn{
    display:inline-flex; align-items:center; gap:var(--sp-2);
    padding:var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
    background:var(--bg-sunken); color:var(--text);
    border:1px solid var(--stroke); border-radius:var(--r-md);
    font:inherit; font-size:var(--fs-sm); cursor:pointer; white-space:nowrap;
  }
  .dd__btn:hover{ border-color:var(--stroke-strong) }
  .dd__btn svg{ width:14px; height:14px; stroke:currentColor; fill:none;
                stroke-width:2; transition:transform var(--dur-1) var(--ease-out) }
  .dd[data-open] .dd__btn{ border-color:var(--accent-line) }
  .dd[data-open] .dd__btn svg{ transform:rotate(180deg) }

  .dd__menu{
    position:absolute; top:calc(100% + 6px); left:0; z-index:var(--z-overlay);
    min-width:100%; padding:var(--sp-1);
    background:var(--bg-elevated);
    border:1px solid var(--stroke-strong); border-radius:var(--r-md);
    box-shadow:var(--sh-2);
    display:grid; gap:2px;
    animation:dd-in var(--dur-1) var(--ease-out) both;
  }
  @keyframes dd-in{ from{ opacity:0; transform:translateY(-4px) } }
  .dd__item{
    display:flex; align-items:center; gap:var(--sp-2);
    padding:var(--sp-2) var(--sp-3); border-radius:var(--r-sm);
    color:var(--text-muted); font-size:var(--fs-sm);
    background:none; border:0; width:100%; text-align:left; cursor:pointer;
    white-space:nowrap;
  }
  .dd__item:hover{ background:var(--bg-sunken); color:var(--text) }
  .dd__item[aria-selected="true"]{ color:var(--accent-ink) }

  /* ============================================== карточка сервера
     Раньше вся карточка была собрана инлайновыми стилями прямо в скрипте:
     править её приходилось в разметке, и от раздела к разделу она немного
     отличалась. Теперь это компонент. */
  .srv{ overflow:hidden; display:flex; flex-direction:column; height:100% }
  .srv__art{
    position:relative; aspect-ratio:16/9; background:var(--bg-sunken);
    overflow:hidden;
  }
  .srv__art img{
    width:100%; height:100%; object-fit:cover; opacity:.85;
    transition:transform var(--dur-3) var(--ease-out);
  }
  .srv:hover .srv__art img{ transform:scale(1.04) }
  .srv__art::after{
    content:""; position:absolute; inset:0;
    background:linear-gradient(180deg, var(--art-fade-1) 30%,
               var(--art-fade-2) 74%, var(--bg-panel));
  }
  .srv__tags{
    position:absolute; top:var(--sp-3); left:var(--sp-3); right:var(--sp-3);
    z-index:1; display:flex; gap:var(--sp-2); flex-wrap:wrap;
  }
  .srv__epoch{ margin-left:auto }
  .srv__cap{
    position:absolute; left:var(--sp-4); right:var(--sp-4);
    bottom:var(--sp-3); z-index:1;
  }
  .srv__map{ display:flex; align-items:baseline; gap:var(--sp-2);
             margin-top:2px; font-size:var(--fs-sm) }
  .srv__body{ display:grid; gap:var(--sp-2); padding:var(--sp-4);
              margin-top:auto }
  .srv__nums{ display:flex; align-items:baseline; gap:var(--sp-2) }
  .srv__ping{ margin-left:auto }
  .srv__acts{ display:flex; align-items:center; gap:var(--sp-2);
              margin-top:var(--sp-2) }
  .srv__acts .icon-btn{ margin-left:auto }
}

@layer components {
  /* Знак эпохи в карточке на главной.

     Задаём высоту, а не ширину, и вписываем в коробку. Знаки разных эпох
     нарисованы в разных пропорциях: 2015 почти квадратный, 2023 широкий.
     При одинаковой ширине они выходили разной высоты и разного веса — это
     и читалось как «логотипы неровные». */
  .doc-card__logo{
    height:96px; width:100%; object-fit:contain; object-position:left center;
    margin-bottom:var(--sp-2);
  }
}

/* ============================================ полоса выбора и группы эпох
   Эпоха — главное деление списка серверов, а не ещё один фильтр: у 2015 и
   2023 разные клиенты, и человек сначала выбирает, во что он вообще может
   играть. Поэтому полоса эпох стоит над панелью фильтров, а не внутри неё. */
@layer components {
  .chips{ display:flex; flex-wrap:wrap; gap:var(--sp-2) }
  .chip{
    display:inline-flex; align-items:center; gap:var(--sp-2);
    padding:var(--sp-2) var(--sp-4);
    background:var(--bg-panel); color:var(--text-muted);
    border:1px solid var(--stroke); border-radius:var(--r-pill);
    font:inherit; font-size:var(--fs-sm); cursor:pointer;
    transition:border-color var(--dur-1) var(--ease-out),
               color var(--dur-1) var(--ease-out),
               background var(--dur-1) var(--ease-out);
  }
  .chip:hover{ border-color:var(--stroke-strong); color:var(--text) }
  .chip[aria-selected="true"]{
    color:var(--text); border-color:var(--accent-line);
    background:var(--accent-soft);
  }
  /* Знак эпохи в переключателе — по высоте: знаки нарисованы в разных
     пропорциях, и по ширине они выровнялись бы только по бумаге. */
  .chip__logo{ height:22px; width:34px; object-fit:contain; flex:none }
  .chip__n{
    font-family:var(--font-data); font-size:var(--fs-xs);
    color:var(--text-faint); font-variant-numeric:tabular-nums;
  }
  .chip[aria-selected="true"] .chip__n{ color:var(--accent-ink) }

  .egroup{ display:grid; gap:var(--sp-4) }
  .egroup__head{
    display:flex; align-items:center; gap:var(--sp-4); flex-wrap:wrap;
    padding-bottom:var(--sp-3);
    border-bottom:1px solid var(--stroke);
  }
  .egroup__logo{ height:52px; width:80px; object-fit:contain; flex:none }
  .egroup__text{ display:grid; gap:2px }
  .egroup__text h2{ font-size:var(--fs-lg) }
  .egroup__count{
    margin-left:auto; color:var(--text-faint); font-size:var(--fs-sm);
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  @media (max-width:640px){
    .egroup__count{ margin-left:0 }
    .egroup__logo{ height:40px; width:60px }
  }
}

@layer components {
  /* Согласие на входе. Кнопка рядом выключена, пока галочки нет, —
     и это видно, а не только проверяется на сервере. */
  .consent{ align-items:flex-start; gap:var(--sp-3); line-height:1.5 }
  .consent input{ margin-top:3px; flex:none }
  .consent a{ color:var(--accent-ink); text-decoration:underline }
}

@layer components {
  /* Строка фактов о сборке на странице загрузки. */
  .dl__facts{ display:flex; flex-wrap:wrap; gap:var(--sp-7) }
  .dl__facts > div{ display:grid; gap:2px }
  .dl__facts .k{ font-size:var(--fs-xs); color:var(--text-faint);
                 letter-spacing:var(--tracking-caps); text-transform:uppercase }
  .dl__facts .v{ font-family:var(--font-data); color:var(--text);
                 font-size:var(--fs-lg) }
}

@layer components {
  /* Поле для кода из лаунчера: моноширинное и разрежённое — так человек
     видит каждый знак отдельно и не теряется, диктуя его вслух. */
  .code-input{
    font-family:var(--font-data); font-size:var(--fs-xl);
    letter-spacing:.28em; text-align:center; text-transform:uppercase;
    /* Разрядка добавляет пробел и после последнего знака, поэтому при
       выравнивании по центру строка съезжает влево ровно на него.
       Компенсируем отступом такой же ширины. */
    text-indent:.28em;
  }
}

/* ======================================================= узкие экраны
   Полосы и сводки собирались из flex с большими промежутками — на широком
   экране это ряд, а на телефоне то же самое разваливается в столбец, где
   между строками полсотни точек пустоты. Ниже — правила для узких. */
@layer components {
  @media (max-width:720px){
    /* Переключатели в строку с прокруткой, а не в четыре ряда: четыре ряда
       занимают треть экрана до того, как человек увидит хоть один режим. */
    .chips{
      flex-wrap:nowrap; overflow-x:auto; scroll-snap-type:x proximity;
      /* Полосу прокрутки прячем: она здесь только мешает, а то, что список
         продолжается, видно по обрезанной таблетке у края. */
      scrollbar-width:none;
      /* Вылезать в поля страницы отрицательными полями нельзя: строка
         становится шире контейнера, и вбок едет вся страница — проверено.
         Пусть таблетки начинаются от края текста, как и всё остальное. */
      padding-bottom:2px;
    }
    .chips::-webkit-scrollbar{ display:none }
    .chip{ scroll-snap-align:start; flex:0 0 auto }

    /* Панель фильтров: поиск на всю ширину, остальное следом. */
    .toolbar{ gap:var(--sp-2) }
    .toolbar__search{ flex:1 1 100% }

    /* Заголовок группы эпохи: знак и текст в строку, счётчик под ними. */
    .egroup__head{ gap:var(--sp-3) }
    .egroup__head .btn{ order:3 }
  }
}

/* ================================================================ отделка

   Слой поверх готовых компонентов: движение, отклик на наведение и
   немного воздуха. Отдельным блоком, а не правками по всему файлу, —
   чтобы было видно, что именно добавлено ради ощущения, и чтобы это можно
   было снять одним куском, если начнёт мешать.

   Всё движение подчинено одному правилу: оно должно объяснять, а не
   развлекать. Карточка приподнимается — значит её можно нажать. Свечение
   идёт цветом режима — значит цвет что-то значит. Точка пульсирует —
   значит данные живые.

   Уважение к настройке «меньше движения» тут не отдельная любезность:
   ниже единый блок, который гасит всё разом. */

@layer components {

/* ------------------------------------------------- главная кнопка сайта

   «Войти через Steam» и «Играть» должны читаться как главное действие на
   экране. Тень цветом самой кнопки, а не чёрная: чёрная прижимает к фону,
   цветная поднимает над ним. */

.btn--primary{
  box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent);
  transition:background var(--dur-1) var(--ease-out),
             box-shadow var(--dur-2) var(--ease-out),
             transform var(--dur-2) var(--ease-out);
}
.btn--primary:hover{
  transform:translateY(-1px);
  box-shadow:0 6px 24px -6px color-mix(in srgb, var(--accent) 55%,
                                       transparent);
}
.btn--primary:active{ transform:translateY(0) }

.btn--lg{ padding:14px var(--sp-6); min-height:48px; font-size:var(--fs-md) }

/* ------------------------------------------------- карточки, на которые жмут

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

/* Карточка режима. Единственное её определение на проект: раньше их
   было два — здесь и в modes.css, — и выигрывал тот, чей файл грузился
   последним. На страницах режимов это была версия из modes.css, на
   остальных — эта, и один компонент выглядел по-разному в зависимости
   от того, откуда на него смотрят.

   Переход не объявляем: карточка помечается классом .reactive (свет по
   курсору), а у него свой переход на те же свойства и он всё равно
   выигрывает по порядку. Второе объявление здесь только создавало бы
   вид, что оно на что-то влияет. */
.mcard{
  display:flex; flex-direction:column;
  height:100%; overflow:hidden;
}
.mcard:hover{
  /* Только рамка. Ни сдвига, ни тени здесь нет намеренно: и то и другое
     задаёт механизм наклона по курсору (.tilt) — сдвиг через transform,
     тень через --sx/--sy, и она следует за курсором. Своя статическая
     тень при наведении гасила бы живую. */
  border-color:color-mix(in srgb, var(--mc, var(--accent)) 45%,
                         var(--stroke));
}
/* Подложка карточки оживает без filter.
   ========================================================================
   Здесь был `transition:filter` с saturate и brightness. Это выглядело
   правильно и работало плохо: filter заставляет браузер перерисовывать
   весь слой на каждом кадре, а под ним — два градиента во всю карточку.
   На сетке из пятнадцати карточек это и есть та самая дёрганость.

   Тот же эффект даёт наложенный слой, у которого меняется только
   прозрачность: прозрачность браузер умеет менять на видеокарте, ничего
   не перерисовывая. */
.mcard__art{ position:relative; isolation:isolate }
.mcard__art::before{
  content:""; position:absolute; inset:0; z-index:1;
  background:radial-gradient(70% 90% at 30% 30%,
             color-mix(in srgb, var(--mc, var(--accent)) 22%, transparent),
             transparent 70%);
  opacity:0;
  transition:opacity var(--dur-3) var(--ease-out);
  pointer-events:none;
}
.mcard:hover .mcard__art::before{ opacity:1 }
.mcard__icon{ transition:transform var(--dur-2) var(--ease-out),
                         opacity var(--dur-2) var(--ease-out) }
.mcard:hover .mcard__icon{ transform:translateY(-50%) scale(1.08) }

/* Карточка эпохи: картинка внутри чуть наезжает, стрелка уходит вправо. */
.epoch{ transition:transform var(--dur-2) var(--ease-out),
                   border-color var(--dur-2) var(--ease-out),
                   box-shadow var(--dur-2) var(--ease-out) }
.epoch:hover{
  transform:translateY(-2px);
  box-shadow:0 16px 40px -22px var(--shadow-deep);
}
.epoch img{
  transition:transform var(--dur-3) var(--ease-out);
  will-change:transform;
  /* Масштаб картинки — это композиция, а не перерисовка, но только если
     браузер знает об этом заранее. Без backface-visibility он на части
     машин пересобирает растр и картинка дёргается на первом кадре. */
  backface-visibility:hidden;
}
.epoch:hover img{ transform:scale(1.05) }
.epoch .arrow, .epoch__more{
  display:inline-block;
  transition:transform var(--dur-2) var(--ease-out);
}
.epoch:hover .arrow, .epoch:hover .epoch__more{ transform:translateX(4px) }

/* Карта эпохи — тот же приём, что у карточек режимов. */
.emap{ transition:transform var(--dur-2) var(--ease-out),
                  border-color var(--dur-2) var(--ease-out) }
.emap:hover{ transform:translateY(-2px); border-color:var(--stroke-strong) }
.emap__img{
  transition:transform var(--dur-3) var(--ease-out);
  will-change:transform; backface-visibility:hidden;
}
.emap:hover .emap__img{ transform:scale(1.04) }
/* Обрезка масштабированной картинки — своя поверхность, иначе браузер
   пересчитывает обрезку по всей сетке карточек. */
.emap{ contain:paint }

/* -------------------------------------------------- дыхание первого экрана

   Свечение медленно расходится и сходится. Двенадцать секунд — это
   заметно телом, но не глазом: экран перестаёт быть картинкой, при этом
   ничто не дёргается на периферии зрения, пока человек читает. */

@keyframes hero-breathe{
  0%,100%{ opacity:.82 }
  50%    { opacity:1 }
}
.hero__glow{
  /* Дышит только прозрачностью. Масштаб отсюда убран намеренно: слой
     занимает весь первый экран и залит радиальными градиентами, и каждый
     кадр масштабирования — это полная его перерисовка. Прозрачность
     меняется на видеокарте и не стоит почти ничего.

     transform:translateZ(0) поднимает слой на отдельную поверхность, и
     тогда его перерисовка не тянет за собой остальную страницу. */
  animation:hero-breathe 12s var(--ease-in-out) infinite;
  will-change:opacity;
  transform:translateZ(0);
  contain:paint;
}

/* ------------------------------------------------------ плавные картинки

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

.has-fade img[data-fade]{ opacity:0 }
.has-fade img[data-fade].is-loaded{
  opacity:1;
  transition:opacity .5s var(--ease-out);
}

/* ------------------------------------------------------------ типографика

   Заголовкам — плотнее межбуквенное, тексту — просторнее межстрочное.
   Контраст между ними и есть половина ощущения «вылизанности»: когда всё
   набрано одинаково, страница выглядит как документ, а не как продукт. */

.page h1, .hero h1{ letter-spacing:-.02em; line-height:1.05 }
.page h2{ letter-spacing:-.01em; line-height:1.15 }
.page p, .panel p{ line-height:1.6 }
.muted{ line-height:1.6 }

/* Больше воздуха между разделами страницы. Отступ задаётся контейнером,
   а не заголовками: иначе каждый новый раздел приносит свой и они
   расходятся. */
.page.stack-lg > * + *{ margin-top:clamp(var(--sp-7), 6vw, var(--sp-9)) }

/* ------------------------------------------------------------- меньше движения

   Один блок на весь слой. Человек, попросивший систему не двигать
   картинку, не должен вылавливать исключения по одному. */

@media (prefers-reduced-motion:reduce){
  .hero__glow{ animation:none }
  .mcard:hover, .epoch:hover, .emap:hover, .btn--primary:hover{
    transform:none;
  }
  .epoch:hover img, .emap:hover .emap__img,
  .mcard:hover .mcard__icon{ transform:none }
  .has-fade img[data-fade]{ opacity:1 }
}

}

/* ================================================ свет по контуру

   ВНИМАНИЕ: этот раздел находится ВНЕ `@layer components`, в отличие от
   всего остального в файле. Неслоевые правила в каскаде сильнее любого
   слоя независимо от специфичности, поэтому здешние правила перебивают
   всё, что объявлено выше и в других файлах.

   Из-за этого, например, `.tilt` перебивает `.mcard:hover`, хотя у того
   специфичность выше: тень карточки при наведении задаёт наклон, а не
   правило компонента. Замерено через CSS.getMatchedStylesForNode —
   победителем идёт `.tilt`.

   Обернуть раздел в слой пробовали: раскладка едет. Здесь же живёт
   сетка `.bento`, и внутри слоя её перебивает `modes.css`, который
   грузится позже, — карточка режима становится 395x625 вместо 608x592.
   Значит переносить надо вместе с сеткой и проверять страницы режимов
   целиком; это отдельная работа, а не строчка. Пока — предупреждение
   здесь, чтобы следующий не искал причину заново.


   Бегущий по рамке отсвет — на конической заливке, которую вращает одна
   анимируемая переменная. Ни холста, ни скрипта: браузер крутит угол на
   видеокарте, а мы платим за это одним псевдоэлементом.

   Почему не Canvas. Рамка повторяет форму карточки, включая скругления, и
   на холсте это означало бы рисовать контур вручную и пересчитывать его
   при каждом изменении размера. Заливка знает форму сама.

   @property нужен, чтобы угол вообще можно было анимировать: без
   объявления типа браузер считает --beam обычной строкой и переключает её
   скачком. Там, где @property не поддержан, свет просто не крутится —
   рамка остаётся ровной, и это по-прежнему выглядит законченно. */

@property --beam{
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes beam-spin{ to{ --beam: 360deg } }

.beam{ position:relative; isolation:isolate }

.beam::before{
  content:"";
  position:absolute; inset:-1px;
  border-radius:inherit;
  padding:1px;
  background:conic-gradient(from var(--beam),
    transparent 0deg,
    color-mix(in srgb, var(--mc, var(--accent)) 85%, transparent) 24deg,
    transparent 60deg,
    transparent 180deg,
    color-mix(in srgb, var(--mc, var(--accent)) 45%, transparent) 200deg,
    transparent 240deg,
    transparent 360deg);
  /* Заливка видна только по кромке: внутренняя часть вырезается маской.
     Так рамка светится, а содержимое остаётся нетронутым. */
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite:exclude;
  opacity:0;
  transition:opacity var(--dur-3) var(--ease-out);
  animation:beam-spin 6s linear infinite;
  pointer-events:none;
  z-index:2;
}

/* В покое свет едва тлеет, при наведении разгорается и ускоряется.
   «Разгоняется по контуру» — это и есть смена длительности: тот же круг
   за две секунды вместо шести. */
.beam:hover::before,
.beam:focus-visible::before{
  opacity:1;
  animation-duration:2s;
}
.beam--on::before{ opacity:.5 }

/* Под светом — мягкое свечение наружу. Отдельным слоем, потому что маска
   рамки не пропускает размытие за пределы кромки. */
.beam::after{
  content:"";
  position:absolute; inset:-1px;
  border-radius:inherit;
  box-shadow:0 0 0 0 color-mix(in srgb, var(--mc, var(--accent)) 40%,
                               transparent);
  transition:box-shadow var(--dur-3) var(--ease-out);
  pointer-events:none;
  z-index:-1;
}
.beam:hover::after{
  box-shadow:0 0 34px -6px color-mix(in srgb,
             var(--mc, var(--accent)) 55%, transparent);
}

@media (prefers-reduced-motion:reduce){
  .beam::before{ animation:none; opacity:0 }
  .beam:hover::before{ opacity:.6 }
}

/* ============================================ переход между страницами

   Браузер сам снимает прежний кадр и разводит его с новым. Настраиваем
   только длительность и кривую — умолчание слишком быстрое и линейное,
   от этого переход читается как мигание, а не как движение.

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

@view-transition{ navigation:auto }

::view-transition-old(root),
::view-transition-new(root){
  animation-duration:.34s;
  animation-timing-function:cubic-bezier(.2,.8,.3,1);
}
::view-transition-old(root){ animation-name:vt-out }
::view-transition-new(root){ animation-name:vt-in }

@keyframes vt-out{ to{ opacity:0; transform:translateY(-6px) } }
@keyframes vt-in{ from{ opacity:0; transform:translateY(8px) } }

.hdr{ view-transition-name:hdr }
.ftr{ view-transition-name:ftr }

@media (prefers-reduced-motion:reduce){
  ::view-transition-old(root),
  ::view-transition-new(root){ animation:none }
}

/* ================================================ карточка, чувствующая курсор

   Три слоя поверх обычной карточки, и все три управляются двумя числами
   из скрипта: --near (насколько близко курсор) и --mx/--my (где он внутри).

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

/* Подсветка границы по курсору — ОДИН механизм на проект и только для
   крупных карточек: `.epoch`, `.mcard`, `.emap`, `.panel--card`
   (список задаётся в shell.js). Кнопкам, полям, строкам списков и
   ячейкам стикеров он не даётся, и это решение, а не недоделка.

   Почему не всем. Механизм стоит обработчика мыши на элемент, наклона
   в 3D и двух псевдоэлементов. На карточке 600x590 это читается как
   объём; на кнопке высотой 40 пикселей наклон выглядит игрушкой, а на
   строке списка — дрожанием. Плюс `transform-style: preserve-3d`
   однажды уже выкинул карточки из проверки попаданий и сделал их
   ненажимаемыми: цена ошибки здесь выше обычного.

   Остальным интерактивным элементам отклик даёт обычный `:hover` —
   рамка и фон. Он одинаковый у всех и описан в разделе состояний. */
.reactive{
  position:relative;
  isolation:isolate;
  /* Значения по умолчанию: без них до первого движения мыши переменные
     пустые, и браузер отбрасывает всё правило целиком. */
  --near:0;
  --mx:50%;
  --my:50%;
  --sx:0px;
  --sy:10px;
  transition:transform var(--dur-3) var(--ease-out),
             box-shadow var(--dur-3) var(--ease-out),
             border-color var(--dur-3) var(--ease-out);
}

/* 1. Граница загорается по мере приближения. Не рамка целиком, а
      конический отблеск, повёрнутый в сторону курсора: так видно, откуда
      идёт свет, и сетка перестаёт быть набором одинаковых коробок. */
.reactive::before{
  content:"";
  position:absolute; inset:-1px;
  border-radius:inherit;
  padding:1px;
  /* Точек в градиенте больше, чем нужно для формы: между соседними
     браузер ведёт яркость по прямой, и на трёх точках виден перелом —
     то самое кольцо вокруг курсора. Пять точек с шагом, повторяющим
     затухание света, дают край, у которого не найти границы. */
  background:
    radial-gradient(420px circle at var(--mx) var(--my),
      color-mix(in srgb, var(--mc, var(--accent)) 85%, transparent) 0%,
      color-mix(in srgb, var(--mc, var(--accent)) 46%, transparent) 18%,
      color-mix(in srgb, var(--mc, var(--accent)) 18%, transparent) 38%,
      color-mix(in srgb, var(--mc, var(--accent)) 5%,  transparent) 62%,
      transparent 85%);
  -webkit-mask:linear-gradient(#000 0 0) content-box,
               linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite:exclude;
  /* Контур — единственный слой, который должен перекрывать рамку под
     собой, а не складываться с ней: рамка тёмная, и сложение оставило бы
     от неё грязь. */
  opacity:calc(var(--near) * 1.15);
  pointer-events:none;
  z-index:6;
}

/* 2. Фонарик внутри. Мягкое пятно, идущее за курсором по поверхности.

      Режим наложения здесь обязателен. Тот же градиент без него ложится
      плёнкой поверх карточки и гасит всё, что под ним; screen складывает
      яркости — подложка остаётся видна, а пятно читается как свет, а не
      как наклейка. На тёмном фоне сложение ещё и безопасно: складывать
      почти нечего, кроме самого пятна. */
.reactive::after{
  content:"";
  position:absolute; inset:0;
  border-radius:inherit;
  background:
    radial-gradient(560px circle at var(--mx) var(--my),
      color-mix(in srgb, var(--mc, var(--accent)) 20%, transparent) 0%,
      color-mix(in srgb, var(--mc, var(--accent)) 11%, transparent) 22%,
      color-mix(in srgb, var(--mc, var(--accent)) 4%,  transparent) 45%,
      transparent 72%);
  mix-blend-mode:var(--blend-fx);
  opacity:calc(var(--near) * 1);
  pointer-events:none;
  /* Свет лежит ПОВЕРХ содержимого, а не под ним.

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

     Поверх текста это безопасно ровно потому, что режим наложения
     складывающий: screen может только осветлить, а светлый текст и так
     у потолка яркости — прибавлять ему нечего. Темнеет от этого слоя
     ничего не может по определению. */
  z-index:5;
}

/* 3. Наклон. Значения ставит скрипт; переход только на возврат — пока
      мышь внутри, наклон должен идти без задержки, иначе он «плывёт».

      Точки схода здесь нет: её ставит скрипт родителю, чтобы вся сетка
      наклонялась к одной точке. Своя perspective() у каждой карточки
      означала бы свою точку схода в её собственном центре — именно от
      этого карточка выглядела перекошенной, а не объёмной. */
.tilt{
  transform:rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style:preserve-3d;
  will-change:transform;
  box-shadow:var(--sx) var(--sy) 30px -14px var(--shadow-color);
}
.tilt:not(.is-tilting){
  transition:transform var(--dur-3) var(--ease-out),
             box-shadow var(--dur-3) var(--ease-out);
}
.tilt.is-tilting{ transition:none }

/* Скользящий блик. Появляется, только когда курсор внутри: снаружи он
   выглядел бы как засвет на всей сетке сразу. Тоже сложением — белая
   полоса поверх карточки читается как запотевшее стекло. */
.tilt.is-tilting .glare,
.reactive:hover .glare{ opacity:.45 }
.glare{
  position:absolute; inset:0;
  border-radius:inherit;
  background:linear-gradient(
    calc(var(--rx, 0deg) + var(--ry, 0deg) + 120deg),
    transparent 34%,
    var(--sheen-3) 47%,
    var(--wash-1) 58%,
    transparent 72%);
  mix-blend-mode:var(--blend-fx);
  opacity:0;
  transition:opacity var(--dur-2) var(--ease-out);
  pointer-events:none;
  z-index:5;
}

/* Содержимое поднимаем над слоями света. Без этого фонарик ложился бы
   поверх текста и мылил его. */
.reactive > *{ position:relative; z-index:4 }

@media (prefers-reduced-motion:reduce){
  .reactive::before, .reactive::after, .glare{ display:none }
  .tilt{ transform:none; box-shadow:none }
}

/* ================================================ сетка бенто

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

@media (min-width:900px){
  .bento{
    display:grid;
    grid-template-columns:repeat(4, 1fr);
    /* Ряд задан снизу, а не намертво. С жёсткой высотой карточка с
       длинным описанием обрезалась по нижнему краю — и первым уходило
       под обрез то, ради чего на страницу и приходят: сколько там
       игроков. Пусть лучше ряд подрастёт. */
    grid-auto-rows:minmax(212px, auto);
    gap:var(--sp-4);
  }
  .bento > *{ grid-column:span 1; grid-row:span 1; min-width:0 }

  /* Первая карточка — во всю высоту двух рядов и на две колонки.
     Вторая — широкая. Дальше обычные: если выделить всё, не выделено
     ничего. */
  .bento > .bento--xl{ grid-column:span 2; grid-row:span 2 }
  .bento > .bento--wide{ grid-column:span 2 }

  /* Карточка растягивается на всю ячейку, а её подложка — на всё
     оставшееся место: иначе крупная карточка выглядела бы обычной с
     пустотой снизу. */
  .bento .mcard{ height:100%; display:flex; flex-direction:column }
  .bento .mcard__art{ flex:1; aspect-ratio:auto; min-height:88px }

  /* Крупная карточка — не увеличенная мелкая, а своя composition.
     Значок крупнее, имя крупнее, описание в три строки вместо двух, и
     воздуха вокруг больше: на площади вчетверо больше прежние отступы
     читаются как поля пустой страницы. */
  .bento .bento--xl .mcard__art{ min-height:200px }
  .bento .bento--xl .mcard__icon{ width:52px; height:52px }
  .bento .bento--xl .mcard__art::after{ font-size:clamp(44px, 5.2vw, 76px) }
  .bento .bento--xl .mcard__body{ padding:var(--sp-5) }
  .bento .bento--xl .mcard__name{ font-size:var(--fs-2xl); line-height:1.1 }
  .bento .bento--xl .mcard__body .muted{ -webkit-line-clamp:3; max-width:46ch }
  .bento .bento--xl .mcard__facts{ gap:var(--sp-6) }
  .bento .bento--xl .mcard__facts .num{ font-size:var(--fs-lg) }

  .bento .bento--wide .mcard__art{ min-height:112px }
  .bento .bento--wide .mcard__art::after{ font-size:clamp(30px, 3vw, 44px) }
}

/* ============================================ заголовок из размытия

   Слово выходит из расфокуса и садится на место. Размытие небольшое —
   восемь пикселей: больше выглядит как дефект экрана, а не как приём.
   Держим на composited-свойствах, чтобы это считала видеокарта. */

.split .word{
  display:inline-block;
  opacity:0;
  filter:blur(8px);
  transform:translateY(14px);
  will-change:opacity, transform, filter;
}
.split.is-shown .word{
  opacity:1;
  filter:blur(0);
  transform:none;
  transition:opacity .55s var(--ease-out) var(--wd, 0ms),
             transform .6s var(--ease-out) var(--wd, 0ms),
             filter .55s var(--ease-out) var(--wd, 0ms);
}

@media (prefers-reduced-motion:reduce){
  .split .word{ opacity:1; filter:none; transform:none }
}

/* ============================================ карта без снимка

   Не у каждой общественной карты есть загрузочный экран. Раньше здесь
   был серый прямоугольник с именем файла, и на фоне настоящих снимков он
   читался как «картинка не загрузилась».

   Теперь это осознанная графика: развёртка с сканлайнами, как на
   мониторе с электронной трубкой, и разведёнными каналами по краям.
   Имя карты набрано моноширинным и подрагивает при наведении. */

.emap__blank{
  /* Контейнер для cqw: размер названия считается от ширины карточки, а
     не от окна — в сетке из четырёх колонок и в одной они разные. */
  container-type:inline-size;
  position:relative; overflow:hidden;
  display:grid; place-items:center;
  width:100%; aspect-ratio:16/9;
  background:
    radial-gradient(120% 90% at 50% 0%,
      color-mix(in srgb, var(--mc, var(--accent)) 22%, transparent),
      transparent 62%),
    linear-gradient(160deg, var(--bg-elevated), var(--bg-sunken));
  border-bottom:1px solid var(--stroke);
  color:var(--text);
  padding:var(--sp-4);
  text-align:center;
  text-shadow:1px 0 0 var(--glitch-a), -1px 0 0 var(--glitch-b);
}
/* Название крупно и своим шрифтом. Моноширинным у нас набирается только
   измеренное сервером — название картой не измеряется, это имя. */
.emap__blank b{
  position:relative; z-index:1;
  font-family:var(--font-display);
  font-weight:700;
  font-size:clamp(var(--fs-md), 9cqw, var(--fs-xl));
  line-height:1.1;
  letter-spacing:var(--tracking-head);
  /* Длинное название не должно вылезать за карточку. */
  overflow-wrap:anywhere;
}

/* Развёртка. Повторяющиеся полосы и медленный проход луча сверху вниз. */
.emap__blank::before{
  content:"";
  position:absolute; inset:0;
  background:repeating-linear-gradient(
    to bottom,
    var(--sheen-1) 0 1px,
    transparent 1px 3px);
  pointer-events:none;
}
.emap__blank::after{
  content:"";
  position:absolute; left:0; right:0; height:36%;
  background:linear-gradient(to bottom,
    transparent, var(--sheen-2), transparent);
  animation:scan 5.5s linear infinite;
  pointer-events:none;
}
@keyframes scan{
  0%{ transform:translateY(-120%) }
  100%{ transform:translateY(320%) }
}

/* Сбой при наведении: имя дёргается и каналы расходятся сильнее. */
.emap:hover .emap__blank{
  animation:glitch .45s steps(2, end) 1;
  text-shadow:2px 0 0 var(--glitch-a-strong),
              -2px 0 0 var(--glitch-b-strong);
}
@keyframes glitch{
  0%   { transform:translate(0) }
  25%  { transform:translate(-2px, 1px) }
  50%  { transform:translate(2px, -1px) }
  75%  { transform:translate(-1px, 0) }
  100% { transform:translate(0) }
}

@media (prefers-reduced-motion:reduce){
  .emap__blank::after{ animation:none }
  .emap:hover .emap__blank{ animation:none }
}

@layer components {

/* Имя человека, ведущее в его карточку. Не ссылка тегом <a>: карточка
   открывается окном, адрес страницы не меняется, и «открыть в новой
   вкладке» дало бы пустое обещание. Но выглядит и ведёт себя как
   ссылка — включая фокус с клавиатуры. */
.person{
  cursor:pointer;
  text-decoration:underline;
  text-decoration-color:transparent;
  text-underline-offset:3px;
  transition:color var(--dur-1) var(--ease-out),
             text-decoration-color var(--dur-1) var(--ease-out);
}
.person:hover{ color:var(--accent-ink); text-decoration-color:var(--accent-line) }
.person:active{ transform:translateY(1px) }

@media (prefers-reduced-motion:reduce){
  .person{ transition:none }
  .person:active{ transform:none }
}

}

/* Пустое состояние строкой — см. stateRow в ui.js.
   Одна строка вместо панели на треть экрана: отсутствие чего-либо не
   заслуживает панели. */
.staterow{
  display:flex; align-items:center; gap:var(--sp-3);
  flex-wrap:wrap;
  padding:var(--sp-3) var(--sp-4);
  border:1px solid var(--stroke); border-radius:var(--r-md);
  background:var(--bg-sunken);
  color:var(--text-muted); font-size:var(--fs-sm);
}
.staterow__text{ flex:1 1 16rem; min-width:0 }
/* Предупреждение: то же самое, но человеку надо это заметить. */
.staterow--warn{
  border-color:var(--accent-line);
  background:var(--accent-soft);
  color:var(--text);
}
