/* ============================================================================
   Smart Lyceum — components.css
   Составные переиспользуемые компоненты для Mini App страниц.
   Загружается после tokens.css и app.css.
   Каждый компонент задокументирован: имя класса, назначение, BEM-блоки.
   ============================================================================ */

/* === Page shell (edge-to-edge для kanban/pet/profile) ====================
   Применяется к <body data-module="..."> через page-attr. Прячет общую
   обвязку (padding body, max-width .app-main) и даёт полноэкранный shell. */
body[data-module="kanban"],
body[data-module="pet"],
body[data-module="profile"],
body[data-module="family"],
body[data-module="parent_invite"],
body[data-module="feedback"],
body[data-module="policy"],
body[data-module="policy_storage"],
body[data-module="consent"] {
  padding: 0;
  background: var(--app-bg-color);
}
body[data-module="kanban"] .app-main,
body[data-module="pet"] .app-main,
body[data-module="profile"] .app-main,
body[data-module="family"] .app-main,
body[data-module="parent_invite"] .app-main,
body[data-module="feedback"] .app-main,
body[data-module="policy"] .app-main,
body[data-module="policy_storage"] .app-main,
body[data-module="consent"] .app-main {
  max-width: none;
  margin: 0;
  width: 100%;
}

/* Page-level shell — обёртка для edge-to-edge модулей. */
.app-shell {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  width: 100%;
}
.app-shell__body {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 12px 16px;
  padding-bottom: calc(12px + var(--app-safe-bottom));
}

/* Контейнер с ограниченной шириной для модулей, которые вписываются в ленту. */
.app-container {
  width: 100%;
  max-width: var(--app-max-width);
  margin: 0 auto;
  padding: 12px 16px calc(24px + var(--app-safe-bottom));
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* === Page header =========================================================
   Шапка страницы: заголовок + подзаголовок, опционально с actions справа.
   Раньше у каждого модуля был свой .X-header (kanban-header, pet-header,
   profile-header, settings-header) — теперь единый .page-header.
   Пример:
     <div class="page-header">
       <div class="page-header__text">
         <h1>Канбан</h1>
         <p>Вызовы и задачи</p>
       </div>
       <div class="page-header__actions">
         <button class="icon-btn icon-btn--primary">+</button>
       </div>
     </div> */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 2px 0 8px;
  flex-shrink: 0;
}
.page-header__text {
  min-width: 0;
}
.page-header__text h1 {
  font-size: 18px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--app-text-primary);
  margin: 0;
}
.page-header__text p {
  font-size: 12px;
  color: var(--app-text-secondary);
  line-height: 1.3;
  margin: 0;
}
.page-header__actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
  align-items: center;
}

/* === Section card ========================================================
   Базовая карточка-секция с заголовком, иконкой-чипом и телом.
   Заменяет .settings-card, .pet-card, .profile-main-card, .kanban-column и т.п.
   Пример:
     <div class="section-card">
       <div class="section-card__head">
         <span class="chip-icon chip-icon--blue">…svg…</span>
         <div class="section-card__head-text">
           <h2>Заголовок</h2>
           <p>Подзаголовок</p>
         </div>
       </div>
       <div class="section-card__body">…</div>
     </div> */
.section-card {
  background: var(--app-card-bg);
  border: 1px solid var(--app-border-soft);
  border-radius: var(--app-radius-l);
  padding: 16px;
  box-shadow: var(--app-shadow);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.section-card__head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 4px;
}

.section-card__head-text h2 {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.2;
  margin: 0 0 2px;
  color: var(--app-text-primary);
}
.section-card__head-text p {
  font-size: 12px;
  line-height: 1.35;
  color: var(--app-text-secondary);
  margin: 0;
}

/* === Chip icon ===========================================================
   Маленькая цветная иконка в заголовке карточки. Цвет задаётся модификатором.
   Раньше: .settings-chip + .settings-chip--{blue,orange,green}.
   Пример:
     <span class="chip-icon chip-icon--blue">…svg…</span> */
