/*****************************************************************************
 *  Цветные пятна фона – единая система на весь сайт
 *
 *  Раньше каждая страница держала свои четыре пятна у себя в <style>, одни и те
 *  же строки лежали в десяти файлах, и правка расползалась по ним вручную.
 *  Теперь геометрия и сила заданы здесь один раз, а страница выбирает только
 *  две вещи: сколько пятен поставить в разметке и какого цвета второе.
 *
 *  ПОРЯДОК СИЛЫ
 *
 *      синее → фиолетовое → зелёное → оранжевое
 *      0.24     0.18         0.15      0.12
 *
 *  Пятна намеренно неравнозначные. Четыре одинаково заметных круга по углам
 *  читались как украшение по периметру, а не как освещение: фон делался
 *  однообразным и грязноватым, особенно внизу, где сходились зелёное с
 *  оранжевым. Разная сила даёт понятную иерархию – главный источник один, а
 *  остальные его поддерживают.
 *
 *  Синее ведущее и не меняется нигде: оно держит логотип, активный пункт в
 *  боковой панели и синие заголовки, то есть работает вместе с интерфейсом, а
 *  не сам по себе.
 *
 *  ЧЕТЫРЕ МЕСТА, ОДНИ И ТЕ ЖЕ ВЕЗДЕ
 *
 *      b1  сверху слева    синее, ведущее, стоит на всех страницах
 *      b2  сверху справа   цвет раздела, второе по силе
 *      b3  снизу слева     тот же цвет, еле заметный подпор
 *      b4  снизу справа    оранжевое, только на главной
 *
 *  Позиции между страницами не прыгают, меняются цвет и количество. Переход по
 *  сайту тогда не выглядит сменой декораций: свет стоит там же, где стоял.
 *
 *  Центр каждого пятна, кроме ведущего, вынесен за край экрана. Пятно, целиком
 *  попавшее в кадр, читается предметом, у него видно середину и границу; от
 *  вынесенного за край остаётся только пологий склон, и он читается светом.
 *  У синего центр оставлен в кадре нарочно, это единственный настоящий источник
 *  на странице.
 *
 *  Все пятна эллипсы разного размера, а не круги одного. Круги по углам
 *  выдавали правило, по которому их расставили, разные эллипсы выглядят
 *  рассеянным светом.
 *
 *  Цвет гаснет к 70–72 процентам радиуса, дальше идёт чистая прозрачность. Если
 *  тянуть до самого края, у пятна появляется различимая граница.
 *
 *  ЦЕНТР СТРАНИЦЫ ОСТАЁТСЯ НЕЙТРАЛЬНЫМ
 *
 *  Под заголовком, таблицей и карточками фон почти ровный, и это главное, ради
 *  чего всё перестраивалось. Стекло у нас прозрачное: цветное пятно под
 *  таблицей просвечивает сквозь неё неравномерно, и разные части одной
 *  поверхности получают разную яркость. Хуже того, под строками со статусами
 *  тёплое пятно начинает спорить с цветными метками.
 *
 *  КАК ПОДКЛЮЧАТЬ
 *
 *  В разметке столько пятен, какого типа страница:
 *
 *      главная                      четыре, плюс класс home на .aurora
 *      каталог, события, подготовка три
 *      статья, справка, служебные   два
 *
 *      <div class="aurora"><div class="blob b1"></div>…</div>
 *
 *  Цвет раздела страница задаёт у себя одной строкой:
 *
 *      :root{--glow:#10b981;--glow-op:.12}
 *
 *  Второе число нужно только телефону, где это пятно остаётся единственным, и
 *  выравнивает цвета по видимой яркости. Таблица значений и весь разбор в конце
 *  файла; страницам на фиолетовом писать его не надо, оно там по умолчанию.
 *
 *  Если нижнему пятну нужен свой цвет, а не тот же самый, есть --glow-low.
 *  Так сделано на главной, где сверху фиолетовое, а снизу зелёное, и в
 *  событиях, где цвет раздела синий и без второго цвета страница вышла бы
 *  одноцветной.
 *
 *  Движение пятен за курсором живёт в blobs.js, оно трогает только transform.
 *****************************************************************************/

