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

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

   Сейчас здесь:
     • шкалы: кегли --t-*, отступы --s-*, радиусы --r-*, тени --shadow-*, движение --ease, --ease-move, --dur-0…3;
       пружина --spring-ok — только верный ответ и магнитная посадка перетаскивания, больше нигде;
     • переходы между страницами (@view-transition), нажатие (scale), уход тостов и окон — «плавность и звук»;
     • «уменьшить движение» в системе — на всех страницах с этим файлом (в конце файла);
     • кнопки (.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 и словарь из шести состояний), у «в работе» и «на проверке» — точка;
     • «Саныч думает» (.mf-mark) — знак как индикатор ожидания; разметку и состояния ставит assets/sanych-mark.js.
   Цвета темы (--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)));
  /* Движение (02.10): одни кривые и три длительности на всю платформу, без пружин с перелётом за цель
     (cubic-bezier(.34,1.56,…) убраны отовсюду). --ease — быстрый старт и мягкая посадка: так появляется
     и откликается всё; --ease-move — разгон и торможение: перелёт с места на место, переворот карточки.
     --dur-1 — отклик на наведение и нажатие, --dur-2 — появление и исчезновение, --dur-3 — перестановка. */
  --ease:cubic-bezier(.2,.8,.2,1); --ease-move:cubic-bezier(.4,0,.2,1);
  --dur-1:120ms; --dur-2:200ms; --dur-3:320ms;
  /* --dur-0 — само нажатие (вдавливание кнопки). --spring-ok — пружина с перелётом 6%: ТОЛЬКО (1) верный ответ и
     (2) магнитная посадка при перетаскивании (makeDraggable в чате). Больше нигде — в остальном интерфейсе перелёт
     за цель читается как дребезг (ТЗ «плавность и звук», шаг 0). */
  --dur-0:80ms;
  --spring-ok:linear(0, .18, .42, .72, .95, 1.06, 1.04, 1.01, .99, 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); }

/* ── Кнопки ─────────────────────────────────────────────────────────────────
   Три вида (ревизия дизайна 2.1, 02.10): прямоугольник со скруглением --r-sm, высота 40 (малая 32).
     .btn--primary — главная, залитая акцентом. ОДНА на экран: «Поехали», «Начать экзамен», «Отправить»,
                     «Войти», «Проверить». При наведении на 6% темнее.
     .btn          — второстепенная: фон --bg-2, рамка, текст --text-1. «Сохранить», «Из банка», «Отключить».
     .btn--ghost   — тихая: «Отмена», «К списку», «На платформу».
   Мягкий бирюзовый (.btn--soft) — не главная кнопка, а выбранное состояние. Красный текст — .btn--danger.
   Места, где кнопке нужно иное (шапка на телефоне, панель сдачи), правят её у себя контекстом. */