.chip-icon {
  width: var(--app-chip-size);
  height: var(--app-chip-size);
  border-radius: var(--app-radius-s);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border: 1px solid transparent;
}
.chip-icon svg {
  width: 18px;
  height: 18px;
}
.chip-icon--blue {
  background: var(--app-primary-soft);
  color: var(--app-primary-color);
  border-color: var(--app-primary-soft-strong);
}
.chip-icon--orange {
  background: var(--app-warning-soft);
  color: var(--app-warning);
  border-color: rgba(255, 149, 0, 0.22);
}
.chip-icon--green {
  background: var(--app-success-soft);
  color: var(--app-success);
  border-color: var(--app-success-soft-border);
}
.chip-icon--purple {
  background: var(--app-purple-soft);
  color: var(--app-purple);
  border-color: rgba(175, 82, 222, 0.22);
}
.chip-icon--gray {
  background: var(--app-gray-soft);
  color: var(--app-gray);
  border-color: rgba(142, 142, 147, 0.22);
}
.chip-icon--red {
  background: var(--app-danger-soft);
  color: var(--app-danger);
  border-color: var(--app-danger-soft-border);
}

/* === Chip (inline status pill) ===========================================
   Маленькая подпись-статус: "Активен", "Ожидает", "7 дней" и т.п.
   Используется в profile.html, family.html, invite-лендинге. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: var(--app-radius-pill);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  background: var(--app-input-bg);
  color: var(--app-text-secondary);
  border: 1px solid var(--app-border-soft);
  white-space: nowrap;
}
.chip--success {
  background: var(--app-success-soft);
  color: #1b8c3e;
  border-color: var(--app-success-soft-border);
}
.chip--warning {
  background: var(--app-warning-soft);
  color: #b45f06;
  border-color: rgba(255, 149, 0, 0.25);
}
.chip--danger {
  background: var(--app-danger-soft);
  color: var(--app-danger);
  border-color: var(--app-danger-soft-border);
}
.chip--muted {
  background: var(--app-input-bg);
  color: var(--app-text-secondary);
  border-color: var(--app-border-soft);
}
.chip--primary {
  background: var(--app-primary-soft);
  color: var(--app-primary-color);
  border-color: var(--app-primary-soft-strong);
}

/* === Segmented tabs ======================================================
   Горизонтальный сегментированный контрол с иконками.
   Использовался в kanban (calendar-tabs) и pet (pet-tabs).
   Пример:
     <div class="segmented-tabs">
       <button class="segmented-tab active">…icon… <span>Label</span></button>
       <button class="segmented-tab">…icon… <span>Label</span></button>
     </div> */
.segmented-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  flex-shrink: 0;
  background: var(--app-input-bg);
  border-radius: var(--app-radius-l);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  -ms-overflow-style: none;
  scrollbar-width: none;
  margin-bottom: 12px;
}
.segmented-tabs::-webkit-scrollbar {
  display: none;
}
.segmented-tab {
  flex: 1 1 0;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 10px 0;
  margin-top: 0;
  border-radius: var(--app-radius-m);
  font-size: 13px;
  font-weight: 500;
  border: none;
  background: transparent;
  color: var(--app-text-secondary);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  transition:
    flex 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    padding 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    gap 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    background-color 0.2s ease,
    color 0.2s ease,
    box-shadow 0.2s ease;
}
.segmented-tab span {
  opacity: 0;
  max-width: 0;
  overflow: hidden;
  transition:
    opacity 0.25s ease 0.05s,
    max-width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.segmented-tab svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  transition: transform 0.2s ease;
}
button.segmented-tab:hover,
button.segmented-tab:active {
  color: #fff;
  background: var(--app-primary-hover);
  font-weight: 600;
}
button.segmented-tab:hover svg,
button.segmented-tab:active svg {
  transform: translateY(-1px);
  stroke: #fff;
}
button.segmented-tab.active {
  flex: 3 1 0;
  gap: 8px;
  padding: 10px 14px;
  background: var(--app-primary-color);
  color: #fff;
  font-weight: 600;
  box-shadow: var(--app-primary-shadow);
}
button.segmented-tab.active svg {
  stroke: #fff;
}
.segmented-tab.active span {
  opacity: 1;
  max-width: 200px;
}