/* Цвета раздела живут на :root, а не на .aurora, хотя пятна и стоят внутри неё:
   так их видно всей странице, а не только слою с пятнами.

   --tint это цвет полосы внизу страницы. Пробовали приравнять его к цвету
   раздела, чтобы раздел выходил цветным целиком, и вернули обратно: полоса
   лежит подо всем содержимым сразу, и цветная она перекрашивает страницу, а не
   подсвечивает её. Особенно заметно было на тёплом янтарном. Холодный
   сине-серый работает как свет, потому что он ближе всего к самому тёмному
   фону и не спорит ни с одной рубрикой.
   Приравнять обратно можно одной строкой, var(--glow). */
:root{
  --glow: #8b5cf6;            /* цвет раздела, по умолчанию фиолетовый */
  --glow-low: var(--glow);    /* нижнее пятно, по умолчанию того же цвета */
  --tint: #5478b6;            /* полоса внизу страницы, см. .app::before */
}

.aurora{
  position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
}

/* Коробка и свет разделены. Снаружи .blob, он только стоит на месте: размер,
   углы, сила. Внутри ::before, он только двигается. Раньше и то и другое висело
   на одном элементе, и любое правило с transform на странице сносило смещение
   от курсора, потому что transform это одно свойство, а не два слоя. Так же
   проще читать: если пятно стоит не там, смотреть надо снаружи, если дёргается
   – внутри.

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

   pointer-events наследуется от .aurora, но выписан заново – слой накрывает
   страницу целиком, и об этом лучше помнить прямо здесь.

   will-change просит браузер завести пятну отдельный слой отрисовки заранее.
   Размытие в 120 пикселей считается тогда один раз, а дальше двигается уже
   готовая картинка. */
.blob{ position: absolute; pointer-events: none }
.blob::before{
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  filter: blur(120px); will-change: transform;
  transform: translate3d(var(--bx, 0px), var(--by, 0px), 0);
}

/* Ведущее. Размер и сила те же, что были у круга, изменилась только форма:
   высота считается от высоты экрана, поэтому на широком мониторе пятно лежит
   вдоль верхнего края, а не громоздится посередине. */
