/* ══════════════════════════════════════════════════════════════════════════
   MathGenius · общие стили интерфейса

   Эталон — витрина /styleguide (Иван утвердил 17.09.2026). Сюда переезжает то, что
   на витрине согласовано: страница подключает этот файл и выбрасывает свою копию.
   Переезд — по одному компоненту и по одной странице, между шагами — стенды и
   tests/run.py data. Файл подключается ДО встроенных стилей страницы.

   Сейчас здесь:
     • шкалы: кегли --t-*, отступы --s-*, радиусы --r-*, тени --shadow-*;
     • кнопки (.btn и варианты), поля (.field, .inp, .ta, .sel, .chk);
     • тосты (.ui-toasts), пустые экраны (.empty), скелетоны загрузки (.sk);
     • окна (.ui-modal) — открывает assets/ui.js: MFUI.modal / MFUI.confirm;
     • панели без «чёлок» (.ui-fade) — домашки, журнал, ЛК, пробник, диагностика, повторение;
     • шапка (.hdr) и меню экосистемы (.app-switcher) — пункты меню рисует assets/shell.js;
     • статусы (.pill и словарь из шести состояний), у «в работе» и «на проверке» — точка.
   Цвета темы (--bg-*, --text-*, --accent*) задаёт страница; здесь — только запасные значения.
   Запас — через второе имя токена: у чата, пробника и диагностики текст зовётся --text,
   у повторения рамка — --border-1. Запас сразу тёмным цветом давал в светлой теме
   белый текст на белом (кнопки и окна этих страниц до 17.09).
   ══════════════════════════════════════════════════════════════════════════ */


/* ── Шкалы ──────────────────────────────────────────────────────────────────
   Семь кеглей, пять радиусов (--r-xs — значки и клавиши), отступы кратно четырём, три тени (витрина, 17.09). Страница
   может переопределить значение у себя, но не заводить новые ступени. Тени сами
   облегчаются в светлой теме через --shade-k. */