/* === Stat grid ==========================================================
   Сетка статистики (3 колонки). Использовалась в pet (pet-stats) и
   profile (stats-grid). Каждая ячейка — .stat-tile.
   Пример:
     <div class="stat-grid">
       <div class="stat-tile">
         <div class="stat-tile__value">42</div>
         <div class="stat-tile__label">Опыт</div>
       </div>
       …
     </div> */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.stat-tile {
  background: var(--app-input-bg);
  border-radius: var(--app-radius-s);
  padding: 10px 6px;
  text-align: center;
  border: 1px solid var(--app-border-soft);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.stat-tile__value {
  font-size: 17px;
  font-weight: 700;
  color: var(--app-text-primary);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-tile__label {
  font-size: 11px;
  font-weight: 600;
  color: var(--app-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

/* === Status / empty / load messages ===================================== */
.status-message {
  text-align: center;
  color: var(--app-text-secondary);
  font-size: 14px;
  padding: 14px 0;
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: var(--app-text-secondary);
}
.empty-state__icon {
  font-size: 48px;
  margin-bottom: 12px;
  opacity: 0.5;
}
.empty-state__text {
  font-size: 15px;
}

/* === Loader spinner ======================================================
   Единый центрированный лоадер. Всегда по центру экрана, фиксированный
   размер, одинаковый на всех модулях. Используется для первичной загрузки
   страницы / вкладки.
   Пример:
     <div class="loader-spinner"><div class="loader-spinner__circle"></div></div>
   Или просто: <div class="loader-spinner" /> — псевдоэлемент сам нарисует. */
.loader-spinner {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
  z-index: var(--app-z-loader);
  display: flex;
  align-items: center;
  justify-content: center;
}
.loader-spinner__circle,
.loader-spinner::after {
  content: "";
  width: 40px;
  height: 40px;
  border: 3px solid var(--app-primary-soft);
  border-top-color: var(--app-primary-color);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  display: block;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Inline-вариант: без fixed-позиционирования (для кнопок, строк, карточек). */
.loader-inline,
.loading-spinner,
.loader-spinner-inline {
  display: inline-block;
  width: 24px;
  height: 24px;
  border: 2px solid var(--app-primary-soft);
  border-top-color: var(--app-primary-color);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  vertical-align: middle;
}

/* Контейнер для inline-лоадера с подписью (истории, списки и т.п.). */
.loading-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 20px;
  color: var(--app-text-secondary);
  font-size: 14px;
  text-align: center;
}

/* === List item (leaderboard / profile item) ==============================
   Универсальная строка списка. Модификаторы: --selected, --with-meta.
   Пример:
     <div class="list-item list-item--selected">
       <div class="list-item__rank">1</div>
       <div class="list-item__body">
         <div class="list-item__name">Иванов</div>
         <div class="list-item__meta">10А · 120 XP</div>
       </div>
       <div class="list-item__trail">120</div>
     </div> */
.list-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-radius: var(--app-radius-m);
  background: var(--app-input-bg);
  border: 1px solid var(--app-border-soft);
  transition:
    background 0.2s ease,
    border-color 0.2s ease,
    transform 0.2s ease;
  cursor: pointer;
  min-width: 0;
}
.list-item:active {
  transform: scale(0.99);
  background: var(--app-input-hover);
}
.list-item--selected {
  background: var(--app-primary-soft);
  border-color: var(--app-primary-soft-strong);
  color: var(--app-primary-active);
}

.list-item__rank {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12px;
  color: var(--app-text-secondary);
  background: var(--app-input-bg);
  border: 1px solid var(--app-border-soft);
  flex-shrink: 0;
}
.list-item--rank-1 .list-item__rank {
  background: #fffbeb;
  color: #b45309;
  border-color: #fde68a;
}
.list-item--rank-2 .list-item__rank {
  background: #f8fafc;
  color: #64748b;
  border-color: #e2e8f0;
}
.list-item--rank-3 .list-item__rank {
  background: #fff7ed;
  color: #c2410c;
  border-color: #fed7aa;
}

.list-item__body {
  flex: 1;
  min-width: 0;
}
.list-item__name {
  font-size: 14px;
  font-weight: 600;
  color: var(--app-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-item__meta {
  font-size: 12px;
  color: var(--app-text-secondary);
}

.list-item__trail {
  font-size: 13px;
  font-weight: 700;
  background: var(--app-primary-soft);
  border: 1px solid var(--app-primary-soft-strong);
  padding: 6px 12px;
  border-radius: var(--app-radius-pill);
  color: var(--app-primary-color);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

/* === Field / form-group =================================================
   Локальное переопределение: label мелкий, вторичный текст. Применяется
   внутри .section-card. */
.field {
  display: flex;
  flex-direction: column;
}
.field > label {
  font-size: 12px;
  font-weight: 600;
  color: var(--app-text-secondary);
  margin: 0 0 6px;
}

/* === Settings save button (full-width primary внутри section-card) ===== */
.save-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 12px 16px;
  margin-top: 4px;
  background-color: var(--app-primary-color);
  color: #fff;
  border: 1px solid transparent;
  border-radius: var(--app-radius-s);
  font-size: 14px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.1s ease;
}
.save-btn:hover:not(:disabled) {
  background-color: var(--app-primary-hover);
}
.save-btn:active:not(:disabled) {
  background-color: var(--app-primary-active);
  transform: scale(0.98);
}
.save-btn:disabled {
  background-color: var(--app-text-tertiary);
  cursor: not-allowed;
  opacity: 0.7;
}

/* === Алиасы легаси-классов к каноническим ================================
   Существующий JS (pets.js, profile.js, kanban.js, tracker.js) ссылается на
   .pet-tab / .pet-header / .pet-card / .stat-box / .stat-value / .stat-label
   / .leaderboard-item / .pet-stats и т.п. Чтобы не править JS и шаблоны
   одновременно, объединяем стили через селекторные группы: легаси-класс
   получает те же правила, что и канонический.
   Эти группы постепенно уменьшаются по мере миграции разметки на
   .segmented-tab, .page-header, .section-card, .stat-tile, .list-item. */
.page-header.pet-header,
.pet-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 2px 0 8px;
  flex-shrink: 0;
}
.page-header__text.pet-header-text,
.pet-header-text {
  min-width: 0;
}

.segmented-tab.pet-tab,
.pet-tab {
  flex: 1 1 0;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 10px 0;
  margin-top: 0;
  border-radius: var(--app-radius-m);
  font-size: 13px;
  font-weight: 500;
  border: none;
  background: transparent;
  color: var(--app-text-secondary);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  transition:
    flex 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    padding 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    gap 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    background-color 0.2s ease,
    color 0.2s ease,
    box-shadow 0.2s ease;
}
.segmented-tab.pet-tab span,
.pet-tab span {
  opacity: 0;
  max-width: 0;
  overflow: hidden;
  transition:
    opacity 0.25s ease 0.05s,
    max-width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.segmented-tab.pet-tab svg,
.pet-tab svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  transition: transform 0.2s ease;
}
.segmented-tab.pet-tab:hover,
.pet-tab:hover {
  color: #fff;
  background: var(--app-primary-hover);
  font-weight: 600;
}
.segmented-tab.pet-tab:hover svg,
.pet-tab:hover svg {
  transform: translateY(-1px);
}
.segmented-tab.pet-tab.active,
.pet-tab.active {
  flex: 3 1 0;
  gap: 8px;
  padding: 10px 14px;
  background: var(--app-primary-color);
  color: #fff;
  font-weight: 600;
  box-shadow: var(--app-primary-shadow);
}
.segmented-tab.pet-tab.active span,
.pet-tab.active span {
  opacity: 1;
  max-width: 200px;
}

.segmented-tabs.pet-tabs,
.pet-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  flex-shrink: 0;
  background: var(--app-input-bg);
  border-radius: var(--app-radius-l);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  -ms-overflow-style: none;
  scrollbar-width: none;
  margin-bottom: 12px;
}
.segmented-tabs.pet-tabs::-webkit-scrollbar,
.pet-tabs::-webkit-scrollbar {
  display: none;
}

/* === Алиас calendar-tab (kanban.js) → segmented-tab === */
.segmented-tab.calendar-tab,
.calendar-tab {
  flex: 1 1 0;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 10px 0;
  margin-top: 0;
  border-radius: var(--app-radius-m);
  font-size: 13px;
  font-weight: 500;
  border: none;
  background: transparent;
  color: var(--app-text-secondary);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  transition:
    flex 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    padding 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    gap 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    background-color 0.2s ease,
    color 0.2s ease,
    box-shadow 0.2s ease;
}
.segmented-tab.calendar-tab span,
.calendar-tab span {
  opacity: 0;
  max-width: 0;
  overflow: hidden;
  transition:
    opacity 0.25s ease 0.05s,
    max-width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.segmented-tab.calendar-tab svg,
.calendar-tab svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  transition: transform 0.2s ease;
}
.segmented-tab.calendar-tab:hover,
.calendar-tab:hover {
  color: #fff;
  background: var(--app-primary-hover);
  font-weight: 600;
}
.segmented-tab.calendar-tab:hover svg,
.calendar-tab:hover svg {
  transform: translateY(-1px);
}
.segmented-tab.calendar-tab.active,
.calendar-tab.active {
  flex: 3 1 0;
  gap: 8px;
  padding: 10px 14px;
  background: var(--app-primary-color);
  color: #fff;
  font-weight: 600;
  box-shadow: var(--app-primary-shadow);
}
.segmented-tab.calendar-tab.active span,
.calendar-tab.active span {
  opacity: 1;
  max-width: 200px;
}

.segmented-tabs.calendar-tabs,
.calendar-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  flex-shrink: 0;
  background: var(--app-input-bg);
  border-radius: var(--app-radius-l);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.segmented-tabs.calendar-tabs::-webkit-scrollbar,
.calendar-tabs::-webkit-scrollbar {
  display: none;
}

/* === Алиасы stat-box / stat-value / stat-label → stat-tile === */
.stat-tile.stat-box,
.stat-box {
  background: var(--app-input-bg);
  border-radius: var(--app-radius-s);
  padding: 10px 6px;
  text-align: center;
  border: 1px solid var(--app-border-soft);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.stat-tile__value.stat-value,
.stat-value {
  font-size: 17px;
  font-weight: 700;
  color: var(--app-text-primary);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-tile__label.stat-label,
.stat-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--app-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

/* === Leaderboard (pet) ==================================================
   Стили перенесены в web/templates/pet.html (head_extra).
   В этом файле legacy-правила для .leaderboard-item / .leader-row /
   .leader-rank / .leader-name / .leader-score / .leader-details
   удалены, т.к. они конфликтовали с новой раскладкой (горизонтальный
   flex в .leaderboard-item ломал вертикальную колонку row+details). */

.rank-1 .leader-rank { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.rank-2 .leader-rank { background: #f8fafc; color: #64748b; border-color: #e2e8f0; }
.rank-3 .leader-rank { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
.list-item--rank-1 .list-item__rank { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.list-item--rank-2 .list-item__rank { background: #f8fafc; color: #64748b; border-color: #e2e8f0; }
.list-item--rank-3 .list-item__rank { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }

/* === Алиас pet-card / settings-card / profile-main-card → section-card === */
.section-card.pet-card,
.pet-card {
  background: var(--app-card-bg);
  border-radius: var(--app-radius-l);
  padding: 16px;
  border: 1px solid var(--app-border-soft);
  box-shadow: var(--app-shadow);
}

/* === Алиасы loader-spinner ===
   Старые .loader/.app-loader/.app-spinner маппятся на канонический
   .loader-spinner. Пустой .loader без .spinner рисуется через ::after. */
.loader-spinner.loader,
.loader {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
  z-index: var(--app-z-loader);
  display: flex;
  align-items: center;
  justify-content: center;
}
.loader-spinner.loader::after,
.loader > .spinner,
.loader::after {
  content: "";
  width: 40px;
  height: 40px;
  border: 3px solid var(--app-primary-soft);
  border-top-color: var(--app-primary-color);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  display: block;
}
/* Если .loader — пустой <div> без вложенного .spinner, рисуем через ::after
   по умолчанию. Иначе (если внутри .spinner) — показываем только его. */
.loader > .spinner {
  background: transparent;
  border: none;
}