.b1{ width: 56vw; height: 68vh; left: -2vw; top: -24vh; opacity: .24 }
.b1::before{ background: radial-gradient(closest-side, #2f6bff, transparent var(--bstop, 72%)) }

/* Цвет раздела. Центр уведён за правый край, до содержимого пятно достаёт
   только мягким склоном. */
.b2{ width: 46vw; height: 56vh; right: -26vw; top: -14vh; opacity: .18 }
.b2::before{ background: radial-gradient(closest-side, var(--glow), transparent var(--bstop, 70%)) }

/* Подпор снизу. Центр ниже нижнего края, сила вдвое меньше, чем у ведущего:
   внизу страницы обычно пусто или идёт длинный список, и заметное пятно там
   тянет взгляд туда, где смотреть нечего. */
.b3{ width: 42vw; height: 50vh; left: -24vw; bottom: -30vh; opacity: .10 }
.b3::before{ background: radial-gradient(closest-side, var(--glow-low), transparent var(--bstop, 72%)) }

/* Оранжевое, только на главной. Вдвое слабее синего и размыто сильнее прочих.
   Цвет намеренно насыщенный, а не приглушённый: тёплый цвет на почти чёрном
   фоне при высокой прозрачности остаётся тёплым, а вот приглушённый заранее
   уходит в коричневое и делает угол грязным. */
.b4{ width: 44vw; height: 46vh; right: -24vw; bottom: -28vh; opacity: .12 }
.b4::before{ background: radial-gradient(closest-side, #ff5c33, transparent var(--bstop, 72%));
  filter: blur(150px) }

/* На главной нижнее пятно чуть заметнее, чем на внутренних страницах: там под
   ним стоят плитки разделов, а не пустое поле. */
.aurora.home .b3{ opacity: .15 }

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

   Эта полоса чернотy и снимает. Она не в .aurora, а на .app, то есть
   прокручивается вместе со страницей и покрывает её целиком, от шапки до
   подвала и во всю ширину.

   Формы у неё нет намеренно, и это главное. Прежде чем прийти к полосе, на
   главной перепробовали овал по центру блока и два пятна по бокам от него, и
   оба раза глаз находил именно фигуру, а не свет: у плиток прозрачное стекло, и
   всё, что лежит под ними, просвечивает на лицевую сторону. Ровная полоса
   ничего под собой не выдаёт, потому что выдавать нечего – яркость зависит
   только от высоты и по горизонтали везде одинаковая.

   Работает только нижняя часть страницы. Верх не трогаем вовсе: там первый
   экран, и его держат закреплённые пятна, синее с фиолетовым, – добавлять туда
   ещё один источник значит спорить с ними. Чернота же появляется ниже, когда
   пятна уехали вверх вместе с экраном, и полоса начинается ровно там.

   ПОЛОСА ОТСЧИТЫВАЕТСЯ ОТ ВЕРХА СТРАНИЦЫ И ЗАДАНА В ПИКСЕЛЯХ

   Это важнее, чем кажется, и к правильному ответу мы пришли не сразу.

   Сначала ступени стояли в процентах от высоты страницы. Высота меняется от
   любой мелочи: выбрали тему в базе знаний, карточек стало вчетверо меньше – и
   весь градиент пересчитался, свет поехал. Фон будто реагировал на фильтры.

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

   Единственная точка, которая не двигается при смене числа карточек, это верх
   страницы. От него и считаем. Длина 8000 пикселей, с запасом на самую длинную
   страницу сайта, поэтому конец полосы не виден нигде и обрываться нечему.
   Пиксели, а не vh, нарочно: vh на телефоне скачет вместе с адресной строкой, и
   полоса дёргалась бы при прокрутке.

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

   Короткие страницы получают только начало подъёма, там почти ничего и не
   нужно: они целиком помещаются в первый экран или два, а его держат пятна.

   ОСТАТОК РАЗНИЦЫ СГЛАЖИВАЕТСЯ ПЕРЕХОДОМ

   Геометрия неподвижна, но страница всё равно кончается в разном месте полосы:
   на короткой выдаче подвал попадает в середину подъёма, на длинной уже на
   ровную часть, и общий тон страницы разный. Само по себе это правильно, плохо
   только то, что переключается оно скачком.

   Поэтому длина полосы умножается на --bandk. Обычно он равен единице, а в
   момент смены высоты скрипт в blobs.js ставит в него отношение старой высоты к
   новой – при таком множителе полоса на новой странице выглядит ровно так же,
   как на старой, – и тут же отпускает обратно к единице. Полоса переезжает сама
   в себя за 1.1 секунды.

   max в длине обязателен, и это не перестраховка. Когда список вырастает,
   множитель меньше единицы, и полоса на время становится КОРОЧЕ страницы. Её
   нижний край при этом видно: ниже полосы фон пустой, и по странице едет резкая
   горизонтальная граница. Именно она и читалась рывком при переходе с одной
   темы на все. max не даёт полосе стать короче самой страницы, край всегда
   остаётся за нижним обрезом, а переход от этого не страдает: функция
   непрерывна, и в точке, где одно значение сменяет другое, скачка нет.

   Время подбиралось на слух, и полсекунды оказалось мало. Дело не в длине как
   таковой, а в кривой: у обычной кривой прихода почти вся разница проходит за
   первые полторы сотни миллисекунд, и переход всё равно читается щелчком.
   Здесь кривая с медленным началом и медленным концом, а длина такая, чтобы
   момента начала не было видно вовсе. Фон не должен привлекать внимание к тому,
   что он меняется, иначе проще было не менять его совсем.

   Множитель объявлен через @property, иначе его не анимировать: для браузера
   обычная переменная это строка. Переход висит на самих слоях, а не на .app:
   у .app уже есть свой transition из rail.css, а transition это сокращение, и
   вторая строка снесла бы первую.

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

   z-index −1 уводит слой под содержимое, но не под страницу: контекст наложения
   создаёт сам .app со своим z-index 2. */
@property --bandk{ syntax: "<number>"; inherits: true; initial-value: 1 }

.app::before{
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--tint) 0%, transparent) 0%,
    color-mix(in srgb, var(--tint) 2.2%, transparent) 25%,
    color-mix(in srgb, var(--tint) 2.2%, transparent) 100%);
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% max(8000px * var(--bandk), 100%);
  transition: --bandk 1.1s cubic-bezier(.33,0,.25,1);
}

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

   Как это устроено. Заливка тут та же самая полоса, только плотнее, а всю
   неровность создаёт маска из шума: маска работает по прозрачности картинки,
   поэтому в тех местах, где шум прозрачен, от полосы не остаётся ничего, а где
   плотен – она видна целиком. Получается не рисунок поверх фона, а тот же фон
   разной плотности, и формы у него по-прежнему нет.

   Шум рисует сам браузер, картинки в файлах нет: feTurbulence с низкой частотой
   и пятью октавами даёт облака с деталью около двух сотен пикселей, а растянутый
   до 1400 – около четырёхсот. Это и есть размер клубов. Мелкое зерно на стекле
   живёт отдельно, в grain.css, и с этим слоем не пересекается: там детали в
   доли пикселя, здесь в сотни.

   stitchTiles='stitch' нужен, чтобы плитки шума сходились краями без швов:
   слой во всю страницу, одной плиткой его не накрыть.

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

   Заливка отсчитывается от верха страницы той же длиной, что и пол под ней, а
   шум и так тайлится от верхнего левого угла. Обе привязки к одной неподвижной
   точке, поэтому при смене числа карточек не едет ни полоса, ни рисунок дыма
   по ней. */