:root {
  --t-xs:0.72rem; --t-sm:0.82rem; --t-md:0.9rem; --t-base:1rem; --t-lg:1.25rem; --t-xl:1.6rem; --t-2xl:2.2rem;
  --r-xs:6px; --r-sm:10px; --r-md:16px; --r-lg:20px; --r-full:9999px;
  --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px; --s-5:24px; --s-6:32px; --s-7:48px;
  --shadow-1:0 2px 10px rgba(var(--shade-rgb, 0,0,0), calc(0.28 * var(--shade-k, 1)));
  --shadow-2:0 8px 28px rgba(var(--shade-rgb, 0,0,0), calc(0.38 * var(--shade-k, 1)));
  --shadow-3:0 20px 60px rgba(var(--shade-rgb, 0,0,0), calc(0.50 * var(--shade-k, 1)));
  --on-accent:#04201f;                 /* текст на залитом акценте; страница может задать свой */
  --backdrop:rgba(0,0,0,0.6);          /* затемнение под окном; страница может задать своё */
}
:root[data-theme="light"] { --on-accent:#FFFFFF; --backdrop:rgba(15,27,25,0.42); }

/* ── Кнопки ─────────────────────────────────────────────────────────────────
   Одна кнопка на всю платформу: прямоугольник со скруглением --r-sm, высота 40 (малая 32).
   Основная — мягкий акцент, при наведении заливается. Места, где кнопке нужно иное
   (шапка на телефоне, панель сдачи), правят её у себя контекстом: `.bar .btn { … }`. */
.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-2);
  padding:10px 16px; min-height:40px;
  background:var(--bg-3, #101c1c); border:1px solid var(--border, var(--border-1, rgba(255,255,255,0.08))); border-radius:var(--r-sm);
  color:var(--text-1, var(--text, #f0ede6)); font:600 var(--t-md)/1 'Inter', -apple-system, sans-serif; text-decoration:none;
  cursor:pointer; transition:border-color .15s, background .15s, color .15s, transform .1s;
}
.btn:hover { border-color:var(--accent-border, rgba(1,169,161,0.22)); }
.btn:active { transform:translateY(1px); }
.btn:focus-visible { outline:2px solid var(--accent, #01A9A1); outline-offset:2px; }
.btn--primary { background:var(--accent-dim, rgba(1,169,161,0.15)); border-color:var(--accent-border, rgba(1,169,161,0.22)); color:var(--accent-2, #3dd9d0); }
.btn--primary:hover { background:var(--accent, #01A9A1); border-color:var(--accent, #01A9A1); color:var(--on-accent); }
.btn--danger { color:var(--red, #f87171); border-color:rgba(248,113,113,0.3); }
.btn--danger:hover { background:var(--red-dim, rgba(248,113,113,0.15)); border-color:var(--red, #f87171); }
.btn--ghost { background:none; border-color:transparent; color:var(--text-3); }
.btn--ghost:hover { color:var(--text-1, var(--text, #f0ede6)); background:var(--bg-3, #101c1c); }
.btn--sm { padding:6px 12px; min-height:32px; font-size:var(--t-sm); }
.btn--icon { width:40px; padding:0; }
.btn:disabled, .btn[disabled] { opacity:0.45; cursor:not-allowed; transform:none; }
.btn:disabled:hover { border-color:var(--border, var(--border-1, rgba(255,255,255,0.08))); }
.btn--primary:disabled:hover { background:var(--accent-dim, rgba(1,169,161,0.15)); border-color:var(--accent-border, rgba(1,169,161,0.22)); color:var(--accent-2, #3dd9d0); }
.btn.is-loading { pointer-events:none; opacity:0.8; }


/* ── Поля ───────────────────────────────────────────────────────────────────
   Подпись сверху, подсказка или ошибка снизу. .field — колонка; отступ МЕЖДУ полями
   задаёт страница (раскладка у всех своя). На телефоне страница держит у полей
   16px — иначе iPhone приближает страницу при фокусе. */
.field { display:flex; flex-direction:column; gap:6px; }
.field__lbl { font-size:var(--t-xs); font-weight:600; color:var(--text-2); }
.field__hint { font-size:var(--t-xs); color:var(--text-4); line-height:1.5; }
.field__err { font-size:var(--t-xs); color:var(--red, #f87171); }
.inp, .ta, .sel {
  width:100%; padding:10px 12px;
  background:var(--bg-3, #101c1c); border:1px solid var(--border, var(--border-1, rgba(255,255,255,0.08))); border-radius:var(--r-sm);
  color:var(--text-1, var(--text, #f0ede6)); font:400 var(--t-md)/1.5 'Inter', -apple-system, sans-serif;
}
.inp:focus, .ta:focus, .sel:focus { outline:none; border-color:var(--accent-border, rgba(1,169,161,0.22)); }
.inp[aria-invalid="true"], .ta[aria-invalid="true"] { border-color:var(--red, #f87171); }
.inp:disabled, .ta:disabled, .sel:disabled { opacity:0.55; cursor:not-allowed; }
.inp::placeholder, .ta::placeholder { color:var(--text-4); }
.ta { min-height:84px; resize:vertical; }
.chk { display:inline-flex; align-items:center; gap:var(--s-2); font-size:var(--t-md); color:var(--text-2); cursor:pointer; }
.chk input { width:17px; height:17px; accent-color:var(--accent, #01A9A1); }

/* ── Тосты ──────────────────────────────────────────────────────────────────
   Стопка внизу по центру, у тоста цветная кромка слева; «Отменить» — .toast__undo.
   Класс .ui-toasts (а не .toast-stack) — чтобы не задеть страницы, у которых тосты пока
   свои (повторение: значки, угол экрана). Высоту над нижними панелями страница правит
   у себя: .ui-toasts { bottom:… }. */
.ui-toasts { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); z-index:500; display:flex; flex-direction:column; gap:var(--s-2); width:min(92vw,420px); }
.ui-toasts > .toast {
  display:flex; align-items:center; gap:var(--s-3);
  padding:11px 15px; border-radius:var(--r-sm);
  background:var(--bg-2, #0a1212); border:1px solid var(--border, var(--border-1, rgba(255,255,255,0.08))); border-left:3px solid var(--accent, #01A9A1);
  color:var(--text-1, var(--text, #f0ede6)); font-size:var(--t-sm); line-height:1.45; box-shadow:var(--shadow-2);
  animation:ui-toast-in .22s ease both;
}
@keyframes ui-toast-in { from { opacity:0; transform:translateY(8px); } }
.ui-toasts > .toast--ok { border-left-color:var(--green, #34d399); }
.ui-toasts > .toast--error { border-left-color:var(--red, #f87171); }
.ui-toasts > .toast--warn { border-left-color:var(--yellow, #fbbf24); }   /* подсказка: «сначала ответь», «нет карточек по фильтру» */
.toast__undo { margin-left:auto; background:none; border:none; color:var(--accent-2, #3dd9d0); font:700 var(--t-sm) 'Inter', -apple-system, sans-serif; cursor:pointer; }

/* ── Пустые экраны и загрузка ───────────────────────────────────────────────
   Пустой экран — значок, короткая фраза и, где можно, действие («Создать домашку»).
   Скелетон — вместо спиннера там, где форма данных известна. */
.empty { text-align:center; padding:var(--s-6) var(--s-4); color:var(--text-3); }
.empty > i { font-size:1.8rem; color:var(--text-4); display:block; margin-bottom:var(--s-3); }
.empty__t { font-weight:700; color:var(--text-2); margin-bottom:6px; }
.empty__s { font-size:var(--t-sm); margin-bottom:var(--s-4); }
.empty__t:last-child, .empty__s:last-child { margin-bottom:0; }
.empty__acts { display:flex; flex-wrap:wrap; justify-content:center; gap:var(--s-2); }
.empty--compact { padding:var(--s-4); }
.empty--error > i { color:var(--red, #f87171); }   /* «не удалось», «раздел не готов» */
.sk { background:linear-gradient(90deg, var(--bg-3, #101c1c) 25%, rgba(var(--ink-rgb, 240,237,230),0.06) 37%, var(--bg-3, #101c1c) 63%); background-size:400% 100%; animation:ui-sk-shine 1.4s ease infinite; border-radius:var(--r-sm); }
@keyframes ui-sk-shine { 0% { background-position:100% 0; } 100% { background-position:0 0; } }
.sk--line { height:12px; margin-bottom:8px; }
.sk--card { height:68px; }
/* Силуэт страницы собираем из кирпичиков: заголовок, кнопка, круг (аватар, печать) — и раскладки ряд / колонка /
   «колонка папок + список». Контейнер скелетона — role="status" с текстом для экранного диктора в .ui-sr. */
.sk--title { height:22px; width:45%; }
.sk--btn { height:40px; width:120px; flex-shrink:0; }
.sk--circle { width:48px; height:48px; border-radius:50%; flex-shrink:0; }
.sk-page { display:flex; flex-direction:column; gap:var(--s-4); }
.sk-row { display:flex; align-items:center; flex-wrap:wrap; gap:var(--s-3); }
.sk-stack { display:flex; flex-direction:column; gap:var(--s-3); }
.sk-stack > .sk--line, .sk-row > .sk--line { margin-bottom:0; }
.sk-grow { flex:1; }
.sk-split { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,3fr); gap:var(--s-4); align-items:start; }
@media (max-width:640px) { .sk-split { grid-template-columns:1fr; } }
.ui-sr { position:absolute !important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
@media (prefers-reduced-motion: reduce) { .sk, .ui-toasts > .toast { animation:none; } }


/* ── Панели без «чёлок» ─────────────────────────────────────────────────────
   Сплошных полос сверху и снизу нет: шапка и нижняя панель прозрачные, их кнопки
   плавают над содержимым, а содержимое тает под ними в размытии. Так с 15.09 сделан
   чат Саныча — здесь то же для остальных страниц.

   Размытие — отдельный слой, а не фон и не ::before самой панели. У ЛК под шапкой
   ещё полоса вкладок: хвост размытия шапки лёг бы на сами вкладки (шапка выше по
   слоям), а панель со своим backdrop-filter вообще не даёт потомку размыть страницу.
   Отдельный слой лежит под всеми плавающими панелями разом.

   Разметка:  <div class="ui-fade ui-fade--top" aria-hidden="true"><span></span><span></span></div>
   Два слоя с разной силой и масками: у края сильнее, дальше сходит на нет — текст не
   обрывается полосой, а тает. Заливка градиентом гасит его до фона, иначе сквозь блюр
   читаются буквы.

   Страница настраивает:
     --ui-fade-top     высота верхнего слоя (по умолчанию шапка + 24px хвоста);
     --ui-fade-bottom  высота нижнего (по умолчанию 132px + вырез экрана);
     --ui-fade-bg      во что тает содержимое (по умолчанию фон страницы --bg-1 или --bg).

   Слой — z-index 55: шапки 60 и выше, нижние панели 70 и выше, клики он не ловит.
   Всё, что прилипает к верху под шапкой (боковые колонки, якоря), ставим ниже
   хвоста: top: calc(var(--hdr) + 26px). */
.ui-fade {
  --ui-fade-c: var(--ui-fade-bg, var(--bg-1, var(--bg)));
  position:fixed; left:0; right:0; z-index:55;
  pointer-events:none;
}
.ui-fade > span { position:absolute; inset:0; }

.ui-fade--top { top:0; height:var(--ui-fade-top, calc(var(--hdr, 56px) + 24px)); }
.ui-fade--top > span:first-child {
  -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
  -webkit-mask-image:linear-gradient(to bottom, #000 0, #000 62%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 0, #000 62%, transparent 100%);
}
.ui-fade--top > span:last-child {
  -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px);
  background:linear-gradient(to bottom, var(--ui-fade-c) 0, color-mix(in srgb, var(--ui-fade-c) 72%, transparent) 55%, transparent 100%);
  -webkit-mask-image:linear-gradient(to bottom, #000 0, #000 34%, transparent 82%);
          mask-image:linear-gradient(to bottom, #000 0, #000 34%, transparent 82%);
}

.ui-fade--bottom { bottom:0; height:var(--ui-fade-bottom, calc(132px + env(safe-area-inset-bottom))); }
.ui-fade--bottom > span:first-child {
  -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px);
  -webkit-mask-image:linear-gradient(to top, #000 0, #000 58%, transparent 100%);
          mask-image:linear-gradient(to top, #000 0, #000 58%, transparent 100%);
}
.ui-fade--bottom > span:last-child {
  -webkit-backdrop-filter:blur(16px); backdrop-filter:blur(16px);
  background:linear-gradient(to top, var(--ui-fade-c) 0, color-mix(in srgb, var(--ui-fade-c) 80%, transparent) 42%, transparent 100%);
  -webkit-mask-image:linear-gradient(to top, #000 0, #000 36%, transparent 78%);
          mask-image:linear-gradient(to top, #000 0, #000 36%, transparent 78%);
}

/* Печать: размытие не нужно, а сплошная заливка слоя легла бы поверх текста. */
@media print { .ui-fade { display:none; } }


/* ── Шапка и меню экосистемы ────────────────────────────────────────────────
   Одна шапка на домашках, журнале, решении, ЛК, расписании, диагностике и /join.
   У чата, повторения и пробника шапки свои — поэтому правила КНОПКИ меню действуют
   только внутри .hdr (через :where — вес селектора остаётся в один класс); само меню — общее. До 17.09 у
   каждой страницы была своя копия, и они разъехались: знак 28 или 30px, название
   по центру или слева, подпись с отступом и без, у меню — три разных наведения.

   Знак — зелёная звезда без подложки, 36px, один на всю экосистему: так же у пробника,
   повторения и досок (Иван, 17.09); тёмная плашка с белой звездой осталась только у чата.
   Название — раздел («Личный кабинет», «Пробник»), а не «MathGenius».

   Слева — знак и название страницы. Где есть меню экосистемы, это кнопка
   .app-switcher__btn (пункты рисует assets/shell.js), где нет — ссылка .hdr__logo;
   выглядят одинаково. Название стоит по центру над подписью, как в чате.
   Шапка прозрачная (Иван, 17.09): содержимое тает под ней в .ui-fade. Свой фон и
   backdrop-filter ей нельзя — слой перестанет размывать страницу.

     <header class="hdr">
       <div class="app-switcher" id="app-switcher">
         <button class="app-switcher__btn" id="app-switcher-btn" aria-haspopup="true" aria-expanded="false">
           <svg viewBox="0 0 40 40" width="36" height="36">…</svg>
           <div><div class="hdr__title">Домашки</div><div class="hdr__sub">Экосистема MathGenius</div></div>
           <i class="fa-solid fa-chevron-down app-switcher__caret"></i>
         </button>
         <div class="app-switcher__menu" id="app-switcher-menu"></div>
       </div>
       <div class="hdr__spacer"></div>
       <div id="auth-slot"></div>
     </header>
     <div class="ui-fade ui-fade--top" aria-hidden="true"><span></span><span></span></div>

   Высота — токен страницы --hdr (56px, у диагностики 62px). */
.hdr { position:sticky; top:0; z-index:60; height:var(--hdr, 56px); display:flex; align-items:center; gap:14px; padding:0 20px; }
.hdr__logo { display:flex; align-items:center; gap:12px; flex-shrink:0; color:inherit; text-decoration:none; text-align:center; }
.hdr__logo svg, :where(.hdr) .app-switcher__btn svg { flex-shrink:0; }
.hdr__title { font-family:'Unbounded', sans-serif; font-size:var(--t-sm); font-weight:800; }
.hdr__sub { margin-top:2px; font-size:var(--t-xs); color:var(--text-3); }
.hdr__spacer { flex:1; }

:where(.hdr) .app-switcher { position:relative; flex-shrink:0; }
:where(.hdr) .app-switcher__btn {
  display:flex; align-items:center; gap:12px; padding:4px 6px 4px 0;
  background:none; border:none; border-radius:var(--r-sm); color:inherit; font:inherit; text-align:center;
  cursor:pointer; transition:background 0.15s;
}
:where(.hdr) .app-switcher__btn:hover { background:rgba(var(--ink-rgb, 240,237,230),0.04); }
:where(.hdr) .app-switcher__caret { margin-left:2px; font-size:var(--t-xs); color:var(--text-4); transition:transform 0.2s; }
:where(.hdr) .app-switcher__btn[aria-expanded="true"] .app-switcher__caret { transform:rotate(180deg); }
/* Выпадающее меню экосистемы — одно на ВСЕ разделы, включая чат, повторение, пробник (Иван, 17.09:
   «предпочтительнее полупрозрачный вид»). Поэтому, в отличие от кнопки, правила меню не привязаны к .hdr.
   Стекло: фон страницы на 84% и размытие того, что под меню, — строчки под ним угадываются, но не мешают
   читать пункты. Рамка и значки — бирюзой, как было у повторения. */
.app-switcher__menu {
  position:absolute; top:calc(100% + 10px); left:0; z-index:300; min-width:226px; padding:6px;
  background:color-mix(in srgb, var(--bg-2, #0a1212) 84%, transparent);
  -webkit-backdrop-filter:blur(20px) saturate(1.3); backdrop-filter:blur(20px) saturate(1.3);
  border:1px solid rgba(1,169,161,0.2); border-radius:var(--r-md);
  box-shadow:var(--shadow-3), 0 0 0 1px rgba(1,169,161,0.08);
  opacity:0; transform:translateY(-6px) scale(0.97); pointer-events:none; transition:opacity 0.18s, transform 0.18s;
}
.app-switcher__menu.open { opacity:1; transform:translateY(0) scale(1); pointer-events:auto; }
.app-switcher__menu-label { padding:6px 10px 4px; font-size:var(--t-xs); font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:var(--text-4); white-space:nowrap; }
.app-switcher__item {
  display:flex; align-items:center; gap:10px; padding:9px 12px; border:1px solid transparent; border-radius:var(--r-sm);
  color:var(--text-2); font-size:var(--t-sm); font-weight:600; text-decoration:none;
  transition:background 0.15s, color 0.15s, border-color 0.15s;
}
.app-switcher__item:hover { background:rgba(1,169,161,0.07); color:var(--text-1, var(--text, #f0ede6)); }
.app-switcher__item.active { background:var(--accent-dim, rgba(1,169,161,0.15)); border-color:rgba(1,169,161,0.22); color:var(--accent-2, #3dd9d0); }
.app-switcher__item i { width:16px; flex-shrink:0; font-size:var(--t-sm); text-align:center; color:var(--accent, #01A9A1); }
.app-switcher__item-sub { margin-top:1px; font-size:var(--t-xs); color:var(--text-4); }
.app-switcher__sep { height:1px; margin:5px 0; background:rgba(1,169,161,0.18); }

/* Телефон. Найдено 01.09: без своих правил шапка не влезала в 375px и на /diagnostic
   страница ездила вбок. Подпись прячем, длинное название — с многоточием; знак не
   сжимается (flex давил его до 17px). Второстепенная кнопка в шапке — только значок:
   подпись съедает под сотню пикселей. */
@media (max-width:600px) {
  .hdr { gap:8px; padding:0 12px; }
  .hdr__sub { display:none; }
  .hdr__title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .hdr__logo, .hdr .app-switcher, .hdr .app-switcher__btn { min-width:0; }
  .hdr .btn--sm { gap:0; padding:7px 10px; font-size:0; }
  .hdr .btn--sm i { font-size:var(--t-sm); }
}


/* ── Статусы: единый словарь ────────────────────────────────────────────────
   Одно состояние — одно слово и один цвет на всей платформе (витрина, «Статусы»):
     не начато — .pill--idle      в работе — .pill--work      на проверке — .pill--wait
     проверено — .pill--done      после срока — .pill--late   спор — .pill--alarm
   Про домашку: «выдано» — .pill--done, черновик и архив — .pill--idle. Всё, что не
   состояние (тип вопроса, «срок продлён»), живёт на своих классах — живая точка
   у «в работе» не должна появляться там, где ничего не происходит.
   Цвета — токенами страницы, с запасными значениями для страниц без них. */
.pill {
  --pill-h:22px;
  display:inline-flex; align-items:center; gap:5px; height:var(--pill-h); padding:0 10px;
  border-radius:var(--r-full, 9999px); font-size:var(--t-xs, 0.72rem); font-weight:700; line-height:1;
  white-space:nowrap; vertical-align:middle;
}
.pill--idle  { background:rgba(var(--ink-rgb, 240,237,230),0.06); color:var(--text-3); }
.pill--work  { background:rgba(96,165,250,0.15); color:var(--blue, #60a5fa); }
.pill--wait  { background:var(--yellow-dim, rgba(251,191,36,0.15)); color:var(--yellow, #fbbf24); }
.pill--done  { background:var(--green-dim, rgba(52,211,153,0.15)); color:var(--green, #34d399); }
.pill--late  { background:rgba(251,146,60,0.15); color:var(--orange, #fb923c); }
.pill--alarm { background:var(--red-dim, rgba(248,113,113,0.15)); color:var(--red, #f87171); }

/* ── Живые статусы ──────────────────────────────────────────────────────────
   «В работе» и «на проверке»: слева плавно мигает точка их цвета — видно, что дело в
   процессе (Иван, 17.09). Сначала была диаметром с плашку — вышло крупно («поменьше всё
   таки»), теперь --pill-dot, по центру плашки. Место под точку плашка держит сама (отступ
   слева), поэтому в ряду и в карточке ничего не наезжает; кто просил поменьше движения —
   точка просто горит. */
.pill--work, .pill--wait { --pill-dot:10px; position:relative; margin-left:calc(var(--pill-dot) + 6px); }
.pill--work::before, .pill--wait::before {
  content:''; position:absolute; top:50%; right:calc(100% + 6px); margin-top:calc(var(--pill-dot) / -2);
  width:var(--pill-dot); height:var(--pill-dot); border-radius:50%;
  background:currentColor; animation:ui-pill-live 1.8s ease-in-out infinite;
}
@keyframes ui-pill-live { 0%, 100% { opacity:0.9; } 50% { opacity:0.22; } }
@media (prefers-reduced-motion: reduce) {
  .pill--work::before, .pill--wait::before { animation:none; opacity:0.6; }
}


/* ── Окна ───────────────────────────────────────────────────────────────────
   Одно окно на платформу (витрина, «Окна»): заголовок, текст или поля, действия справа,
   главное — последним. Закрывается крестиком, Esc и кликом мимо; фокус живёт внутри окна
   и возвращается туда, откуда открыли. На телефоне — шторка снизу, кнопки во всю ширину,
   главное — сверху. Разметку и поведение даёт assets/ui.js (MFUI.modal, MFUI.confirm) —
   руками окно не собираем. Системные confirm()/alert() не используем: их не оформить,
   а на телефоне они чужие. Разрушительное лучше без вопроса — с «Отменить» в тосте.

   <div class="ui-modal">                          затемнение, z-index 400
     <div class="ui-modal__card" role="dialog">    .ui-modal--wide — 560px, --xl — 760px
       <button class="ui-modal__x">✕</button>
       <h2 class="ui-modal__title">…</h2>  <p class="ui-modal__text">…</p>  <div class="ui-modal__body">поля .field</div>
       <div class="ui-modal__acts"><button class="btn btn--ghost">Отмена</button><button class="btn btn--primary">…</button></div> */
.ui-modal {
  position:fixed; inset:0; z-index:400;
  display:flex; align-items:center; justify-content:center; padding:var(--s-4);
  background:var(--backdrop); -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
  animation:ui-modal-bg .16s ease both;
}
.ui-modal__card {
  position:relative; width:100%; max-width:440px; max-height:calc(100dvh - 2 * var(--s-4)); overflow:auto;
  background:var(--bg-2, #0a1212); border:1px solid var(--border-1, var(--border, var(--border-1, rgba(255,255,255,0.08)))); border-radius:var(--r-lg);
  padding:var(--s-5); box-shadow:var(--shadow-3); color:var(--text-1, var(--text, #f0ede6));
  animation:ui-modal-in .2s cubic-bezier(.2,.8,.2,1) both;
}
.ui-modal--wide .ui-modal__card { max-width:560px; }
.ui-modal--xl .ui-modal__card { max-width:760px; }
.ui-modal__x {
  position:absolute; top:12px; right:12px; width:36px; height:36px; display:flex; align-items:center; justify-content:center;
  background:none; border:none; border-radius:var(--r-sm); color:var(--text-3); font-size:var(--t-base); cursor:pointer;
}
.ui-modal__x:hover { background:var(--bg-3, #101c1c); color:var(--text-1, var(--text, #f0ede6)); }
.ui-modal__x:focus-visible { outline:2px solid var(--accent, #01A9A1); outline-offset:2px; }
.ui-modal__title { margin:0 44px var(--s-3) 0; font:800 var(--t-base)/1.35 'Unbounded', sans-serif; text-wrap:balance; }
.ui-modal__title > i { margin-right:8px; color:var(--accent-2, #3dd9d0); }
.ui-modal__text { margin:0; font-size:var(--t-md); line-height:1.55; color:var(--text-2); }
.ui-modal__text + .ui-modal__body { margin-top:var(--s-4); }
.ui-modal__body { display:flex; flex-direction:column; gap:var(--s-4); }
.ui-modal__acts { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:var(--s-2); margin-top:var(--s-5); }
.ui-modal.is-closing, .ui-modal.is-closing .ui-modal__card { animation:ui-modal-out .14s ease both; }
@keyframes ui-modal-bg { from { opacity:0; } }
@keyframes ui-modal-in { from { opacity:0; transform:translateY(10px) scale(.98); } }
@keyframes ui-modal-out { to { opacity:0; } }
@keyframes ui-sheet-in { from { transform:translateY(40px); opacity:.4; } }

/* Шторка: на телефоне сама, в витрине — классом --sheet (показать, как будет на телефоне). */
@media (max-width:640px) {
  .ui-modal { align-items:flex-end; padding:0; }
  .ui-modal__card { max-width:none; max-height:88dvh; border-radius:var(--r-lg) var(--r-lg) 0 0; border-bottom:none;
    padding:var(--s-6) var(--s-4) calc(var(--s-4) + env(safe-area-inset-bottom)); animation-name:ui-sheet-in; }
  .ui-modal__card::before { content:''; position:absolute; top:10px; left:50%; width:36px; height:4px; margin-left:-18px; border-radius:2px; background:var(--border, var(--border-1, rgba(255,255,255,0.08))); }
  .ui-modal__acts { flex-direction:column-reverse; }
  .ui-modal__acts > .btn { width:100%; min-height:46px; }
}
.ui-modal--sheet { align-items:flex-end; padding:0; }
.ui-modal--sheet .ui-modal__card { max-width:none; border-radius:var(--r-lg) var(--r-lg) 0 0; border-bottom:none; padding:var(--s-6) var(--s-4) var(--s-4); }
.ui-modal--sheet .ui-modal__card::before { content:''; position:absolute; top:10px; left:50%; width:36px; height:4px; margin-left:-18px; border-radius:2px; background:var(--border, var(--border-1, rgba(255,255,255,0.08))); }
.ui-modal--sheet .ui-modal__acts { flex-direction:column-reverse; }
.ui-modal--sheet .ui-modal__acts > .btn { width:100%; min-height:46px; }
html.ui-modal-open { overflow:hidden; }
@media (prefers-reduced-motion: reduce) { .ui-modal, .ui-modal__card, .ui-modal.is-closing .ui-modal__card { animation:none; } }