.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-2);
  padding:10px 16px; min-height:40px;
  background:var(--bg-2, #0a1212); 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;   /* переходы и нажатие — общим правилом ниже (шаг 2) */
}
.btn:hover { border-color:var(--accent-border, rgba(1,169,161,0.22)); }
/* Нажатие (шаг 2): вдавливание свойством scale, а не transform — не спорит с transform карточек и окон. */
:is(.btn, .tab, .act, .a-card, .my-card, .fld-item, .verdict-btn, .grade, .round-btn, .nav-btn, .hw-switch__tab, .app-switcher__item) {
  transition:background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
             scale var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
:is(.btn, .tab, .verdict-btn, .grade, .round-btn, .nav-btn, .hw-switch__tab, .my-card, .fld-item):active:not(:disabled) { scale:.97; transition-duration:var(--dur-0); }
:is(.act, .a-card):active:not(:disabled) { scale:.98; transition-duration:var(--dur-0); }   /* крупные карточки — меньше, иначе «прыгают» */
:is(.btn--icon, .a-card__fld):active:not(:disabled) { scale:.92; }
.btn:focus-visible { outline:2px solid var(--accent, #01A9A1); outline-offset:2px; }
.btn--primary { background:var(--accent, #01A9A1); border-color:var(--accent, #01A9A1); color:var(--on-accent); }
.btn--primary:hover { background:color-mix(in srgb, var(--accent, #01A9A1) 94%, #000); border-color:color-mix(in srgb, var(--accent, #01A9A1) 94%, #000); color:var(--on-accent); }
.btn--soft { 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--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, #01A9A1); border-color:var(--accent, #01A9A1); color:var(--on-accent); }
.btn.is-loading { pointer-events:none; opacity:0.8; }

/* ── Цифры (ревизия 2.4) ────────────────────────────────────────────────────
   Все числа — Inter с цифрами одной ширины: столбцы, баллы и таймеры не прыгают. JetBrains Mono — только таймер
   пробника и код. Unbounded — одно главное число на экран («5 / 32», прогноз, «242 дня»). */
:where(html) { font-variant-numeric:tabular-nums; }
.num { font-family:'Inter', -apple-system, sans-serif; font-variant-numeric:tabular-nums; letter-spacing:0; }

/* ── Переключатель (ревизия 2.1) ────────────────────────────────────────────
   Вкладки и выбор из двух-пяти (ЛК, «Мои задания / Работы учеников», «Теория / Задачи», «Галерея / Учёба /
   Прогресс»): одна общая подложка и выделенный сегмент — мягкий бирюзовый, как .btn--soft. Белых пилюль с рамкой
   нет. Выбранный пункт — .active, .on или aria-checked/selected/pressed="true"; data-ui-slide — подложка едет. */
.seg { display:inline-flex; align-items:center; gap:2px; padding:3px; border-radius:var(--r-full);
  background:var(--bg-3, #101c1c); border:1px solid var(--border, var(--border-1, rgba(255,255,255,0.08))); max-width:100%; }
.seg > button, .seg > a, .seg__btn {
  display:inline-flex; align-items:center; justify-content:center; gap:7px; min-height:34px; padding:6px 14px;
  border:none; border-radius:var(--r-full); background:none; color:var(--text-3); text-decoration:none; white-space:nowrap;
  font:600 var(--t-sm)/1.2 'Inter', -apple-system, sans-serif; cursor:pointer;
  transition:color var(--dur-1) var(--ease), background var(--dur-1) var(--ease), scale var(--dur-2) var(--ease);
}
.seg > button:hover, .seg > a:hover, .seg__btn:hover { color:var(--text-1, var(--text, #f0ede6)); }
.seg > button:active:not(:disabled), .seg > a:active, .seg__btn:active:not(:disabled) { scale:.97; transition-duration:var(--dur-0); }   /* нажатие (шаг 2) */
.seg > :is(.active, .on, [aria-checked="true"], [aria-selected="true"], [aria-pressed="true"]) {
  background:var(--accent-dim, rgba(1,169,161,0.15)); color:var(--accent-2, #3dd9d0); box-shadow:inset 0 0 0 1px var(--accent-border, rgba(1,169,161,0.22));
}
.seg > button:focus-visible, .seg > a:focus-visible, .seg__btn:focus-visible { outline:2px solid var(--accent, #01A9A1); outline-offset:1px; }
.seg--block { display:flex; width:100%; }
.seg--block > button, .seg--block > a, .seg--block > .seg__btn { flex:1 1 0; }
@media (pointer:coarse) { .seg > button, .seg > a, .seg__btn { min-height:40px; } }

/* ── Оценка ответа (ревизия 2.1) ────────────────────────────────────────────
   «Не вспомнил / Трудно / Хорошо / Легко» в повторении — не кнопки общего вида, а свой компонент: четыре цвета
   из словаря статусов (.pill): красный, оранжевый, зелёный, синий. */
.grade { display:inline-flex; align-items:center; justify-content:center; gap:.5em; min-height:40px; padding:0 1.1rem;
  border-radius:var(--r-full); border:1.5px solid; white-space:nowrap; cursor:pointer;
  font:700 var(--t-sm)/1.2 'Inter', -apple-system, sans-serif;
  transition:background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease); }
.grade:disabled { opacity:.4; cursor:not-allowed; }
.grade:focus-visible { outline:2px solid var(--accent, #01A9A1); outline-offset:2px; }
.grade--again { color:var(--red, #f87171); background:var(--red-dim, rgba(248,113,113,0.15)); border-color:color-mix(in srgb, var(--red, #f87171) 42%, transparent); }
.grade--hard  { color:var(--orange, #fb923c); background:rgba(251,146,60,0.13); border-color:color-mix(in srgb, var(--orange, #fb923c) 42%, transparent); }
.grade--good  { color:var(--green, #34d399); background:var(--green-dim, rgba(52,211,153,0.15)); border-color:color-mix(in srgb, var(--green, #34d399) 42%, transparent); }
.grade--easy  { color:var(--blue, #60a5fa); background:rgba(96,165,250,0.14); border-color:color-mix(in srgb, var(--blue, #60a5fa) 42%, transparent); }
.grade--again:hover:not(:disabled) { border-color:var(--red, #f87171); }
.grade--hard:hover:not(:disabled)  { border-color:var(--orange, #fb923c); }
.grade--good:hover:not(:disabled)  { border-color:var(--green, #34d399); }
.grade--easy:hover:not(:disabled)  { border-color:var(--blue, #60a5fa); }

/* ── Чертёж из банка (ревизия 2.3) ──────────────────────────────────────────
   Картинки задач и карточек теории — сканы на белом. Не растягивать больше натурального размера (--fig-w из
   naturalWidth, у 2x-картинок — половина; ставит MFUI.fig), белый фон картинки = фон карточки (multiply), в тёмной
   теме — инверсия. Цветные места, которые инверсия портит, помечаем data-fig="keep": светлая подложка без свечения. */
.fig { max-width:min(100%, var(--fig-w, 480px)); height:auto; image-rendering:auto; background:transparent; }
/* Тёмная тема на платформе — это ОТСУТСТВИЕ data-theme у <html> (светлую ставит скрипт темы как data-theme="light"),
   поэтому тёмная ловится через :not([data-theme="light"]). Яркость после инверсии, а не контраст: при contrast(.9)
   белый фон картинки становился серым 5% и проступал прямоугольником, а чёрный при brightness остаётся чёрным —
   и в screen исчезает без следа; линии чуть приглушены (92%). */
:root[data-theme="light"] .fig { mix-blend-mode:multiply; }
:root:not([data-theme="light"]) .fig:not([data-fig="keep"]) { filter:invert(1) hue-rotate(180deg) brightness(.92); mix-blend-mode:screen; }
:root:not([data-theme="light"]) .fig[data-fig="keep"] { mix-blend-mode:normal; background:var(--bg-3, #101c1c); border-radius:var(--r-sm); padding:6px; }


/* ── Поля ───────────────────────────────────────────────────────────────────
   Подпись сверху, подсказка или ошибка снизу. .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-color: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;
}
/* Список выбора (ревизия 1.6, 02.10): свой шеврон вместо системной стрелки, высота 40px — как поле. Стрелка
   нарисована двумя градиентами цветом --text-3 страницы, поэтому верна в любой теме. Фон у .sel страницы задают
   только через background-color: сокращение background стёрло бы стрелку. --auto — по ширине текста, --sm — в строке. */
.sel {
  -webkit-appearance:none; appearance:none; cursor:pointer; text-overflow:ellipsis;
  min-height:40px; padding:8px 34px 8px 12px;
  background-image:linear-gradient(45deg, transparent 50%, var(--sel-arrow, var(--text-3, #8a9997)) 50%),
                   linear-gradient(135deg, var(--sel-arrow, var(--text-3, #8a9997)) 50%, transparent 50%);
  background-position:calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size:5px 5px, 5px 5px; background-repeat:no-repeat;
}
.sel--auto { width:auto; max-width:100%; }
.sel--sm { min-height:32px; padding:4px 28px 4px 9px; font-size:var(--t-sm); background-position:calc(100% - 15px) 50%, calc(100% - 10px) 50%; }
.sel option { background-color:var(--bg-2, #0a1212); color:var(--text-1, var(--text, #f0ede6)); }
.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 {
  /* Уход (шаг 3): тост гаснет и схлопывается по высоте (grid 1fr → 0fr), соседи съезжают сами; узел удаляется только
     после схлопывания, стопку не перерисовываем. Содержимое — в одной обёртке .toast__in (иначе 0fr не схлопнет). */
  display:grid; grid-template-rows:1fr;
  transition:opacity 160ms var(--ease), scale var(--dur-2) var(--ease), grid-template-rows 240ms var(--ease-move),
             margin 240ms var(--ease-move), padding 240ms var(--ease-move), border-width 240ms var(--ease-move);
  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 var(--dur-2) var(--ease) both;
}
@keyframes ui-toast-in { from { opacity:0; transform:translateY(8px); } }
/* Стопка тостов (24.10, Иван: «ограничить четырьмя и перенести, где меньше мешают»): на широком экране — в левом нижнем
   углу, а не посреди страницы; на телефоне — внизу по центру, как было. Повтор того же тоста — счётчик «×N». */
@media (min-width:960px) { .ui-toasts { left:20px; transform:none; width:min(340px, calc(100vw - 40px)); } }
.toast__n { margin-left:8px; font-weight:700; color:var(--text-3, #8a9a98); font-variant-numeric:tabular-nums; }
.ui-toasts > .toast.is-bump { animation:ui-toast-bump 240ms var(--ease) both; }
@keyframes ui-toast-bump { 45% { scale:1.03; } }
.toast__in { display:flex; align-items:center; gap:var(--s-3); min-height:0; overflow:hidden; }
.ui-toasts > .toast.is-leaving { opacity:0; scale:.96; grid-template-rows:0fr; padding-block:0; border-top-width:0; border-bottom-width:0;
  margin-top:calc(-1 * var(--s-2)); pointer-events:none; }   /* отрицательный margin съедает gap стопки */
.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, .ui-toasts > .toast.is-bump { 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; }
/* Знак в шапке — цветом темы, как знак автора в чате (Иван, 18.10): днём тёмно-бирюзовый --accent (#077570), ночью
   #01A9A1. В разметке у пути стоит fill="#01A9A1" — это запас для страниц без ui.css; правило его перекрывает. */
.hdr__logo svg path, .app-switcher__btn svg path { fill:var(--accent, #01A9A1); }
.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 var(--dur-1) var(--ease);
}
: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 var(--dur-2) var(--ease); }
:where(.hdr) .app-switcher__btn[aria-expanded="true"] .app-switcher__caret { transform:rotate(180deg); }
/* Выпадающее меню экосистемы — одно на ВСЕ разделы, включая чат, повторение, пробник. Поэтому, в отличие
   от кнопки, правила меню не привязаны к .hdr. Фон сплошной (Иван, 03.10, с айфона: «прозрачность не к
   месту»): со стеклом 84% и размытием (17.09) строчки страницы проступали между пунктами, а на телефоне
   размытие не всегда срабатывает. Рамка и значки — бирюзой, как было у повторения. */
.app-switcher__menu {
  position:absolute; top:calc(100% + 10px); left:0; z-index:300; min-width:226px; padding:6px;
  background:var(--bg-2, #0a1212);
  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 var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.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 var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
@media (pointer:coarse) { .app-switcher__item { min-height:40px; } }   /* пункт меню под палец (замеры ревизии: было 35px) */
.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; }
  /* 29.09, обход под учеником: min-width:0 не работал — меню стояло с flex-shrink:0, а блок
     с названием внутри кнопки сжиматься не умел. У ученика с длинным именем /account ехал
     вбок на 33px (408 при экране 375). Теперь при нехватке места сжимается название. */
  .hdr .app-switcher { flex-shrink:1; }
  .hdr .app-switcher__btn > div { min-width:0; }
  .hdr .btn--sm { gap:0; padding:7px 10px; font-size:0; }
  .hdr .btn--sm i { font-size:var(--t-sm); }
}
/* Имя у кружка с буквой на телефоне не помещается рядом с названием раздела —
   остаётся кружок, имя и почта — в его меню. */
@media (max-width:480px) {
  .hdr .auth-name { display:none; }
}


/* ── Статусы: единый словарь ────────────────────────────────────────────────
   Одно состояние — одно слово и один цвет на всей платформе (витрина, «Статусы»):
     не начато — .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 var(--dur-2) var(--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 var(--dur-2) var(--ease) 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); }
/* Уход окна (шаг 4): фон гаснет, карточка чуть опускается и сжимается; на телефоне шторка уезжает вниз. ui.js убирает
   узел через 180 мс — чуть позже конца анимации. */
.ui-modal.is-closing { animation:ui-modal-bg-out 160ms var(--ease) both; }
.ui-modal.is-closing .ui-modal__card { animation:ui-modal-out 160ms var(--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; transform:translateY(6px) scale(.97); } }
@keyframes ui-modal-bg-out { to { opacity:0; } }
@keyframes ui-sheet-out { to { transform:translateY(40px); 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.is-closing .ui-modal__card { animation-name:ui-sheet-out; }
}
.ui-modal--sheet { align-items:flex-end; padding:0; }
.ui-modal--sheet.is-closing .ui-modal__card { animation-name:ui-sheet-out; }
.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; } }

/* ── Верно / неверно (шаг 8) ───────────────────────────────────────────────
   .ui-ok-pop — значок «верно» появляется с пружиной --spring-ok (scale .55 → 1 за 520 мс, прозрачность — за --dur-2);
   это одно из двух мест, где пружина разрешена. .ui-err-shake — неверный ответ: без пружины, плашка дважды качнётся
   на 4px за 240 мс. Вешать на сам значок/плашку в момент вердикта; при «меньше движения» — сразу конечный вид. */
.ui-ok-pop { animation:ui-ok-scale 520ms var(--spring-ok) both, ui-ok-fade var(--dur-2) var(--ease) both; }
@keyframes ui-ok-scale { from { scale:.55; } }
@keyframes ui-ok-fade { from { opacity:0; } }
.ui-err-shake { animation:ui-err-shake 240ms var(--ease) both; }
@keyframes ui-err-shake { 20% { translate:-4px 0; } 40% { translate:4px 0; } 60% { translate:-4px 0; } 80% { translate:4px 0; } }

/* ── Переходы между страницами (шаг 1) ──────────────────────────────────────
   Межстраничный View Transition: старая страница уходит влево и гаснет, новая выходит справа; шапка — общая
   группа mf-hdr и стоит на месте. Нужна у ОБЕИХ страниц: доска и студия ставят то же правило у себя (board-ui.css).
   Где нет поддержки (Firefox), переход обычный. Имя mf-hdr на странице одно — у демо-шапки витрины оно снято. */
@view-transition { navigation:auto; }
:is(.hdr, .is-header, .exam-hdr, .top-header) { view-transition-name:mf-hdr; }
::view-transition-old(root) { animation:160ms var(--ease) both mf-vt-out; }
::view-transition-new(root) { animation:240ms var(--ease) both mf-vt-in; }
@keyframes mf-vt-out { to   { opacity:0; translate:-8px 0; } }
@keyframes mf-vt-in  { from { opacity:0; translate:8px 0; } }
/* Смена темы (шаг 7) — тоже View Transition, но кругом от кнопки: на время смены у <html> класс mf-theme-vt, и
   своих кадров у снимков нет (круг рисует JS), иначе правило без условия погасило бы переходы между страницами. */
html.mf-theme-vt::view-transition-old(root), html.mf-theme-vt::view-transition-new(root) { animation:none; mix-blend-mode:normal; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation:none !important; }
}

/* ── «Уменьшить движение» ───────────────────────────────────────────────────
   Системная настройка — на всех страницах с этим файлом (02.10; раньше её слушали только повторение,
   пробник, чат и решение домашки, да и там по кусочку). Анимации и переходы не выключены, а сжаты до
   миллисекунды: transitionend и animationend приходят как раньше, и код, который их ждёт (плитки,
   окна, тосты), не зависает в промежуточном виде. Холсты (звёзды повторения) слушают настройку сами. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:1ms !important; animation-delay:0s !important; animation-iteration-count:1 !important;
    transition-duration:1ms !important; transition-delay:0s !important; scroll-behavior:auto !important;
  }
}

/* ── Переключатели с ползунком ──────────────────────────────────────────────
   Группа с data-ui-slide (MFUI в ui.js): подложка выбранного пункта переезжает за --dur-2 по --ease-move,
   а не прыгает. Призрак — под текстом пунктов; у нового пункта своя подложка спрятана на время полёта. */
.ui-slide-ghost { position:absolute; z-index:-1; pointer-events:none; }
.ui-slide-hide { background:transparent !important; box-shadow:none !important; }

/* ── Знак думает (ТЗ «Саныч думает», 03.10) ─────────────────────────────────
   Разметку собирает assets/sanych-mark.js (MFMark.mount): шесть частей знака — шесть <svg> друг на друге (у каждой
   свои переменные: --ox/--oy — её центр, --tx/--ty — куда она съезжает, задержки цикла), у морфов 1f/1g/1h ещё цели
   — точки или столбики (.mf-mark__t), у 2d/2e — холст. Ключевые кадры — из макета «Саныч думает.dc.html», с
   приставкой mf-mark- (mfOrbit → mf-mark-orbit и т. д.), чтобы не столкнуться с кадрами страниц; mfPetalIn макета —
   возврат переходом от замершего кадра (без провала к .22). Анимируются только transform и opacity. Свечение —
   статичное, только у .mf-mark--glow и только в тёмной теме; вспышка — один раз на done().
   Состояния: .is-think — думает, .is-done — возвращается в знак, .is-live — то и другое (will-change), .is-flash —
   вспышка, .is-rm — «уменьшить движение», .is-fb — холст уступил другому и стал 1b, .is-voice — 1h от громкости. */
.mf-mark { --mf-ease:cubic-bezier(.65,0,.25,1); position:relative; display:inline-block; flex:none; vertical-align:middle;
  width:var(--mf-size, 22px); height:var(--mf-size, 22px); line-height:0; }
.mf-mark__p { position:absolute; inset:0; width:100%; height:100%; overflow:visible; fill:currentColor; transform-origin:var(--ox) var(--oy); }
.mf-mark.is-live .mf-mark__p, .mf-mark.is-live .mf-mark__t, .mf-mark.is-live .mf-mark__tw { will-change:transform, opacity; }
:root:not([data-theme="light"]) .mf-mark--glow { filter:drop-shadow(0 0 calc(var(--mf-size, 22px) * .05) rgba(1,169,161,.5)); }
:root:not([data-theme="light"]) .mf-mark--glow.is-flash { animation:mf-mark-done 700ms var(--ease) var(--mf-flash, 0ms) both; }

/* 1b · обход: лепестки загораются по часовой (верх → правый → нижние → левый), шаг 0,3 с, круг 1,5 с; центр горит.
   Так же думает холст, уступивший другому (.is-fb). */
.mf-mark.is-think:is(.mf-mark--1b, .is-fb) .mf-mark__p:not(.mf-mark__p--c) { animation:mf-mark-orbit 1.5s ease-in-out var(--ko) infinite; }
/* 1c · раскрытие: лепестки по очереди отходят от центра на 7% и возвращаются, волна 0,14 с, 1,8 с */
.mf-mark--1c.is-think:not(.is-rm) .mf-mark__p:not(.mf-mark__p--c) { animation:mf-mark-bloom 1.8s ease-in-out var(--kb) infinite; }
/* возврат 1b/1c: от замершего кадра — в знак за 260 мс, лепесток за лепестком через 50 мс */
.mf-mark.is-done:is(.mf-mark--1b, .mf-mark--1c, .is-fb) .mf-mark__p { transition:opacity 260ms var(--ease) var(--kd), transform 260ms var(--ease) var(--kd); }

/* 1f · 1g · 1h — морф: части знака съезжаются к целям, сжимаются до .2 и гаснут; цели проявляются (через 300 мс) и
   запускают свой цикл. Обратно — части со сдвигом 140 + 40·i мс, цели гаснут на месте. */
.mf-mark--m .mf-mark__p { transition:transform 520ms var(--mf-ease) var(--dout), opacity 200ms var(--mf-ease) var(--dout); }
.mf-mark--m.is-think:not(.is-rm) .mf-mark__p { transform:translate(var(--tx), var(--ty)) scale(.2); opacity:0;
  transition:transform 520ms var(--mf-ease) var(--din), opacity 260ms var(--mf-ease) var(--din2); }
.mf-mark__tw { position:absolute; inset:0; pointer-events:none; }
.mf-mark__t { position:absolute; opacity:0; transform:scale(.4); transition:opacity 220ms var(--mf-ease), transform 360ms var(--mf-ease); }
.mf-mark--m.is-think:not(.is-rm) .mf-mark__t { opacity:var(--op, 1); transform:scale(1); transition-delay:300ms; }
.mf-mark__t > i { display:block; width:100%; height:100%; background:currentColor; }
.mf-mark--m.is-think:not(.is-rm) .mf-mark__t > i { animation:var(--anim); }
.mf-mark--1g.is-think:not(.is-rm) .mf-mark__tw { animation:mf-mark-spin 1.4s linear var(--spin, .5s) infinite; }
.mf-mark--snap .mf-mark__p, .mf-mark--snap .mf-mark__t { transition:none !important; }
/* 1h от громкости: высоту ставит JS, сглаживание 80 мс */
.mf-mark.is-voice .mf-mark__t > i { animation:none !important; transition:transform 80ms linear; }

/* 2d · 2e — холст строго по знаку + 10% под свечение; в покое спрятан */
.mf-mark__cv { position:absolute; left:-5%; top:-5%; width:110%; height:110%; pointer-events:none; }

/* «Уменьшить движение»: любой вариант — 1b с мягкой пульсацией 0,5 → 1 раз в 1,6 с, без морфов и холста; done() —
   без вспышки, 200 мс до полной непрозрачности. !important — перебить общее правило «уменьшить движение» выше
   (оно сжимает анимации и переходы до 1 мс). */
.mf-mark.is-rm.is-think .mf-mark__p:not(.mf-mark__p--c) { animation:mf-mark-pulse 1.6s ease-in-out var(--kp) infinite !important; }
.mf-mark.is-rm.is-done .mf-mark__p { transition:opacity 200ms linear !important; }
.mf-mark.is-rm .mf-mark__tw, .mf-mark.is-rm .mf-mark__cv { display:none; }

@keyframes mf-mark-orbit { 0%, 100% { opacity:.22; } 18% { opacity:1; } 45% { opacity:.22; } }
@keyframes mf-mark-bloom { 0%, 100% { transform:translate(0, 0); } 50% { transform:translate(var(--dx), var(--dy)); } }
@keyframes mf-mark-dot { 0%, 64%, 100% { transform:translateY(0); } 32% { transform:translateY(-85%); } }
@keyframes mf-mark-spin { to { transform:rotate(360deg); } }
@keyframes mf-mark-bar { 0%, 100% { transform:scaleY(.32); } 50% { transform:scaleY(1); } }
@keyframes mf-mark-pulse { 0%, 100% { opacity:.5; } 50% { opacity:1; } }
@keyframes mf-mark-done {
  0%   { transform:scale(1.14); filter:drop-shadow(0 0 calc(var(--mf-size, 22px) * .15) rgba(1,169,161,.95)); }
  100% { transform:scale(1);    filter:drop-shadow(0 0 calc(var(--mf-size, 22px) * .05) rgba(1,169,161,.5)); }
}

/* ── Формула в строке не рвётся (Иван, 10.10) ────────────────────────────────────────────────
   KaTeX переносит строчную формулу после «=» и «+», и «φ = ωt +» оставался на одной строке, а дробь
   уезжала на следующую. Теперь формула переносится целиком. Длиннее своей строки — иначе вылезла бы
   за край — ей снова можно переноситься: класс ставит assets/ui.js (формулыВСтроке). */
.katex { white-space:nowrap; }
.katex.mf-katex-wrap { white-space:normal; }
/* Дробь в тексте (Иван, 24.10: «строчки чуть склеились»): высокая формула растягивает строку ровно по себе, и дробь
   следующей строки вставала вплотную под ней. Поля сверху и снизу — только у кусков формулы с дробью, большим знаком
   (сумма, предел) или таблицей: строки с обычными формулами остаются с прежним шагом, с дробью — между строками воздух. Доска (board.html и студия) — не трогаем:
   там формула — объект с замеренной рамкой; исключаем сам слой доски (#bd-world, .bd-obj), а не страницу. */
.katex .base:has(.mfrac, .op-limits, .mtable):not(#bd-world *, #bd-world-below *, .bd-obj *) { margin-block:.35em; }

/* ── Тост с возвратом (ТЗ домашек 0.1, 23.10): тёмная плашка, «Вернуть ⌘Z», полоска на 3 секунды ── */
/* Фон, текст и кромка — как у остальных тостов, из темы (Иван, 25.10: «тосты не под тему» — тёмная плашка в светлой теме
   смотрелась чужой); «Вернуть» и полоска — акцентом. */
.ui-toasts > .toast.toast--undo { position:relative; overflow:hidden; padding-bottom:14px; }
.toast--undo .toast__undo { display:inline-flex; align-items:center; gap:8px; color:var(--accent, #01A9A1); white-space:nowrap; }
.toast--undo .toast__undo kbd { font:600 11px/1 'Inter', -apple-system, sans-serif; color:var(--text-3, #8a9a98); border:1px solid var(--border-1, var(--border, var(--border-1, rgba(255,255,255,0.08)))); border-radius:6px; padding:3px 5px; }
/* Подсказка клавиш — только где есть клавиатура: на телефоне и планшете (сенсор без мыши) её нет (Иван, 25.10). */
@media (hover:none) and (pointer:coarse) { .toast--undo .toast__undo kbd { display:none; } }
.toast__bar { position:absolute; left:0; bottom:0; height:3px; width:100%; background:var(--accent, #01A9A1); transform-origin:left center;
  animation:ui-toast-bar var(--toast-ms, 3000ms) linear forwards; }
@keyframes ui-toast-bar { from { transform:scaleX(1); } to { transform:scaleX(0); } }
@media (prefers-reduced-motion: reduce) { .toast__bar { animation-duration:var(--toast-ms, 3000ms); } }

/* ── Боковая панель (ТЗ домашек 0.2): справа, сдвигом за 320 мс, затемнение за ней ── */
.ui-drawer-scrim { position:fixed; inset:0; z-index:420; background:rgba(15,27,25,.28); opacity:0; transition:opacity 320ms cubic-bezier(.4,0,.2,1); }
.ui-drawer-scrim.is-on { opacity:1; }
.ui-drawer { position:fixed; z-index:430; top:0; right:0; bottom:0; width:min(var(--drawer-w, 560px), 100%); display:flex; flex-direction:column;
  background:var(--bg-1, #fff); color:var(--text-1); box-shadow:-24px 0 60px rgba(0,0,0,.18); border-left:1px solid var(--border);
  transform:translateX(105%); transition:transform 320ms cubic-bezier(.4,0,.2,1); }
.ui-drawer.is-on { transform:none; }
.ui-drawer__body { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; }
@media (prefers-reduced-motion: reduce) { .ui-drawer, .ui-drawer-scrim { transition:opacity 160ms linear; } .ui-drawer { transform:none; opacity:0; } .ui-drawer.is-on { opacity:1; } }

/* ── Строки-кнопки (ТЗ домашек 0.3): кликабельна вся высота, без зазоров, подсветка с полоской слева, шеврон ── */
.ui-rows { display:flex; flex-direction:column; }
.ui-row { position:relative; display:flex; align-items:center; gap:12px; padding:14px 40px 14px 16px; cursor:pointer; user-select:none; -webkit-user-select:none;
  box-shadow:inset 0 1px 0 var(--border); transition:background-color 160ms var(--ease), box-shadow 160ms var(--ease); background:none; border:0; width:100%;
  text-align:left; font:inherit; color:inherit; }
.ui-rows > .ui-row:first-child { box-shadow:none; }
.ui-row::after { content:'\f054'; font:900 11px/1 'Font Awesome 6 Free'; position:absolute; right:16px; top:50%; translate:0 -50%; color:var(--text-4); }
.ui-row:hover, .ui-row:focus-visible { background:#E3F2F0; box-shadow:inset 3px 0 0 #077570, inset 0 1px 0 var(--border); outline:none; }
:root:not([data-theme="light"]) .ui-row:hover, :root:not([data-theme="light"]) .ui-row:focus-visible { background:rgba(1,169,161,.10); box-shadow:inset 3px 0 0 #2bb9b1, inset 0 1px 0 var(--border); }
.ui-row.is-inert { cursor:default; padding-right:16px; }
.ui-row.is-inert::after { content:none; }
.ui-row.is-inert:hover { background:none; box-shadow:inset 0 1px 0 var(--border); }

/* ── Индексы вне KaTeX (0.4): ₂ ² — обычными цифрами в <sub>/<sup> ── */
.ui-idx { font-size:.72em; line-height:0; }