.app::after{
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--tint) 0%, transparent) 0%,
    color-mix(in srgb, var(--tint) 4%, transparent) 25%,
    color-mix(in srgb, var(--tint) 4%, transparent) 100%);
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% max(8000px * var(--bandk), 100%);
  transition: --bandk 1.1s cubic-bezier(.33,0,.25,1);
  --smoke-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='700' height='700'%3E%3Cfilter id='s' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.005' numOctaves='5' seed='11' stitchTiles='stitch'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='1.25' intercept='-0.12'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='700' height='700' filter='url(%23s)'/%3E%3C/svg%3E");
  -webkit-mask-image: var(--smoke-img); mask-image: var(--smoke-img);
  -webkit-mask-size: 1400px 1400px; mask-size: 1400px 1400px;
  -webkit-mask-repeat: repeat; mask-repeat: repeat;
}

/* Пока висит этот класс, множитель ставится мгновенно: так скрипт задаёт
   прежнюю геометрию, не показывая скачка, и только потом отпускает её. */
.app.band-jump::before, .app.band-jump::after{ transition: none }

/* Неподвижный фон. Два случая, и оба про одно и то же: движение тут украшение,
   и там, где оно мешает или обходится дорого, его просто нет.

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

   Просьба поменьше движения. Медленно плывущий фон это ровно то, от чего эта
   настройка спасает людей с чувствительным вестибулярным аппаратом.

   Слой отрисовки в обоих случаях тоже не нужен, поэтому will-change снимается:
   держать его без движения значит впустую занимать память видеокарты.
   Скрипт blobs.js проверяет те же условия и не запускает счёт кадров вовсе. */
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce){
  .blob::before{ transform: none; will-change: auto }
}

/* ── телефон: фон без фильтров ───────────────────────────────────────────
   Всё, что ниже, про одно и то же. Размытие и маска это отдельные слои
   отрисовки, и на телефоне за них платят заметно дороже, чем за то, что они
   дают на маленьком экране.

   ПЯТНА БЕЗ BLUR

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

   Вместо фильтра растягиваем последний стоп градиента до самого края эллипса.
   Спад становится длиннее и мягче, и пятно занимает примерно то же место, что
   занимало с размытием. Число одно и меняется здесь, --bstop, у каждого пятна
   свой запасной стоп на случай, если переменной нет.

   ПОЛОСА БЕЗ ДЫМА

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

   Маску снимаем, но полоса от этого не должна стать ярче. Шум у нас колеблется
   около половины, значит в среднем маска пропускала примерно половину слоя,
   и ровно на эту половину мы гасим его прозрачностью. Если на глаз окажется
   темнее или светлее нужного, править тут одно число.

   ОДНО ПЯТНО ВМЕСТО ЧЕТЫРЁХ

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

   Поэтому на пальце остаётся одно, ведущее, в левом верхнем углу. Остальные
   убраны через display, а не прозрачностью: нулевая прозрачность прячет
   результат, но слой всё равно считается.

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

   Главная из этого правила выбита нарочно, отсюда и :not(.home) в селекторе.
   Раздела у неё нет, --glow там стоит фиолетовый просто по умолчанию, и красить
   им единственное пятно значило бы выдавать настройку за смысл. Поэтому правило
   её не трогает вовсе, и она светится тем же синим и той же силой, что и на
   большом экране: это цвет самого сайта, а не какого-то раздела.

   ОДИНАКОВОЕ ЧИСЛО НЕ ЗНАЧИТ ОДИНАКОВЫЙ СВЕТ

   У главной эталон, синий при 0.24. Разделы стояли на том же числе и светили
   заметно сильнее. Дело не в настройке, а в глазе: он взвешивает каналы очень
   неравномерно, зелёный примерно в семьдесят процентов вклада, красный в
   двадцать, синий всего в семь. Поэтому чистый синий при любой прозрачности
   самый тихий из всех наших цветов, а зелёный и янтарный самые громкие.

   Во сколько раз каждый цвет светлее эталонного синего и какая прозрачность
   выравнивает их по видимой яркости:

       #2f6bff  главная       1.00   0.24   эталон
       #8b5cf6  фиолетовый    1.08   0.22   по умолчанию
       #3b82f6  синий раздел  1.29   0.19   события, о проекте
       #f472b6  розовый       1.91   0.13   подготовка
       #10b981  зелёный       2.00   0.12   база знаний
       #f59e0b  янтарный      2.42   0.10   юридический отдел

   Считано по относительной яркости, с переводом в линейный свет, а не по
   складыванию самих чисел цвета: без перевода разница выходит вдвое меньше
   настоящей, и янтарный остался бы ярче прочих.

   Страница задаёт своё число рядом с цветом, одной строкой, там же где --glow.
   Значение по умолчанию отвечает фиолетовому, поэтому страницам на нём, а это
   статьи, английская заглушка, админка и 404, дописывать ничего не нужно.
   Появится новый цвет раздела, число к нему считается по той же таблице.

   На большом экране всё это не нужно: там цвет раздела несёт второе пятно при
   0.18, а рядом светит синее ведущее, и оно задаёт общий тон.

   ШИРИНА ОТДЕЛЬНО ОТ ЯРКОСТИ

   Размеры ведущего считались под широкий монитор, и на телефоне 56vw это всего
   двести пикселей. Вместе со снятым размытием свет обрывался где-то на середине
   экрана, правая половина оставалась чёрной, и пятно читалось не освещением, а
   кляксой, прижатой к левому краю.

   Растягиваем коробку так, чтобы склон уходил за правый край, а вершина осталась
   там же, где была. Считается это парой: вершина лежит в left + width/2, край
   света в left + width. При 160vw и -46vw вершина выходит на 34vw, то есть чуть
   левее середины, а свет кончается за 114vw, то есть уже вне экрана.

   Яркость при этом не растёт: в вершине те же 0.24, шире стал только спад.

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

   СИНИЙ ФОН ВМЕСТО ЧЁРНОГО

   Пятно на телефоне одно, и свет от него кончается примерно на 44 процентах
   высоты экрана. Ниже не остаётся ничего: чистый цвет страницы, 14 единиц
   яркости из 255, и так на каждом экране, потому что пятна закреплены на экране,
   а не на странице. Полоса внизу подпирает, но она набирает полную силу только
   через две тысячи пикселей от верха страницы, и на первых экранах её почти нет.

   Чёрный низ лечим самой страницей: на телефоне её цвет уходит из почти чёрного
   #0b0d12 в тёмно-синий #10141d. Цвет один и лежит здесь, менять надо его.

   Меняем именно background-color, а не background целиком. Страница объявляет
   фон сокращением, вместе с тёплым пятном наверху; трогать сокращение значило бы
   переписать это пятно сюда и держать две копии. Отдельное свойство перебивает
   только цвет, картинка остаётся страничной.

   СВЕТ ПРИНАДЛЕЖИТ ШАПКЕ СТРАНИЦЫ, А НЕ ЭКРАНУ

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

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

   Обрезка по высоте ничего не съедает: пятно кончается на 44 процентах высоты
   экрана, а коробка слоя ровно в экран. По горизонтали обрезка та же, что была.

   Позиционированного предка у слоя нет, значит absolute считается от начала
   документа, а это ровно то, что нужно. Ставить body position:relative не надо,
   и лучше не надо: это меняет отсчёт всему, что внутри. */
@media (pointer: coarse){
  .blob::before{ filter: none; --bstop: 100% }
  .app::after{ -webkit-mask-image: none; mask-image: none; opacity: .5 }
  /* Сглаживание полосы при смене высоты страницы тут снято. Оно меняет
     background-size, а это перерисовка слоя высотой во всю страницу, кадр за
     кадром больше секунды. На телефоне это совпадает по времени с переключением
     темы в базе знаний, и соседние анимации начинают подрагивать. Сам скачок
     полосы стоит куда меньше, пара единиц яркости. Счёт множителя при тех же
     условиях не запускает и blobs.js, там же разбор. */
  .app::before, .app::after{ transition: none }
  .aurora{ position: absolute; inset: 0 auto auto 0; width: 100%; height: 100vh }
  /* Двойной :root – чтобы перебить страничное правило, оно идёт ниже по
     документу. Тот же приём, что у --glass в grain.css и у .rail в rail.css. */
  :root:root body{ background-color: #10141d }

  .b2, .b3, .b4{ display: none }
  .b1{ width: 160vw; left: -46vw }
  .aurora:not(.home) .b1{ opacity: var(--glow-op, .22) }
  .aurora:not(.home) .b1::before{ background: radial-gradient(closest-side, var(--glow), transparent var(--bstop, 100%)) }

  /* Свет у нижней кромки экрана тут был и отсюда ушёл. Он висел на body::after,
     светил цветом раздела и гас у подвала, то есть работал подсказкой «внизу
     ещё есть страница». Слой приходится класть поверх содержимого, иначе его
     закрывает .app, а сверху он красит всё подряд, включая пустой фон и нижнюю
     строку текста. Ту же мысль пробовали потом перенести на сами плашки, ещё
     тремя способами, и тоже отказались; разбор всех четырёх в grain.css. */
}
