/*
 * Оболочка Mini App: небо, вертикальная игровая рамка, экраны, HUD, навигация,
 * шторка и облачные переходы.
 *
 * Игра PORTRAIT-first (plan_project2.md — канонический документ ориентации,
 * имеющий приоритет над landscape-разделами старого ТЗ). Размеры считаются
 * в долях художественного канваса 1080×1920 через единицу --u, а не в фиксированных
 * пикселях, поэтому интерфейс держит пропорции на 320×568 и на 430×932 одинаково.
 * Экрана «поверни телефон» больше нет: при физическом повороте устройства
 * вертикальная рамка просто остаётся по центру, состояние игры не теряется.
 *
 * Производительность (ТЗ, часть 03, раздел 34): только CSS-трансформы и прозрачность,
 * никаких тяжёлых фильтров и canvas. Всё движение выключается при prefers-reduced-motion.
 */

* { box-sizing: border-box; }

/* Атрибут hidden должен побеждать любые display в правилах ниже.
   Без этого спрятанный блок продолжает занимать место — и на экране висят
   и заглушка, и пустая карточка зоны одновременно. */
[hidden] { display: none !important; }

html, body {
    height: 100%;
    margin: 0;
    overflow: hidden;               /* игра не скроллится: случайный свайп не уводит экран */
    overscroll-behavior: none;
    touch-action: manipulation;     /* без двойного тапа-зума */
}

body {
    font-family: var(--font-text);
    color: var(--cream);
    background: var(--panel-deep);
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;

    /* Фракция ещё не выбрана — нейтральное золото: оно принадлежит войне, а не стороне. */
    --brand: var(--gold);
    --brand-accent: var(--sky);
}

/* ============================================================
   НЕБО И ПАРАЛЛАКС
   Три слоя (ТЗ, раздел 7): дальнее небо, облачная гряда, передние облака.
   Арта пока нет — слои собраны градиентами, и это осознанно:
   геймплей и композиция проверяются до генерации картинок.
   ============================================================ */

.sky {
    position: fixed;
    inset: 0;
    z-index: var(--z-sky);
    pointer-events: none;
    overflow: hidden;
    background:
        radial-gradient(120% 90% at 12% -20%, var(--cats-purple-dark) 0%, transparent 58%),
        radial-gradient(120% 90% at 100% -10%, var(--dogs-blue-dark) 0%, transparent 55%),
        linear-gradient(180deg, #2A2170 0%, var(--panel-deep) 62%, #120E30 100%);
}

/*
 * Тема острова подкрашивает небо (ТЗ, раздел 138). Слой полупрозрачный и лежит
 * поверх градиента, но под облаками: тема меняет настроение, а не структуру неба.
 * Без надетой темы переменная равна `transparent`, и слоя фактически нет.
 */
.sky::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--theme-sky, transparent);
    opacity: 0.35;
    transition: background var(--t-mid) var(--ease);
}

.sky__layer {
    position: absolute;
    inset: -10% -20%;
    background-repeat: repeat-x;
    will-change: transform;
}

/* Дальняя гряда: медленная, приглушённая, создаёт глубину. */
.sky__layer--far {
    background-image:
        radial-gradient(closest-side, rgba(64, 200, 248, 0.16) 0%, transparent 100%),
        radial-gradient(closest-side, rgba(184, 96, 240, 0.14) 0%, transparent 100%);
    background-size: 46% 26%, 34% 20%;
    background-position: 8% 26%, 62% 16%;
    animation: drift-far 90s linear infinite;
}

/* Средний слой: облака, за которыми «прячется» мир. */
.sky__layer--mid {
    background-image:
        radial-gradient(closest-side, rgba(237, 223, 227, 0.12) 0%, transparent 100%),
        radial-gradient(closest-side, rgba(237, 223, 227, 0.09) 0%, transparent 100%);
    background-size: 30% 18%, 22% 14%;
    background-position: 24% 68%, 78% 74%;
    animation: drift-mid 60s linear infinite;
}

/* Передний слой: крупные мягкие облака у самой камеры. */
.sky__layer--near {
    background-image: radial-gradient(closest-side, rgba(237, 223, 227, 0.10) 0%, transparent 100%);
    background-size: 52% 24%;
    background-position: 50% 104%;
    animation: drift-near 40s linear infinite;
}

/*
 * Затемнение неба (им пользуется бой). Раньше это была одна строка
 * `filter: brightness(0.7)` на самом небе — и она стоила бою кадров: фильтр
 * на родителе заставляет браузер перерисовывать ВЕСЬ слой каждый кадр,
 * потому что под ним всегда едут три облачных слоя. Измерено: с фильтром
 * 54.7 FPS и 4.3 % кадров длиннее 33 мс, без него — 60.0 и 0.0 %.
 *
 * Чёрный поверх с прозрачностью 0.3 даёт РОВНО тот же цвет: C×0.7 + 0×0.3 = 0.7C,
 * а это и есть brightness(0.7) — фильтры CSS считаются в sRGB. Чёрный, а не
 * `--ink`: совпадение здесь обязано быть точным, а не похожим.
 * `z-index` — чтобы затемнение легло и на подкраску темой: она тоже небо.
 */
.sky__dim {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: #000;
    opacity: 0;
}

/*
 * Небо сезонного события (ТЗ, часть 09, разделы 279–282).
 *
 * Пустой слой без картинки: путь приезжает от сервера и только у утверждённого
 * ассета, а ставится он в переменную ПОСЛЕ загрузки файла. Поэтому у слоя нет
 * ни своего фона, ни рамки — не доехало, значит небо осталось прежним
 * (раздел 282). Прозрачность неполная намеренно: сезонное небо меняет
 * настроение, а не подменяет мир, — под ним по-прежнему виден канон.
 */
.sky__event {
    position: absolute;
    inset: 0;
    background-image: var(--event-sky, none);
    background-size: cover;
    background-position: center top;
    opacity: 0;
    transition: opacity var(--t-cloud) var(--ease);
}

.sky__event.is-on { opacity: 0.85; }

@keyframes drift-far  { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-6%, 0, 0); } }
@keyframes drift-mid  { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-12%, 0, 0); } }
@keyframes drift-near { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-20%, 0, 0); } }

/* ============================================================
   ИГРОВАЯ РАМКА
   На телефоне это весь экран. На Telegram Desktop и планшете игра НЕ растягивается
   на широкий экран (разделы 139–142): интерактивная часть остаётся вертикальной
   и стоит по центру, а по бокам видно небо и подложку.
   ============================================================ */

/*
    Рамка НЕ ЕЗДИТ.

    Внутри неё намеренно лежит больше, чем видно: слой мира шире картинки
    (`--- ОСТРОВ`, шаг 69), облака уезжают за край, сцены корабля тащатся
    пальцем. При `overflow: hidden` это делает рамку СКРОЛЛ-КОНТЕЙНЕРОМ:
    полос прокрутки не видно, но `scrollHeight` 1161 против 844 — и любой
    `scrollIntoView`, автофокус или якорь уводит её вверх ВМЕСТЕ с шапкой
    и навигацией, которые лежат в ней `position: absolute`. Замерено:
    сдвиг 317 — и HUD уезжает на −309, то есть кошелёк, профиль и все пять
    разделов пропадают с экрана, а вернуть их нечем: прокрутки не видно,
    пальцем её не взять.

    `overflow: clip` обрезает то же самое и при этом НЕ заводит
    скролл-контейнера — уехать рамке физически некуда. Строка `hidden`
    остаётся первой ради движков, которые про `clip` не знают: там же
    работает сетка в `js/app.js`. Прилипшие элементы (`.fitting`,
    `.pass__actions`, `.boss__actions`) не трогаются — их скроллер
    это их собственный экран, а не рамка.
*/
.app {
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: var(--frame-w);
    height: var(--app-h);
    overflow: hidden;
    overflow: clip;
    z-index: var(--z-world);
}

/*
    Подложка вокруг рамки: видна только там, где экран шире игры.

    Картинки здесь НЕТ намеренно. `opacity: 0` прячет её от глаза, но не от
    сети: элемент отрисован, и браузер честно качает фон, которого никто
    не увидит. Инструмент `deploy/perf-probe.py` поймал это числом — 328 КБ
    из 1.4 МБ входа на вьюпорте 390 пикселей, то есть у каждого игрока
    с телефоном. Правило простое: картинка объявляется там же, где она видна.
*/
.backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-sky);
    pointer-events: none;
    background-size: cover;
    background-position: center;
    filter: blur(18px) brightness(0.45) saturate(0.8);
    opacity: 0;
    transition: opacity var(--t-mid) var(--ease);
}

/* Ровно тот случай, ради которого подложка существует. */
@media (min-width: 560px) {
    .backdrop {
        background-image: var(--backdrop, none);
        opacity: 1;
    }
    .app {
        border-left: var(--stroke) solid var(--ink);
        border-right: var(--stroke) solid var(--ink);
        box-shadow: 0 0 calc(var(--u) * 8) rgba(24, 19, 31, 0.75);
    }
}

/* ============================================================
   ЭКРАНЫ
   ============================================================ */

.screen { display: none; }

body[data-screen="boot"]     .screen--boot,
body[data-screen="intro"]    .screen--intro,
body[data-screen="outside"]  .screen--outside,
body[data-screen="error"]    .screen--error,
body[data-screen="faction"]  .screen--faction,
body[data-screen="hub"]      .screen--hub,
body[data-screen="battle"]   .screen--battle,
body[data-screen="heroes"]   .screen--heroes,
body[data-screen="raid"]     .screen--raid,
body[data-screen="defense"]  .screen--defense,
body[data-screen="war"]      .screen--war,
body[data-screen="boss"]     .screen--boss,
body[data-screen="shop"]     .screen--shop,
body[data-screen="pass"]     .screen--pass,
body[data-screen="wardrobe"] .screen--wardrobe,
body[data-screen="friends"]  .screen--friends,
body[data-screen="visit"]    .screen--visit,
body[data-screen="clan"]     .screen--clan,
body[data-screen="airship"]  .screen--airship,
body[data-screen="squad"]    .screen--squad-screen,
body[data-screen="notfound"] .screen--notfound {
    display: flex;
}

.screen {
    position: absolute;
    inset: 0;
    z-index: var(--z-screen);
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: calc(var(--u) * 2.5);
    text-align: center;

    /* UI-safe zone: реальные значения среды, а не выдуманные отступы (разделы 148, 154). */
    padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
    animation: screen-in var(--t-mid) var(--ease) both;
}

/*
 * Экраны с закреплёнными полосами не имеют права заезжать под них: ни одна
 * важная кнопка не должна оказаться под навигацией (разделы 154, 166).
 * Отступ ставится на сам экран, поэтому его наследуют и прокручиваемые списки.
 */
/* Полноэкранная модалка детали героя убирает закреплённый интерфейс целиком. */
body.has-detail .hud,
body.has-detail .nav { display: none; }

body.has-chrome .screen--heroes,
body.has-chrome .screen--raid,
body.has-chrome .screen--defense,
body.has-chrome .screen--squad-screen {
    padding-top: calc(var(--hud-total) + var(--gap-1));
    padding-bottom: calc(var(--nav-total) + var(--gap-1));
}

@keyframes screen-in {
    from { opacity: 0; transform: scale(0.985); }
    to   { opacity: 1; transform: none; }
}

/*
 * Фон-сцена экрана. Путь подставляет js/app.js из реестра утверждённых ассетов —
 * в CSS имён файлов нет, поэтому замена картинки не требует правки вёрстки.
 * `cover` кадрирует по центру: композиция ассетов сделана так, что важное лежит
 * в центральных 70% и обрезка краёв на узком экране ничего не съедает.
 */
.screen.has-scene::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image: var(--scene);
    background-size: cover;
    background-position: center;
    animation: scene-in 0.6s var(--ease) both;
}

/* Затемнение поверх сцены: без него белый текст теряется на светлом небе. */
.screen.has-scene::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(180deg, rgba(26, 20, 64, 0.55) 0%, rgba(26, 20, 64, 0.35) 45%, rgba(18, 14, 48, 0.75) 100%);
}

@keyframes scene-in {
    from { opacity: 0; transform: scale(1.04); }
    to   { opacity: 1; transform: none; }
}

.stack {
    display: grid;
    gap: var(--gap-2);
    justify-items: center;
    max-width: calc(var(--u) * 70);
}

/*
 * Вход по камере. Код лежит на кремовой карточке с толстой обводкой — как
 * любая панель игры, — но БЕЗ скруглений на самом коде и без картинки под
 * ним: сканеру нужен контраст и поле вокруг, а не оформление поверх модулей.
 * Поле (quiet zone) уже вшито в сам SVG, отступ карточки идёт сверх него.
 */
.outside-qr {
    display: grid;
    justify-items: center;
    gap: var(--gap-1);
    padding: calc(var(--u) * 1.8);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-md);
    background: var(--cream);
    box-shadow: 0 6px 0 var(--ink-shadow);
}

.outside-qr__code {
    display: block;
    width: max(150px, calc(var(--u) * 26));
    height: auto;
    image-rendering: pixelated;
}

.outside-qr__hint {
    margin: 0;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: max(11px, calc(var(--u) * 1.5));
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* ============================================================
   ЗАГРУЗОЧНЫЙ ЭКРАН
   Картинка владельца во весь кадр, логотип поверх, полоса с настоящим
   прогрессом внизу. Версия ровно одна — вертикальная (раздел 91):
   горизонтальная ушла в подложку вокруг рамки на широких экранах.
   ============================================================ */

/* Затемнение у загрузочного экрана мягче общего: картинка тут — главный объект,
   а не подложка под текст. Тёмное только внизу, под полосой. */
.screen--boot.has-scene::after {
    background: linear-gradient(180deg, rgba(26, 20, 64, 0.1) 0%, rgba(26, 20, 64, 0.12) 52%, rgba(18, 14, 48, 0.82) 100%);
}

/* Логотип живёт в верхней половине кадра, полоса — внизу, где затемнение сильнее
   и текст читается по-настоящему. Посередине у картинки самые плотные детали. */
.boot {
    display: grid;
    grid-template-rows: 1fr auto;
    align-content: center;
    justify-items: center;
    gap: var(--gap-3);
    width: min(calc(var(--u) * 96), 100%);
    height: 100%;
    padding-bottom: calc(var(--u) * 4);
}

.boot__progress {
    display: grid;
    justify-items: center;
    gap: var(--gap-1);
    width: 100%;
}

.boot__logo {
    align-self: center;
    width: min(calc(var(--u) * 78), 78%);
    height: auto;
    filter: drop-shadow(0 calc(var(--u) * 0.8) 0 var(--ink-shadow));
    animation: boot-logo-in 0.7s var(--ease) both;
}

@keyframes boot-logo-in {
    from { opacity: 0; transform: scale(0.9) translateY(calc(var(--u) * 2)); }
    to   { opacity: 1; transform: none; }
}

/* Полоса загрузки: та же «пухлая» геометрия, что у кнопок — толстая обводка,
   крупное скругление, внутренний блик. */
.boot__bar {
    position: relative;
    display: grid;
    align-items: center;
    width: 100%;
    height: max(28px, calc(var(--u) * 3.4));
    padding: 3px;
    border: var(--stroke) solid var(--ink);
    border-radius: 999px;
    background: rgba(24, 19, 31, 0.72);
    box-shadow: 0 4px 0 var(--ink-shadow), inset 0 2px 0 rgba(237, 223, 227, 0.12);
    overflow: hidden;
}

.boot__fill {
    height: 100%;
    width: 0;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--cats-main) 0%, var(--gold) 52%, var(--sky) 100%);
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.35);
    /* Ширину двигает только реальный прогресс; переход лишь сглаживает рывки. */
    transition: width 0.3s var(--ease);
}

.boot__pct {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    margin: 0;
    font-family: var(--font-display);
    font-size: max(12px, calc(var(--u) * 1.7));
    letter-spacing: 1px;
    color: var(--cream);
    text-shadow: 2px 2px 0 var(--ink);
}

.boot__status {
    margin: 0;
    font-family: var(--font-text);
    font-weight: 700;
    font-size: max(13px, calc(var(--u) * 1.8));
    color: var(--cream);
    text-shadow: 2px 2px 0 var(--ink);
}

/* На очень низкой рамке (клавиатура, свёрнутый Telegram) логотип съедает всю высоту. */
@media (max-height: 520px) {
    .boot { gap: var(--gap-2); }
    .boot__logo { width: min(calc(var(--u) * 44), 46%); }
}

/* ============================================================
   ТИПОГРАФИКА
   Размеры в долях канваса, но не ниже читаемого минимума в пикселях.
   ============================================================ */

.logo {
    margin: 0;
    font-family: var(--font-display);
    font-size: max(38px, calc(var(--u) * 7.6));
    line-height: 0.95;
    text-transform: uppercase;
    transform: skewX(-6deg);
    text-shadow: 5px 5px 0 var(--ink);
}

.logo__cats { color: var(--cats-neon); }
.logo__dogs { color: var(--sky); }

.logo__sub {
    margin: 0;
    font-family: var(--font-display);
    font-size: max(13px, calc(var(--u) * 1.9));
    letter-spacing: 6px;
    color: var(--gold);
    text-shadow: 2px 2px 0 var(--ink);
}

.title {
    margin: 0;
    font-family: var(--font-display);
    font-size: max(24px, calc(var(--u) * 4.2));
    line-height: 1.05;
    text-transform: uppercase;
    text-shadow: 4px 4px 0 var(--ink);
}

.title--tight { font-size: max(20px, calc(var(--u) * 3.2)); }

.eyebrow {
    margin: 0;
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--brand);
}

.lead {
    margin: 0;
    font-size: max(14px, calc(var(--u) * 1.75));
    line-height: 1.5;
    font-weight: 600;
    color: rgba(237, 223, 227, 0.86);
}

.footnote {
    margin: 0;
    font-size: max(12px, calc(var(--u) * 1.4));
    color: rgba(237, 223, 227, 0.55);
}

/* ============================================================
   КНОПКИ: пухлые, с толстой обводкой и внутренним бликом
   ============================================================ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: max(var(--tap-target), calc(var(--u) * 6));
    padding: 0 calc(var(--u) * 3.5);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius);
    font-family: var(--font-text);
    font-size: max(15px, calc(var(--u) * 1.8));
    font-weight: 900;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--ink);
    background: var(--gold);
    box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.45), 0 5px 0 var(--ink-shadow);
    cursor: pointer;
    transition: transform var(--t-fast) var(--ease), filter var(--t-fast) var(--ease);
}

.btn:active { transform: translateY(3px); box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.3); }
.btn:disabled { opacity: 0.55; cursor: default; transform: none; }

.btn--gold { background: var(--gold); }

/* Второстепенная кнопка. Класс использовался в вёрстке с этапа 5, но описан не был:
   «Выйти», «Закрыть» и «На остров» кричали золотом наравне с главным действием. */
.btn--ghost {
    background: rgba(24, 19, 31, 0.5);
    color: var(--cream);
    box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.14), 0 5px 0 var(--ink-shadow);
}

.btn--side {
    width: 100%;
    color: var(--cream);
    background: var(--side);
}

/* Драка — не главное действие острова, но и не сноска: красная кнопка
   рядом с уходом, обе под палец. Живёт среди кнопок, а не в главе врага:
   модификатор `btn--` обязан работать всюду, где стоит кнопка, — иначе
   первое же второе применение выйдет бесцветным и никто не поймёт почему. */
.btn--danger {
    color: var(--cream);
    background: var(--danger);
}

/* ============================================================
   ВЫБОР СТОРОНЫ
   ============================================================ */

/*
 * Portrait-композиция выбора стороны (разделы 87–90): коты занимают верхнюю
 * половину кадра, псы — нижнюю, разделённые диагональю. Экран не делится
 * на левую и правую колонки — в вертикальном кадре это две узкие полоски,
 * в которых не читается ни имя стороны, ни её характер.
 */
.faction-head {
    position: absolute;
    top: var(--safe-top);
    left: var(--safe-left);
    right: var(--safe-right);
    z-index: 2;
    display: grid;
    gap: 4px;
    pointer-events: none;
    text-shadow: 0 2px 8px rgba(18, 14, 48, 0.9);
}

.factions {
    display: grid;
    grid-template-rows: repeat(2, minmax(0, 1fr));
    gap: calc(var(--u) * 1.2);
    width: 100%;
    height: 100%;
    padding-top: calc(var(--u) * 12);
}

/*
 * Половина — это и есть кнопка (раздел 90): нажимается вся зона целиком,
 * маленьких кнопок «CATS» и «DOGS» в вертикальном кадре быть не должно.
 * Диагональ даёт ощущение VS, оставаясь вертикальной композицией (раздел 89).
 */
/*
 * Половина прозрачна сверху и затемнена снизу: ключевой арт нарисован диагональю,
 * лица героев стоят в верхней части каждой половины, и заливать их панелью — значит
 * выбросить то, ради чего арт рисовался. Текст живёт внизу половины, на скриме.
 */
.faction {
    position: relative;
    display: grid;
    align-content: end;
    gap: 4px;
    padding: calc(var(--u) * 2.4);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-lg);
    background: linear-gradient(180deg,
        rgba(24, 19, 31, 0) 0%,
        color-mix(in srgb, var(--side) 30%, rgba(24, 19, 31, 0.55)) 48%,
        color-mix(in srgb, var(--side) 42%, rgba(18, 14, 48, 0.92)) 100%);
    box-shadow: 0 6px 0 var(--ink-shadow);
    text-align: left;
    cursor: pointer;
    transition: transform var(--t-fast) var(--ease);
}

.faction:active { transform: scale(0.985); }
.faction.is-busy { opacity: 0.6; }

.faction__name {
    margin: 0;
    font-family: var(--font-display);
    font-size: max(34px, calc(var(--u) * 8));
    line-height: 1;
    text-transform: uppercase;
    transform: skewX(-6deg);
    color: var(--side-accent);
    text-shadow: 4px 4px 0 var(--ink);
}

.faction__title {
    margin: 0;
    font-size: max(12px, calc(var(--u) * 1.5));
    font-weight: 800;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.75);
}

.faction__text {
    margin: 0;
    font-size: max(14px, calc(var(--u) * 1.8));
    line-height: 1.4;
    font-weight: 600;
}

/* CTA внутри половины: не кнопка выбора, а подпись «нажми сюда». */
.faction__cta {
    margin-top: 4px;
    font-family: var(--font-display);
    font-size: max(15px, calc(var(--u) * 2));
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--gold);
    text-shadow: 2px 2px 0 var(--ink);
}

/*
 * Подкрепление меньшей стороне (ТЗ, часть 02, раздел 247). Плашка стоит над
 * названием половины и отличается ФОРМОЙ — скруглённая таблетка на золоте
 * среди прямых панелей, — а не только цветом: цвет здесь принадлежит стороне
 * и на роль отличия не годится.
 *
 * Соотношения сил на ней нет (раздел 245): написано, что дают, а не почему.
 */
.faction__bonus {
    justify-self: start;
    margin: 0 0 2px;
    padding: 5px 10px;
    border: 2px solid var(--ink);
    /*
     * Не таблетка: на 320×568 состав форы не помещается в строку, а таблетка
     * в две строки читается как ошибка вёрстки. Прямоугольник со скруглением
     * панели переносится спокойно.
     */
    border-radius: var(--radius-sm);
    background: var(--gold);
    box-shadow: 0 3px 0 var(--ink-shadow);
    color: var(--ink);
    font-size: max(12px, calc(var(--u) * 1.45));
    font-weight: 900;
    letter-spacing: 0.5px;
    text-shadow: none;
}

/* Обещание сдержано: та же половина говорит, что подкрепление пришло. */
.faction.is-paid .faction__cta { color: var(--cream); }

.notice {
    margin: 0;
    padding: 12px var(--gap-2);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    font-weight: 800;
}

.notice--error { background: var(--danger); color: var(--cream); }

/* ============================================================
   ВЕРХНИЙ HUD
   Компактный (раздел 11): аватар с уровнем, мягкая и премиальная валюта.
   Двух строк валют в шапке быть не должно — редкие валюты живут в шторке.
   ============================================================ */

.hud {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--z-hud);
    display: flex;
    align-items: center;
    gap: var(--gap-1);
    height: var(--hud-total);
    padding: var(--safe-top) var(--safe-right) 0 var(--safe-left);
    pointer-events: none;
}

.hud > * { pointer-events: auto; }

.hud__id,
.hud__wallet {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 4px 10px 4px 4px;
    border: var(--stroke) solid var(--ink);
    border-radius: 999px;
    background: linear-gradient(180deg, rgba(36, 29, 87, 0.94), rgba(18, 14, 48, 0.94));
    color: var(--cream);
    font: inherit;
    cursor: pointer;
}

.hud__id { min-width: 0; flex: 1 1 auto; text-align: left; }
.hud__wallet { flex: 0 0 auto; padding: 4px 12px; gap: 12px; }

.hud__names { display: grid; gap: 1px; min-width: 0; }

.hud__name {
    font-family: var(--font-display);
    font-size: max(14px, calc(var(--u) * 2));
    line-height: 1.1;
    letter-spacing: 0.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hud__sub {
    font-size: max(10px, calc(var(--u) * 1.3));
    font-weight: 700;
    color: rgba(237, 223, 227, 0.66);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hud__coin {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font-display);
    font-size: max(13px, calc(var(--u) * 1.8));
    color: var(--cream);
    text-shadow: 2px 2px 0 var(--ink);
}

/* Валюта различается формой и цветом сразу: монета — круг золотом,
   кристалл — ромб цианом. Только цветом информацию не передаём. */
.hud__coin i {
    width: max(14px, calc(var(--u) * 1.9));
    height: max(14px, calc(var(--u) * 1.9));
    border: 2px solid var(--ink);
    background: var(--gold);
    border-radius: 50%;
}

.hud__coin[data-currency="GEMS"] i {
    background: var(--sky);
    border-radius: 3px;
    transform: rotate(45deg);
}

.crest {
    display: grid;
    place-items: center;
    width: max(38px, calc(var(--u) * 5));
    height: max(38px, calc(var(--u) * 5));
    flex: 0 0 auto;
    border: var(--stroke) solid var(--ink);
    border-radius: 50%;
    background: var(--brand);
    color: var(--ink);
    font-family: var(--font-display);
    font-size: max(17px, calc(var(--u) * 2.4));
}

/* ============================================================
   НИЖНЯЯ НАВИГАЦИЯ
   Пять разделов, центральный крупнее (разделы 8–10). Половину полосы
   он не занимает: «БОЙ» — главное действие, а не единственное.
   ============================================================ */

.nav {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-hud);
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    align-items: end;
    gap: 2px;
    height: var(--nav-total);
    padding: 0 var(--safe-right) var(--safe-bottom) var(--safe-left);
    background: linear-gradient(180deg, rgba(18, 14, 48, 0) 0%, rgba(18, 14, 48, 0.92) 34%, rgba(18, 14, 48, 0.98) 100%);
}

.nav__item {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: 3px;
    min-height: 48px;
    height: var(--bottom-nav-h);
    padding: 0 2px;
    border: 0;
    background: none;
    color: rgba(237, 223, 227, 0.6);
    font: inherit;
    cursor: pointer;
}

.nav__item[data-active] { color: var(--gold); }

.nav__label {
    font-family: var(--font-display);
    font-size: max(10px, calc(var(--u) * 1.3));
    letter-spacing: 0.6px;
    text-transform: uppercase;
    /* На 320px «ФРОНТ» разрывался на две строки и полоса прыгала по высоте. */
    white-space: nowrap;
}

.nav__icon {
    width: max(22px, calc(var(--u) * 3));
    height: max(22px, calc(var(--u) * 3));
    border: 2px solid currentColor;
}

/* Иконки — формы, а не картинки: остров трапецией, герои кругом,
   фронт ромбом, «ещё» полосами. Формы разные, поэтому раздел различим
   и без цвета, и при дальтонизме. */
.nav__icon--island  { border-radius: 40% 40% 12% 12%; }
.nav__icon--heroes  { border-radius: 50%; }
.nav__icon--world   { border-radius: 4px; transform: rotate(45deg); }
.nav__icon--more    { border-radius: 4px; border-width: 2px 2px 8px; }
.nav__icon--battle  { border-radius: 6px; transform: rotate(45deg); }

/* Центральная кнопка боя: приподнята над полосой, но не занимает её половину. */
.nav__item--main {
    align-self: end;
    margin-bottom: 2px;
}

.nav__item--main .nav__icon {
    width: max(44px, calc(var(--u) * 6));
    height: max(44px, calc(var(--u) * 6));
    border: var(--stroke) solid var(--ink);
    border-radius: 50%;
    background: var(--gold);
    box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.45), 0 4px 0 var(--ink-shadow);
    transform: none;
}

.nav__item--main .nav__label { color: var(--gold); }
.nav__item--main:active .nav__icon { transform: translateY(3px); }

.nav__item[data-locked] { opacity: 0.55; }

/* ============================================================
   НИЖНЯЯ ШТОРКА
   Быстрые действия в portrait живут внизу, под большим пальцем (раздел 117),
   а не в модалке на пол-экрана.
   ============================================================ */

.sheet {
    position: absolute;
    inset: 0;
    z-index: calc(var(--z-hud) + 5);
    display: flex;
    align-items: flex-end;
}

.sheet__scrim {
    position: absolute;
    inset: 0;
    background: rgba(18, 14, 48, 0.62);
}

.sheet__panel {
    position: relative;
    display: grid;
    gap: var(--gap-2);
    width: 100%;
    max-height: 76%;
    overflow-y: auto;
    padding: var(--gap-1) var(--gap-2) calc(var(--safe-bottom) + var(--gap-2));
    border: var(--stroke) solid var(--ink);
    border-bottom: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: linear-gradient(180deg, rgba(36, 29, 87, 0.98), rgba(18, 14, 48, 0.99));
    box-shadow: 0 -8px 0 var(--ink-shadow);
    text-align: left;
    animation: sheet-in var(--t-mid) var(--ease) both;
}

@keyframes sheet-in {
    from { transform: translateY(24%); opacity: 0; }
    to   { transform: none; opacity: 1; }
}

.sheet__grip {
    justify-self: center;
    width: calc(var(--u) * 12);
    height: 5px;
    border-radius: 999px;
    background: rgba(237, 223, 227, 0.35);
}

.sheet__body { display: grid; gap: var(--gap-1); }

/* Строка кошелька и пункт меню — одна и та же геометрия. */
.sheet__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-1);
    min-height: 48px;
    padding: 0 var(--gap-2);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
    color: var(--cream);
    font: inherit;
    text-align: left;
}

button.sheet__row { cursor: pointer; }
button.sheet__row:active { transform: translateY(2px); }

.sheet__label {
    font-size: max(11px, calc(var(--u) * 1.4));
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.7);
}

.sheet__value {
    font-family: var(--font-display);
    font-size: max(17px, calc(var(--u) * 2.2));
    color: var(--gold);
    text-shadow: 2px 2px 0 var(--ink);
}

/*
 * ПРИКАЗЫ ШТАБА (ТЗ, часть 01, разделы 121–122).
 *
 * Строка задания живёт в той же шторке, что кошелёк и звук: своего экрана
 * у трёх целей нет. Состояние различается ФОРМОЙ — счёт, полоса и подпись
 * действия, — а не одной заливкой: цвет здесь только подчёркивает готовое.
 * Ни одной бесконечной анимации: полоса стоит, пока её не двинет новый ответ.
 */

.quest {
    display: grid;
    gap: calc(var(--u) * 0.8);
    padding: var(--gap-1) var(--gap-2);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
}

.quest[data-state="COMPLETED"] { border-color: var(--gold); }
.quest[data-state="CLAIMED"] { opacity: 0.62; }

.quest__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--gap-1);
}

.quest__title {
    font-size: max(13px, calc(var(--u) * 1.7));
    font-weight: 800;
    color: var(--cream);
}

.quest__count {
    font-family: var(--font-display);
    font-size: max(12px, calc(var(--u) * 1.5));
    color: var(--gold);
    white-space: nowrap;
}

.quest__bar {
    height: max(8px, calc(var(--u) * 0.9));
    border: 2px solid var(--ink);
    border-radius: 999px;
    background: rgba(24, 19, 31, 0.6);
    overflow: hidden;
}

.quest__bar span {
    display: block;
    height: 100%;
    background: var(--gold);
}

.quest__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-1);
}

.quest__prize,
.quest__done {
    font-size: max(11px, calc(var(--u) * 1.4));
    color: rgba(237, 223, 227, 0.72);
}

.quest__window {
    margin: calc(var(--u) * 0.6) 0 0;
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.6);
}

/*
 * ЕЖЕДНЕВНЫЙ ВХОД (ТЗ, часть 27, разделы 177–181).
 *
 * Ящик у причала и календарь в шторке. Ящик — предмет ОСТРОВА, поэтому стоит
 * в холсте мира и меряется в долях картинки; календарь — обычная шторка,
 * своего экрана у него нет (раздел 178 запрещает гигантский календарь первым
 * делом).
 *
 * Полный и открытый ящик различаются ФОРМОЙ — крышка на месте или откинута
 * набок, — а не заливкой: разница цвета на траве не читается ни на солнце,
 * ни у того, кто цвет не различает.
 */

.daily-crate {
    position: absolute;
    z-index: var(--z-world);
    transform: translate(-50%, -100%);
    /* Не меньше пальца. Доли кадра на самом тесном телефоне дают 25 точек,
       а нажимать по игре обязано быть чем: 44 — та же мерка, которой меряет
       маркеры зон `deploy/island-geometry-probe.py`. */
    width: max(44px, calc(var(--u) * 11));
    height: max(34px, calc(var(--u) * 8.5));
    padding: 0;
    border: var(--stroke) solid var(--ink);
    border-radius: 3px;
    /* Доска поперёк ящика: та же форма, что у ящиков с борта, — груз в этой
       игре выглядит одинаково, откуда бы он ни пришёл. */
    background:
        linear-gradient(180deg, transparent 0 44%, var(--ink) 44% 52%, transparent 52% 100%),
        linear-gradient(180deg, var(--dogs-orange) 0%, #8A4A10 100%);
    box-shadow: 0 3px 0 var(--ink-shadow);
    cursor: pointer;
}

/*
 * Пол под палец у ящика держала одна сторона из двух: ширина 44, высота 34
 * (шаг 146). Ящик поперёк себя шире по рисунку, и тянуть его вверх значило бы
 * рисовать не ящик, — поэтому запас берёт прозрачный `::after`, поровну сверху
 * и снизу: доска на крышке (`::before`) и так висит выше самого ящика.
 */
.daily-crate::after {
    content: '';
    position: absolute;
    inset: -5px 0;
}

/* Крышка. У полного она лежит ровно и золотая — это её и видно издалека;
   у открытого откинута набок и потеряла цвет: брать больше нечего. */
.daily-crate::before {
    content: '';
    position: absolute;
    left: 50%;
    top: calc(var(--u) * -1.4);
    width: calc(100% + var(--u) * 0.8);
    height: max(7px, calc(var(--u) * 1.8));
    border: var(--stroke) solid var(--ink);
    border-radius: 3px;
    background: var(--gold);
    transform: translateX(-50%);
}

.daily-crate[data-state="OPEN"]::before {
    background: #8A4A10;
    transform: translateX(-64%) rotate(-24deg);
}

.daily-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: calc(var(--u) * 0.8);
    margin: var(--gap-1) 0;
}

.daily-day {
    display: grid;
    gap: calc(var(--u) * 0.3);
    padding: calc(var(--u) * 0.9) calc(var(--u) * 0.5);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
    text-align: center;
}

/* Пройденный день — приглушённый и с галочкой в номере; сегодняшний —
   в золотой обводке. Форма, а не только цвет. */
.daily-day[data-state="DONE"] { opacity: 0.55; }
.daily-day[data-state="DONE"] .daily-day__num::after { content: ' ✓'; }
.daily-day[data-state="TODAY"] { border-color: var(--gold); }

.daily-day__num {
    font-family: var(--font-display);
    font-size: max(12px, calc(var(--u) * 1.5));
    color: var(--gold);
}

.daily-day__prize {
    font-size: max(9px, calc(var(--u) * 1.1));
    line-height: 1.2;
    color: rgba(237, 223, 227, 0.82);
}

.daily-streak {
    margin: 0 0 var(--gap-1);
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.6);
}

/*
 * ОБЗОР БАЗЫ (ТЗ, часть 08, раздел 33; этап 41, шаг 99).
 *
 * Строка построенного здания: имя, уровень, что с ним сейчас происходит
 * и единственное действие. Состояние различается ФОРМОЙ — подписью и
 * наличием кнопки, — а обводка только подчёркивает готовое к завершению.
 * Бесконечных анимаций нет ни одной: строка меняется от ответа сервера.
 */

.base {
    display: grid;
    gap: calc(var(--u) * 0.8);
    padding: var(--gap-1) var(--gap-2);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
}

.base[data-state="READY"] { border-color: var(--gold); }
.base[data-state="WORKING"] { opacity: 0.72; }

.base__head,
.base__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-1);
}

.base__title {
    font-size: max(13px, calc(var(--u) * 1.7));
    font-weight: 800;
    color: var(--cream);
}

.base__level {
    font-family: var(--font-display);
    font-size: max(12px, calc(var(--u) * 1.5));
    color: var(--gold);
    white-space: nowrap;
}

.base__note {
    font-size: max(11px, calc(var(--u) * 1.4));
    color: rgba(237, 223, 227, 0.72);
}

/* Что даст следующий уровень. Своей строкой и своим весом: цена стоит
   на кнопке рядом, и без прибавки строка обзора называет только расход. */
.base__gain {
    font-size: max(11px, calc(var(--u) * 1.35));
    font-weight: 800;
    color: var(--gold);
}

.base__gain[hidden] { display: none; }

.base__lead {
    margin: calc(var(--u) * 0.6) 0 0;
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.6);
}

/*
 * СТРАНИЦА ИГРОКА (ТЗ, часть 02, разделы 233–234).
 *
 * Единственное место, где видно НАДЕТОЕ на самом игроке: рамка — цвет обводки
 * герба, титул — строка под именем. Оба цвета приходят вещью из гардероба,
 * поэтому в правилах ниже их нет: есть переменная и умолчание.
 *
 * Ни одной бесконечной анимации: карточка стоит.
 */

.profile__head {
    display: flex;
    align-items: center;
    gap: var(--gap-2);
}

.profile__crest {
    width: max(56px, calc(var(--u) * 7));
    height: max(56px, calc(var(--u) * 7));
    font-size: max(24px, calc(var(--u) * 3.2));
    /* Надетая рамка. Не надета — обводка остаётся обычной, чернильной. */
    border-color: var(--profile-frame, var(--ink));
    box-shadow: inset 0 0 0 2px rgba(24, 19, 31, 0.55);
}

.profile__names {
    display: grid;
    gap: calc(var(--u) * 0.3);
    min-width: 0;
}

.profile__name {
    font-family: var(--font-display);
    font-size: max(19px, calc(var(--u) * 2.6));
    color: var(--cream);
    text-shadow: 2px 2px 0 var(--ink);
}

.profile__title {
    justify-self: start;
    padding: calc(var(--u) * 0.2) calc(var(--u) * 0.9);
    border: 2px solid var(--ink);
    border-radius: 999px;
    background: var(--gold);
    color: var(--ink);
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}

.profile__sub {
    font-size: max(11px, calc(var(--u) * 1.4));
    color: rgba(237, 223, 227, 0.72);
}

.profile__bar {
    height: max(8px, calc(var(--u) * 0.9));
    border: 2px solid var(--ink);
    border-radius: 999px;
    background: rgba(24, 19, 31, 0.6);
    overflow: hidden;
}

.profile__bar span {
    display: block;
    height: 100%;
    background: var(--gold);
}

.profile__section {
    margin: calc(var(--u) * 1.2) 0 0;
    font-size: max(11px, calc(var(--u) * 1.3));
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: rgba(237, 223, 227, 0.6);
}

.profile__hero,
.profile__medal {
    display: flex;
    align-items: center;
    gap: var(--gap-1);
    padding: var(--gap-1);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
}

.profile__face {
    display: grid;
    place-items: center;
    width: max(44px, calc(var(--u) * 5.6));
    height: max(44px, calc(var(--u) * 5.6));
    flex: 0 0 auto;
    border: 2px solid var(--ink);
    border-radius: var(--radius-sm);
    background: var(--brand) center / cover no-repeat;
    font-family: var(--font-display);
    font-size: max(17px, calc(var(--u) * 2.2));
    color: var(--ink);
}

.profile__about,
.profile__medal > div {
    display: grid;
    gap: calc(var(--u) * 0.2);
    min-width: 0;
}

/*
 * Значок достижения — ФОРМА, а не цвет (docs/ACCESSIBILITY.md): у каждого
 * рода вехи свой силуэт, тот же словарь, что на полке памяти острова.
 */
.profile__medal::before {
    content: "";
    width: max(22px, calc(var(--u) * 2.6));
    height: max(22px, calc(var(--u) * 2.6));
    flex: 0 0 auto;
    background: var(--gold);
    clip-path: polygon(50% 0, 100% 35%, 82% 100%, 18% 100%, 0 35%);
}

.profile__medal[data-icon="medal"]::before { clip-path: circle(46% at 50% 42%); }
.profile__medal[data-icon="banner"]::before { clip-path: polygon(12% 0, 88% 0, 88% 100%, 50% 74%, 12% 100%); }
.profile__medal[data-icon="plate"]::before { clip-path: inset(18% 6% 18% 6% round 3px); }
.profile__medal[data-icon="crate"]::before { clip-path: polygon(8% 22%, 50% 4%, 92% 22%, 92% 84%, 8% 84%); }
.profile__medal[data-icon="oddity"]::before { clip-path: polygon(50% 2%, 78% 22%, 96% 62%, 62% 96%, 22% 88%, 4% 44%); }
.profile__medal[data-icon="hook"]::before { clip-path: polygon(38% 0, 62% 0, 62% 62%, 100% 62%, 100% 88%, 38% 88%); }
.profile__medal[data-icon="stake"]::before { clip-path: polygon(34% 0, 66% 0, 66% 76%, 50% 100%, 34% 76%); }

/*
 * ЖУРНАЛ БОЁВ
 * Строка журнала живёт по тем же правилам, что медаль профиля: своя разметка
 * ей не нужна, нужна своя ФОРМА исхода. Победа — стрела вверх, поражение —
 * вниз, ничья — полоса; слово рядом написано тем же исходом, поэтому журнал
 * читается и на чёрно-белом экране, и с выключенными картинками.
 */
.log__row {
    display: flex;
    align-items: center;
    gap: var(--gap-1);
    padding: var(--gap-1);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
}

.log__row > div {
    display: grid;
    gap: calc(var(--u) * 0.2);
    min-width: 0;
    flex: 1 1 auto;
}

/* Имя соперника приходит от игрока и бывает длинным: строка обязана остаться
   строкой, иначе исход и рейтинг уезжают на второй этаж. */
.log__row > div > * {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.log__row::before {
    content: "";
    width: max(18px, calc(var(--u) * 2.2));
    height: max(18px, calc(var(--u) * 2.2));
    flex: 0 0 auto;
    background: var(--cream);
    clip-path: inset(42% 6% 42% 6%);
}

.log__row[data-outcome="WIN"]::before {
    background: var(--gold);
    clip-path: polygon(50% 4%, 100% 54%, 72% 54%, 72% 96%, 28% 96%, 28% 54%, 0 54%);
}

.log__row[data-outcome="LOSS"]::before {
    background: var(--danger);
    clip-path: polygon(50% 96%, 0 46%, 28% 46%, 28% 4%, 72% 4%, 72% 46%, 100% 46%);
}

.log__delta {
    font-family: var(--font-display);
    font-size: max(15px, calc(var(--u) * 1.9));
    color: var(--cream);
}

/*
 * ТАБЛИЦА РЕЙТИНГА (ТЗ, часть 02, разделы 155–157)
 *
 * Стили лежат в ядре, а не в `css/screens/`, и это не забывчивость: таблица —
 * не раздел, а подробность нижней шторки, как журнал боёв и медали профиля.
 * Дверь в неё стоит внутри карточки игрока, которую строит ядро, — модуль
 * ради сорока строк означал бы `import()` под нажатием на список.
 */
.board__chips {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--u) * 0.6);
    margin-bottom: var(--gap-1);
}

.board__chip {
    padding: calc(var(--u) * 0.6) calc(var(--u) * 1.2);
    border: 2px solid var(--ink);
    border-radius: 999px;
    background: rgba(24, 19, 31, 0.45);
    color: var(--cream);
    font-family: var(--font-display);
    font-size: max(12px, calc(var(--u) * 1.4));
}

/* Выбранный отличается не только цветом: у него своя рамка и свой вес. */
.board__chip[data-on="1"] {
    background: var(--gold);
    border-width: var(--stroke);
    color: var(--ink);
}

.board__row {
    display: flex;
    align-items: center;
    gap: var(--gap-1);
    padding: calc(var(--u) * 0.7) var(--gap-1);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: rgba(24, 19, 31, 0.45);
}

/* Своя строка — толще рамкой и с меткой слева: цвет тут не единственный
   различитель (docs/ACCESSIBILITY.md). */
.board__row[data-me="1"] {
    background: rgba(253, 197, 1, 0.16);
    box-shadow: inset 4px 0 0 var(--gold);
}

.board__place {
    min-width: max(24px, calc(var(--u) * 3));
    font-family: var(--font-display);
    font-size: max(14px, calc(var(--u) * 1.8));
    color: rgba(237, 223, 227, 0.7);
    text-align: right;
}

.board__crest {
    display: grid;
    place-items: center;
    width: max(22px, calc(var(--u) * 2.6));
    height: max(22px, calc(var(--u) * 2.6));
    flex: 0 0 auto;
    border: 2px solid var(--ink);
    background: var(--cats-main);
    color: var(--ink);
    font-family: var(--font-display);
    font-size: max(11px, calc(var(--u) * 1.3));
    /* Сторона различается ФОРМОЙ: у котов круг, у псов щит. */
    border-radius: 50%;
}

.board__crest[data-faction="DOGS"] {
    background: var(--dogs-main);
    border-radius: 30% 30% 45% 45%;
}

/*
 * ЗНАК КЛАНА В СПИСКЕ (доска кланов, ТЗ часть 01 раздел 94).
 *
 * Форма берётся из тех же `[data-emblem]`, что и знамя у штаба, — здесь
 * только размер и заливка. Обводка не border: clip-path срезает его вместе
 * с фоном, поэтому сам элемент это чернильный силуэт, а заливка — ::before
 * на два пикселя меньше с тем же клипом (приём статусов боя и знаков клана
 * в разделе). Наружу подложку выносить нельзя: clip-path создаёт контекст
 * наложения, и ::before с отрицательным z-index лёг бы ПОВЕРХ фона.
 */
.board__emblem {
    position: relative;
    display: block;
    flex: 0 0 auto;
    width: max(22px, calc(var(--u) * 2.6));
    height: max(22px, calc(var(--u) * 2.6));
    background: var(--ink);
}

.board__emblem::before {
    content: "";
    position: absolute;
    inset: 2px;
    background: var(--cream);
    clip-path: inherit;
}

.board__row[data-me="1"] .board__emblem::before {
    background: var(--gold);
}

.board__name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Тег клана — то, чем его зовут вслух: он впереди имени и золотом. */
.board__tag {
    margin-right: var(--gap-1);
    font-family: var(--font-display);
    color: var(--gold);
}

.board__value {
    font-family: var(--font-display);
    font-size: max(14px, calc(var(--u) * 1.8));
    color: var(--gold);
}

/* Разрыв между верхом списка и своей строкой: между ними могут быть сотни. */
.board__gap {
    margin: 0;
    text-align: center;
    color: rgba(237, 223, 227, 0.5);
    letter-spacing: 4px;
}

/* ============================================================
   ОСТРОВ КАК ЭКРАН
   ============================================================ */

.screen--hub { justify-content: flex-start; gap: 0; padding: 0; }

.hub-body {
    display: grid;
    gap: var(--gap-2);
    place-content: center;
    flex: 1;
    padding: var(--hud-total) var(--gap-2) var(--nav-total);
}

/* ============================================================
   ЗНАК МАСТЕРСТВА (этап 41, шаги 105–106; ТЗ ч.01 раздел 153)

   Ступень читается ЧИСЛОМ шевронов, а не заливкой: цветом на карточке уже
   говорит редкость, и второе значение той же краской — ровно то, что запрещает
   правило доступности (`docs/ACCESSIBILITY.md`). Третья ступень вдобавок
   берётся в рамку — форма, а не оттенок.

   Лежит в ЯДРЕ, потому что знак показывают ДВА раздела: карточка героя
   (`css/screens/heroes.css`) и полоса момента на экране итога боя
   (`css/screens/battle.css`). Второй копии словаря в проекте быть не должно —
   то же решение, что у знака способности ниже и у знаков клана (шаг 89).
   Размещение остаётся в разделах: где именно висит знак, знает его экран.

   Ни одной анимации: знак висит на карточке, которая рисуется списком.
   ============================================================ */

.hero-mark {
    display: inline-flex;
    gap: 2px;
    align-items: center;
    padding: 2px 5px;
    border: 2px solid var(--ink);
    border-radius: 999px;
    background: var(--panel);
    line-height: 0;
}

.hero-mark i {
    display: block;
    width: 7px;
    height: 7px;
    background: var(--cream);
    /* Шеврон: он не похож ни на звезду прокачки, ни на значок редкости. */
    clip-path: polygon(50% 0, 100% 55%, 100% 100%, 50% 45%, 0 100%, 0 55%);
}

.hero-mark[data-tier="3"] {
    padding: 3px 6px;
    border-color: var(--gold);
    box-shadow: 0 0 0 2px var(--ink);
}

.hero-mark[data-tier="3"] i { background: var(--gold); }

/* ============================================================
   ЗНАК СПОСОБНОСТИ (этап 40, шаг 50; ТЗ ч.03 приёмка, ч.04 разделы 28–33)

   Кнопка боя рисовала первую букву названия: «Р» у рывка, «Г» у ульты Тени.
   Буква не отличает лечение от щита и ничего не говорит тому, кто взял героя
   впервые, — приёмка части 03 записала это провалом.

   Знак называет СЕРВЕР и выводит его из эффект-компонентов способности
   (`AbilityEngine::signFor`), поэтому здесь нет ни одного имени героя
   и ни одного названия способности: словарь форм, и только.

   Форма, а не цвет: те же правила, что у статусов боя и личных вещей
   (этап 40, шаги 6 и 49) — знак обязан читаться на золотой ульте, на тёмной
   карточке героя и в оттенках серого. Ни одной анимации: кнопка и так пульсует.
   ============================================================ */

.glyph {
    position: relative;
    display: block;
    width: var(--glyph-size, 26px);
    height: var(--glyph-size, 26px);
    /* Цвет наследуется от кнопки: свой знак у ульты и у Special не заводится. */
    color: currentColor;
    pointer-events: none;
}

.glyph::before,
.glyph::after {
    content: '';
    position: absolute;
    background: currentColor;
}

/* Адресовано стороне целиком, а не одной цели: лечение отряда и лечение
   раненого — разные решения, и видно это должно быть до чтения описания.
   Три точки под знаком, сам знак при этом поднимается. */
.glyph[data-team="1"] {
    background-image: radial-gradient(circle at 50% 50%, currentColor 40%, transparent 44%);
    background-size: 32% 18%;
    background-position: bottom center;
    background-repeat: repeat-x;
}

.glyph[data-team="1"]::before,
.glyph[data-team="1"]::after { transform: translateY(-12%) scale(0.86); }

/* Лечение — крест. */
.glyph[data-glyph="HEAL"]::before { inset: 8% 38%; }
.glyph[data-glyph="HEAL"]::after  { inset: 38% 8%; }

/* Щит — щит. */
.glyph[data-glyph="SHIELD"]::before {
    inset: 6% 14%;
    clip-path: polygon(50% 0, 100% 20%, 100% 58%, 50% 100%, 0 58%, 0 20%);
}
.glyph[data-glyph="SHIELD"]::after { display: none; }

/* Ловушка — мина: круг с шипами. */
.glyph[data-glyph="TRAP"]::before { inset: 26%; border-radius: 50%; }
.glyph[data-glyph="TRAP"]::after {
    inset: 10%;
    clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
}

/* Притяжение — две стрелки внутрь. */
.glyph[data-glyph="PULL"]::before { inset: 22% 52% 22% 6%; clip-path: polygon(0 0, 100% 50%, 0 100%); }
.glyph[data-glyph="PULL"]::after  { inset: 22% 6% 22% 52%; clip-path: polygon(100% 0, 0 50%, 100% 100%); }

/* Отбрасывание — те же стрелки наружу. */
.glyph[data-glyph="PUSH"]::before { inset: 22% 52% 22% 6%; clip-path: polygon(100% 0, 0 50%, 100% 100%); }
.glyph[data-glyph="PUSH"]::after  { inset: 22% 6% 22% 52%; clip-path: polygon(0 0, 100% 50%, 0 100%); }

/* Оглушение — молния. */
.glyph[data-glyph="STUN"]::before {
    inset: 4% 26%;
    clip-path: polygon(58% 0, 100% 0, 62% 40%, 96% 40%, 30% 100%, 46% 54%, 8% 54%);
}
.glyph[data-glyph="STUN"]::after { display: none; }

/* Зона — кольцо на земле. */
.glyph[data-glyph="ZONE"]::before {
    inset: 12%;
    border: 3px solid currentColor;
    border-radius: 50%;
    background: none;
}
.glyph[data-glyph="ZONE"]::after { inset: 40%; border-radius: 50%; }

/* Рывок — двойной шеврон. */
.glyph[data-glyph="DASH"]::before {
    inset: 16% 44% 16% 6%;
    clip-path: polygon(0 0, 46% 0, 100% 50%, 46% 100%, 0 100%, 54% 50%);
}
.glyph[data-glyph="DASH"]::after {
    inset: 16% 6% 16% 44%;
    clip-path: polygon(0 0, 46% 0, 100% 50%, 46% 100%, 0 100%, 54% 50%);
}

/* Замедление — песочные часы. */
.glyph[data-glyph="SLOW"]::before {
    inset: 8% 22%;
    clip-path: polygon(0 0, 100% 0, 54% 50%, 100% 100%, 0 100%, 46% 50%);
}
.glyph[data-glyph="SLOW"]::after { display: none; }

/* Взрыв по площади — вспышка. */
.glyph[data-glyph="BLAST"]::before {
    inset: 4%;
    clip-path: polygon(50% 0, 62% 30%, 92% 12%, 78% 42%, 100% 50%, 78% 58%, 92% 88%,
        62% 70%, 50% 100%, 38% 70%, 8% 88%, 22% 58%, 0 50%, 22% 42%, 8% 12%, 38% 30%);
}
.glyph[data-glyph="BLAST"]::after { display: none; }

/* Выстрел — стрела вперёд. */
.glyph[data-glyph="SHOT"]::before {
    inset: 24% 6% 24% 6%;
    clip-path: polygon(0 26%, 56% 26%, 56% 0, 100% 50%, 56% 100%, 56% 74%, 0 74%);
}
.glyph[data-glyph="SHOT"]::after { display: none; }

/* Усиление — шеврон вверх над чертой. */
.glyph[data-glyph="EMPOWER"]::before {
    inset: 8% 14% 44% 14%;
    clip-path: polygon(50% 0, 100% 100%, 72% 100%, 50% 46%, 28% 100%, 0 100%);
}
.glyph[data-glyph="EMPOWER"]::after { inset: 74% 20% 10% 20%; }

/* Ослабление — тот же шеврон вниз. */
.glyph[data-glyph="WEAKEN"]::before {
    inset: 8% 14% 44% 14%;
    clip-path: polygon(0 0, 28% 0, 50% 54%, 72% 0, 100% 0, 50% 100%);
}
.glyph[data-glyph="WEAKEN"]::after { inset: 74% 20% 10% 20%; }

/* Прямой удар — след когтя. */
.glyph[data-glyph="STRIKE"]::before {
    inset: 8%;
    background: repeating-linear-gradient(66deg,
        currentColor 0 3px, transparent 3px 9px);
}
.glyph[data-glyph="STRIKE"]::after { display: none; }

/* ============================================================
   ОБЛАЧНЫЙ ПЕРЕХОД
   Фирменный переход из ТЗ (раздел 32): облако проходит перед камерой,
   за ним уже новый экран. Заодно прячет загрузку, а не добавляет её.
   ============================================================ */

.cloud-wipe {
    position: fixed;
    inset: 0;
    z-index: var(--z-transition);
    pointer-events: none;
    opacity: 0;
}

.cloud-wipe__puff {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 62%;
    height: 130%;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(237, 223, 227, 0.98) 60%, rgba(237, 223, 227, 0) 100%);
    transform: translate(-50%, -50%) scale(1);
    will-change: transform;
}

.cloud-wipe__puff:nth-child(1) { left: 22%; height: 150%; }
.cloud-wipe__puff:nth-child(2) { left: 50%; height: 175%; }
.cloud-wipe__puff:nth-child(3) { left: 78%; height: 150%; }

body.is-wiping .cloud-wipe {
    opacity: 1;
    animation: cloud-sweep var(--t-cloud) var(--ease) both;
}

body.is-wiping .cloud-wipe__puff {
    animation: cloud-puff var(--t-cloud) var(--ease) both;
}

@keyframes cloud-sweep {
    0%   { opacity: 0; }
    28%  { opacity: 1; }
    62%  { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes cloud-puff {
    0%   { transform: translate(-50%, -50%) translateX(-130%) scale(0.9); }
    45%  { transform: translate(-50%, -50%) translateX(0) scale(1.05); }
    100% { transform: translate(-50%, -50%) translateX(130%) scale(0.9); }
}

/* ============================================================
   ОРИЕНТАЦИЯ УСТРОЙСТВА
   Экран «Поверни поле боя» удалён на этапе 19.1 (раздел 145 канона портрета).
   Игра ничего не требует от положения телефона: если игрок повернул устройство,
   вертикальная рамка остаётся по центру, а состояние боя, карты и списков
   не сбрасывается (разделы 143–144, 205).
   ============================================================ */
/* ============================================================
   АДАПТАЦИЯ
   ============================================================ */

/*
 * Очень узкий экран (320–360px). Здесь пиксельные минимумы, спасающие текст
 * на больших телефонах, начинают выдавливать контент за пределы кадра —
 * поэтому минимумы опускаются, а отступы сжимаются. Ни один модальный элемент
 * не имеет права вылезти за 320px по ширине (раздел 230).
 */
@media (max-width: 360px) {
    .title { font-size: max(20px, calc(var(--u) * 4.2)); }
    .title--tight { font-size: max(17px, calc(var(--u) * 3.2)); }
    .lead { font-size: max(13px, calc(var(--u) * 1.75)); line-height: 1.4; }
    .btn { min-height: 48px; padding: 0 calc(var(--u) * 2.4); font-size: max(13px, calc(var(--u) * 1.8)); }
    .hud__wallet { gap: 8px; padding: 4px 8px; }
    .nav__label { font-size: 9px; letter-spacing: 0.2px; }
}

/* Низкая рамка: Telegram не развернулся во весь экран либо открыта клавиатура. */
@media (max-height: 620px) {
    .screen { gap: var(--gap-1); }
    .faction { padding: calc(var(--u) * 2); }
    .faction__name { font-size: max(26px, calc(var(--u) * 6)); }
}
/*
 * Уважение к системной настройке: без анимаций мир остаётся тем же,
 * просто перестаёт двигаться. Игровая логика от этого не зависит.
 */
@media (prefers-reduced-motion: reduce) {
    .sky__layer,
    /* Сезонные частицы остаются текстурой: событие видно, движения нет. */
    .island__event[data-preset]::before,
    .world-card,
    .sheet__panel,
    .screen,
    body.is-wiping .cloud-wipe,
    body.is-wiping .cloud-wipe__puff {
        animation: none !important;
    }
    .btn, .zone { transition: none; }
}

/* ============================================================
   ОСТРОВ
   Сцена мира, а не дашборд: зоны застройки — объекты поверх острова,
   и нажимается сам объект, а не строка в меню (ТЗ, часть 03, разделы 30–31).
   Координаты зон приходят с сервера в долях канваса — вёрстка их не выдумывает.
   ============================================================ */

.island {
    position: absolute;
    inset: 0;
    width: 100%;
    min-height: 0;
    /* Кадр острова умеет наезжать (камерный тур возвращения, ТЗ часть 27,
       разделы 36–37). Обрезает наезд рамка, а не сама сцена: `overflow`
       на элементе своё же преобразование не удерживает. */
    overflow: hidden;
}

/*
 * Сцена острова — вертикальный кадр во весь экран. Верхние ~14% и нижние ~18%
 * мастера нарисованы свободными: там лежат HUD и навигация, и мир под ними
 * не прячется, а продолжается (разделы 16, 23, 225).
 */
.island__scene {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

/*
 * Холст мира: слой РОВНО по нарисованной картинке (этап 40, шаг 69).
 *
 * Всё, что стоит НА острове, лежит внутри него и позиционируется в долях
 * этого слоя — то есть в долях рисунка. До этого мир жил в долях РАМКИ,
 * а рамка у каждого телефона своей пропорции: на 390×844 картинка вылезает
 * за края на 18% ширины, на 320×568 совпадает с рамкой, на Telegram Desktop
 * теряет 12% высоты. Одна и та же координата попадала на три разных места
 * рисунка — измерено `deploy/island-geometry-probe.py`.
 *
 * Слой шире рамки — и это нормально: обрезает его `.island`, ровно как
 * обрезал бы `background-size: cover`. Наверху ничего не обрезается никогда
 * (`top: 0`), потому что мастер нарисован от неба вниз.
 *
 * `z-index` тут не ставится намеренно: слой не создаёт своего контекста
 * наложения, и порядок его детей по-прежнему считается вместе с тревогой,
 * вспышкой и силуэтом босса — они остаются снаружи, потому что принадлежат
 * КАДРУ, а не острову.
 */
.island__world {
    /*
     * Ширина считается от РАМКИ (100% — это её ширина), а не от `--frame-w`:
     * между рамкой и островом лежит экран со своими отступами, и на Telegram
     * Desktop остров на шесть пикселей уже рамки. Высота не задаётся вовсе —
     * её даёт пропорция мастера, и она сама выходит равной `cover`.
     */
    --stage-w: max(100%, calc(var(--app-h) * 9 / 16));

    position: absolute;
    top: 0;
    left: calc((100% - var(--stage-w)) / 2);
    width: var(--stage-w);
    aspect-ratio: var(--art-aspect);
}

/*
 * Картинка острова. Отдельный слой, а не фон сцены, — потому что на неё
 * ставится `filter` платной темы, а сцена это родитель ВСЕГО движущегося:
 * героев живого слоя, осадков, следов набега, эффектов. Фильтр на родителе
 * перерисовывает их каждый кадр (та же болезнь, что у неба за ареной,
 * глава 9 `PERF_AUDIT.md`).
 *
 * Слой обязан остаться листом: любой ребёнок, который двигается, вернёт
 * цену обратно. Это под тестом (`tests/PaintCostTest.php`).
 */
.island__art {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-position: center top;
    background-size: cover;
}

.island__scene.has-scene .island__art { background-image: var(--scene); }

/*
 * Ассета нет — остров всё равно есть (ТЗ, часть 09, тест 316 и раздел 282).
 *
 * Мастер приезжает отдельным полем и бывает пустым: неутверждённый ассет
 * сервер не отдаёт вовсе. До этого пустой слой означал дыру на месте острова —
 * зоны и герои висели в воздухе. Небо, трава и скальный низ нарисованы теми же
 * тремя цветами канона, что и мастер: остров остаётся читаемым местом,
 * а не сломанным экраном.
 */
.island__art {
    background-color: var(--sky-mid);
    background-image: linear-gradient(
        to bottom,
        var(--sky) 0%,
        var(--sky-mid) 38%,
        #4E9A3A 52%,
        #7A4A24 62%,
        #4A2C16 78%,
        rgba(74, 44, 22, 0) 92%
    );
}

/*
 * Камера возвращения (ТЗ, часть 27, разделы 36–40). Единственное состояние,
 * в котором кадр острова двигается сам: очень короткий путь по местам, где
 * что-то случилось, — и назад.
 *
 * Переезд длиннее самой остановки был бы мазнёй, короче — рывком: 420 мс
 * на переезд при остановке в 700 укладывают наезд и паузу в один такт.
 * Отъезд назад медленнее: возвращённое управление не должно щёлкать.
 */
.island__scene[data-camera="MOVING"] { transition: transform 420ms cubic-bezier(0.33, 0.1, 0.2, 1); }
.island__scene[data-camera="BACK"]   { transition: transform 560ms cubic-bezier(0.33, 0.1, 0.2, 1); }

/* Пропуск отдаёт кадр сразу: доезжать полсекунды после нажатия — это не
   пропуск, а ещё одна анимация поверх пропущенной (раздел 39). */
.island__scene[data-camera="CUT"] { transition: none; }

/* Игрок попросил меньше движения — камера не двигается вовсе. Тур ничего
   не рассказывает сверх мира: он только подчёркивает, и без него всё
   на месте (раздел 349). */
@media (prefers-reduced-motion: reduce) {
    .island__scene { transition: none !important; transform: none !important; }
}

/* Время суток — оверлей поверх сцены. На геймплей не влияет, только на настроение. */
.island__scene[data-time-of-day="DAWN"]::after,
.island__scene[data-time-of-day="DUSK"]::after,
.island__scene[data-time-of-day="NIGHT"]::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
}

/* Утро остаётся светлым: раздел 64 просит мягкого солнца, а не фильтра —
   тёплый верх и почти чистый низ. */
.island__scene[data-time-of-day="DAWN"]::after {
    background: linear-gradient(180deg, rgba(253, 197, 1, 0.2), rgba(184, 96, 240, 0.12));
}

/* Вечер — закат (раздел 65). Прежние 0.18 не читались вовсе: мастер острова
   нарисован ярким днём, и слабый оверлей поверх него виден только в пипетке.
   Тяжелее ночного он при этом не становится — вечером остров ещё виден весь. */
.island__scene[data-time-of-day="DUSK"]::after {
    background: linear-gradient(180deg,
        rgba(232, 112, 38, 0.5) 0%,
        rgba(202, 74, 122, 0.36) 32%,
        rgba(101, 30, 153, 0.3) 100%);
}

/* Ночью раздел 66 просит не темноты, а звёзд. Они лежат в верхней трети
   кадра — там, где у вертикального острова небо, — и нарисованы точками
   градиента: ради восьми точек ассет не генерируют. Слои идут первыми,
   то есть поверх ночного градиента.

   Сам градиент тяжелее НАВЕРХУ, и это не вкусовщина: мастер острова нарисован
   с дневным небом, и пока небо остаётся голубым, звёзды на нём читаются как
   мусор, а не как ночь. Низ кадра, наоборот, притемнён слабее — там остров,
   и раздел 126 требует света, а не темноты: подсвеченные окна зданий
   и свечение реактора должны оставаться видимыми. */
.island__scene[data-time-of-day="NIGHT"]::after {
    background:
        radial-gradient(circle at 12% 6%,  rgba(237, 223, 227, 0.95) 0 2px, rgba(237, 223, 227, 0.25) 3px, transparent 5px),
        radial-gradient(circle at 27% 13%, rgba(237, 223, 227, 0.7) 0 1.5px, transparent 4px),
        radial-gradient(circle at 41% 5%,  rgba(237, 223, 227, 0.85) 0 2px, rgba(237, 223, 227, 0.2) 3px, transparent 5px),
        radial-gradient(circle at 58% 11%, rgba(64, 200, 248, 0.8) 0 2px, rgba(64, 200, 248, 0.2) 3px, transparent 5px),
        radial-gradient(circle at 69% 4%,  rgba(237, 223, 227, 0.75) 0 1.5px, transparent 4px),
        radial-gradient(circle at 83% 15%, rgba(237, 223, 227, 0.9) 0 2px, rgba(237, 223, 227, 0.25) 3px, transparent 5px),
        radial-gradient(circle at 91% 7%,  rgba(184, 96, 240, 0.7) 0 2px, transparent 4px),
        radial-gradient(circle at 49% 18%, rgba(237, 223, 227, 0.6) 0 1.5px, transparent 4px),
        linear-gradient(180deg,
            rgba(9, 16, 52, 0.74) 0%,
            rgba(12, 32, 84, 0.6) 34%,
            rgba(24, 19, 31, 0.42) 100%);
}

.island__zones {
    position: absolute;
    inset: 0;
    /* Слой прозрачен для нажатий: кликается САМА кнопка зоны, а пустое место
       над островом пропускает нажатие ниже — к героям живого слоя. Без этого
       контейнер зон съедал каждый тап по острову, и по герою нельзя было попасть. */
    pointer-events: none;
}

/*
 * Маркер зоны застройки. В вертикальном кадре двенадцать подписей поверх острова
 * превращаются в кашу, поэтому по умолчанию зона — компактный world-space маркер
 * под палец (раздел 18): здание нажимается прямо на острове, а название и цифры
 * читаются в карточке снизу. Пятнадцати больших кнопок вокруг острова здесь нет
 * намеренно — раздел 17 запрещает их прямо.
 */
.zone {
    position: absolute;
    transform: translate(-50%, -50%);
    display: grid;
    place-items: center;
    width: max(44px, calc(var(--u) * 6));
    height: max(44px, calc(var(--u) * 6));
    padding: 0;
    border: var(--stroke) solid var(--ink);
    border-radius: 50%;
    background: rgba(24, 19, 31, 0.72);
    color: var(--cream);
    font-family: var(--font-text);
    cursor: pointer;
    pointer-events: auto;
    transition: transform var(--t-fast) var(--ease);
}

.zone:hover  { transform: translate(-50%, -50%) scale(1.06); }
.zone:active { transform: translate(-50%, -50%) scale(0.94); }

/* Подписи живут в карточке зоны; на самом маркере — только первая буква. */
.zone__label,
.zone__meta { display: none; }

/*
 * Знак построенного здания (ТЗ, часть 09, разделы 289–293 — там же, где чертёж).
 *
 * До этого на маркере стояла первая буква названия, и она была тем же дефектом,
 * который у чертежа закрыт силуэтом: первых букв на двенадцать зданий приходится
 * семь, и на застроенном острове рядом стоят «П» Причала, «П» Поста обороны
 * и «П» Площадки событий. Знак называет здание, а знак, одинаковый у троих,
 * не называет никого.
 *
 * Форма, а не цвет (`docs/ACCESSIBILITY.md`), и ноль анимаций. Обводка сделана
 * так же, как у эмблем доски: чернильная фигура снизу, светлая внутри с отступом
 * (`clip-path: inherit` на том же контуре) — поэтому знак читается и на тёмном
 * маркере, и поверх налитого золотом бака, и отдельных правил под заливку
 * ему не нужно.
 */
/*
 * Размер и толщина обводки — не вкус, а мерка формы. Обводка съедает по 2 точки
 * с каждой стороны, поэтому деталь тоньше 22 % ширины знака доходит до глаза
 * сплошной чернильной полосой. Первый заход это и показал: вымпел клана
 * и ствол турели были нарисованы тонкими и в кадре оказались тёмными пятнами.
 * Отсюда правило форм ниже: самая тонкая часть знака шире двойной обводки.
 */
.zone__sign {
    display: block;
    /* Над налитым баком: абсолютный слой заливки иначе накрыл бы знак. */
    position: relative;
    width: max(30px, calc(var(--u) * 4.2));
    height: max(30px, calc(var(--u) * 4.2));
    background: var(--ink);
}

.zone__sign::before {
    content: '';
    position: absolute;
    inset: 2px;
    background: var(--cream);
    clip-path: inherit;
}

/*
 * Формы различаются СЕМЕЙСТВОМ силуэта, а не деталью. Первый заход развёл их
 * деталью — и градирня реактора, наковальня мастерской и дирижабль на мачте
 * пришли в кадр одним и тем же «широко–узко–широко»: три одинаковых пятна
 * вместо трёх зданий, ровно то, чем была буква. Поэтому здесь стоят вертикаль,
 * горизонталь, зигзаг, треугольник, дом, крест, звезда и щит — вещи, которые
 * не спутать даже пятном.
 */

/*
 * Запасная форма — род занятий. Объявлена ЗДЕСЬ, ВЫШЕ знаков зданий,
 * и это не порядок ради красоты: у `[data-kind]` и `[data-sign]` одинаковая
 * сила селектора, поэтому спор решает порядок в файле. Первая версия свела
 * эти формы с чертежом и оставила их НИЖЕ — и все двенадцать знаков разом
 * стали формой своего рода занятий: казармы и мастерская пришли в кадр
 * одинаковыми домиками. Порядок сторожит `tests/IslandTest.php`.
 *
 * Формы общие с чертежом закрытой зоны нарочно: чертёж обещает род занятий,
 * и здание без своей формы показывает ровно то, что было обещано.
 */
.zone__silhouette[data-kind="CORE"],
.zone__sign[data-kind="CORE"] {
    clip-path: polygon(2% 100%, 98% 100%, 98% 86%, 72% 86%, 72% 40%, 60% 40%, 60% 14%,
                       50% 4%, 40% 14%, 40% 40%, 28% 40%, 28% 86%, 2% 86%);
}

/* Производство — купол с трубой. */
.zone__silhouette[data-kind="PRODUCTION"],
.zone__sign[data-kind="PRODUCTION"] {
    clip-path: polygon(2% 100%, 98% 100%, 98% 86%, 80% 86%, 80% 52%, 66% 34%, 60% 34%,
                       60% 10%, 44% 10%, 44% 34%, 34% 34%, 20% 52%, 20% 86%, 2% 86%);
}

/* Военное — двускатная крыша. */
.zone__silhouette[data-kind="MILITARY"],
.zone__sign[data-kind="MILITARY"] {
    clip-path: polygon(2% 100%, 98% 100%, 98% 86%, 84% 86%, 84% 46%, 50% 18%, 16% 46%, 16% 86%, 2% 86%);
}

/* Оборона — зубцы на стене. */
.zone__silhouette[data-kind="DEFENSE"],
.zone__sign[data-kind="DEFENSE"] {
    clip-path: polygon(2% 100%, 98% 100%, 98% 86%, 78% 86%, 78% 30%, 68% 30%, 68% 18%,
                       58% 18%, 58% 30%, 42% 30%, 42% 18%, 32% 18%, 32% 30%, 22% 30%,
                       22% 86%, 2% 86%);
}

/* Общее — площадка с мачтой. */
.zone__silhouette[data-kind="SOCIAL"],
.zone__sign[data-kind="SOCIAL"] {
    clip-path: polygon(2% 100%, 98% 100%, 98% 86%, 84% 86%, 84% 62%, 54% 62%, 54% 12%,
                       46% 12%, 46% 62%, 16% 62%, 16% 86%, 2% 86%);
}

/* Штаб — ступенчатая башня со шпилем: вертикаль. */
.zone__sign[data-sign="COMMAND_HQ"] {
    clip-path: polygon(6% 100%, 94% 100%, 94% 84%, 72% 84%, 72% 46%, 62% 46%, 62% 26%, 58% 26%, 58% 6%, 42% 6%, 42% 26%, 38% 26%, 38% 46%, 28% 46%, 28% 84%, 6% 84%);
}

/*
 * Причал — дирижабль носом вправо: горизонталь.
 *
 * Первая версия рисовала корпус с гондолой, и в кадре она читалась грибом:
 * округлый верх на короткой ножке — то же семейство, что наковальня мастерской.
 * Гондола убрана, вместо неё хвостовое оперение — оно даёт направление,
 * а направление ни у одного другого знака острова не встречается.
 */
.zone__sign[data-sign="AIRSHIP_DOCK"] {
    clip-path: polygon(2% 26%, 14% 26%, 14% 38%, 34% 32%, 70% 32%, 98% 50%, 70% 68%, 34% 68%, 14% 62%, 14% 74%, 2% 74%);
}

/* Реактор — молния: зигзаг. */
.zone__sign[data-sign="RESOURCE_REACTOR"] {
    clip-path: polygon(60% 2%, 22% 56%, 46% 56%, 34% 98%, 80% 42%, 54% 42%, 74% 2%);
}

/* Казна — сундук с замком. */
.zone__sign[data-sign="TREASURY"] {
    clip-path: polygon(8% 96%, 92% 96%, 92% 44%, 76% 26%, 60% 26%, 60% 42%, 40% 42%, 40% 26%, 24% 26%, 8% 44%);
}

/* Лаборатория — колба: треугольник. */
.zone__sign[data-sign="RESEARCH_LAB"] {
    clip-path: polygon(36% 4%, 64% 4%, 64% 34%, 94% 100%, 6% 100%, 36% 34%);
}

/* Казармы — длинный дом с воротами. */
.zone__sign[data-sign="HERO_BARRACKS"] {
    clip-path: polygon(6% 100%, 6% 50%, 50% 16%, 94% 50%, 94% 100%, 62% 100%, 62% 66%, 38% 66%, 38% 100%);
}

/* Тренировочный двор — чучело с руками: крест. */
.zone__sign[data-sign="TRAINING_YARD"] {
    clip-path: polygon(38% 2%, 62% 2%, 62% 20%, 96% 20%, 96% 42%, 62% 42%, 62% 74%, 82% 98%, 58% 98%, 50% 76%, 42% 98%, 18% 98%, 38% 74%, 38% 42%, 4% 42%, 4% 20%, 38% 20%);
}

/* Мастерская — наковальня с рогом: талия и рог вправо. */
.zone__sign[data-sign="WORKSHOP"] {
    clip-path: polygon(6% 28%, 76% 28%, 96% 40%, 76% 48%, 62% 48%, 62% 74%, 86% 74%, 86% 96%, 14% 96%, 14% 74%, 38% 74%, 38% 48%, 6% 48%);
}

/* Пост обороны — башня и стена с зубцом: сплошной блок. */
.zone__sign[data-sign="DEFENSE_CONTROL"] {
    clip-path: polygon(62% 12%, 96% 12%, 96% 96%, 4% 96%, 4% 44%, 24% 44%, 24% 24%, 46% 24%, 46% 44%, 62% 44%);
}

/* Военный зал — щит. */
.zone__sign[data-sign="WAR_ROOM"] {
    clip-path: polygon(12% 6%, 88% 6%, 88% 44%, 50% 96%, 12% 44%);
}

/* Клановый пост — вымпел на древке: флаг. */
.zone__sign[data-sign="CLAN_OUTPOST"] {
    clip-path: polygon(24% 2%, 46% 2%, 46% 8%, 96% 30%, 46% 52%, 46% 100%, 24% 100%);
}

/* Площадка событий — звезда. */
.zone__sign[data-sign="EVENT_PLATFORM"] {
    clip-path: polygon(50% 2%, 62% 36%, 97% 36%, 68% 58%, 79% 94%, 50% 72%, 21% 94%, 32% 58%, 3% 36%, 38% 36%);
}

/* Построено — цвет фракции. Свободно — пунктир. Закрыто — приглушённый силуэт. */
.zone[data-state="built"] {
    background: color-mix(in srgb, var(--brand) 78%, rgba(24, 19, 31, 0.9));
    box-shadow: 0 4px 0 var(--ink-shadow);
}

.zone[data-state="free"] {
    border-style: dashed;
    background: rgba(24, 19, 31, 0.5);
}

/*
 * Закрытая зона — ЧЕРТЁЖ, а не замок (ТЗ, часть 09, разделы 289–293).
 *
 * Раздел 289 запрещает «просто иконку замка», 290 перечисляет, чем показывать:
 * фундамент, ящики, голограмма чертежа. Здесь взяты две вещи сразу — пунктирное
 * кольцо (чертёж) и силуэт на широком основании (фундамент): игрок видит,
 * что место занято будущим, а не сломано.
 */
.zone[data-state="locked"] {
    background: rgba(24, 19, 31, 0.58);
    border-color: color-mix(in srgb, var(--sky) 70%, var(--ink));
    border-style: dashed;
    opacity: 0.85;
}

/*
 * Силуэт будущего здания (раздел 292).
 *
 * Форма — единственное, чем он отличается: цветом различать нельзя
 * (`docs/ACCESSIBILITY.md`), а буквы у закрытой зоны нет вовсе. Пять форм
 * по роду занятий; у каждой снизу широкое основание — это и есть фундамент
 * из раздела 290, и он же не даёт силуэтам слипнуться в кружке.
 */
.zone__silhouette {
    position: relative;
    display: block;
    width: max(20px, calc(var(--u) * 2.8));
    height: max(20px, calc(var(--u) * 2.8));
    background: color-mix(in srgb, var(--sky) 78%, var(--cream));
    opacity: 0.9;
}

/* Ядро острова — башня со ступенями. */

/*
 * Бак производства (ТЗ, часть 27, разделы 56, 60, 63).
 *
 * Вместо всплывающего «+1000 GOLD» уровень видно в самом объекте: маркер
 * наливается снизу вверх на долю, которую посчитал сервер. Вернувшийся утром
 * игрок видит полный бак — и понимает, откуда взялись монеты, ещё до нажатия.
 */
.zone[data-fill]::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    /* Уровень с бликом по кромке: без него заливка читается как фон, а не как
       налитое. Светлая полоса и есть поверхность — она же граница уровня. */
    background: linear-gradient(
        to top,
        var(--gold) calc(var(--fill, 0) * 100% - 3px),
        #FFF0A8 calc(var(--fill, 0) * 100% - 3px),
        #FFF0A8 calc(var(--fill, 0) * 100%),
        rgba(253, 197, 1, 0) calc(var(--fill, 0) * 100%)
    );
    pointer-events: none;
}

/* Полный бак — производство встало, и это видно ободком, а не текстом. */
.zone[data-fill="FULL"]::before {
    background: linear-gradient(to top, rgba(253, 197, 1, 0.95), rgba(253, 197, 1, 0.72));
    box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.55);
}

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

/* Действие — единственное, что имеет право на точку (ТЗ: red dot только на действиях). */
.zone[data-action="ready"] {
    border-color: var(--gold);
    box-shadow: 0 0 0 3px rgba(253, 197, 1, 0.35), 0 4px 0 var(--ink-shadow);
}

.zone[data-action="ready"]::after {
    content: '';
    position: absolute;
    top: -4px;
    right: -4px;
    width: 12px;
    height: 12px;
    border: 2px solid var(--ink);
    border-radius: 50%;
    background: var(--gold);
}

/* ============================================================
   ФЛАГ У ШТАБА И ВЗЯТАЯ ВЕХА (ТЗ, часть 09, разделы 259–261, 296–300)
   ============================================================ */

/*
 * Флаг стоит на координатах ШТАБА и поднят над ним: зона — круг под пальцем,
 * флаг — предмет мира над ней. Нажатий он не берёт вовсе: кнопка здесь одна,
 * и это сам штаб.
 */
.island-flag {
    position: absolute;
    transform: translate(-50%, -100%);
    /* Флаг СТОИТ на зоне, а не висит в ней: подъём — половина круга зоны
       плюс зазор, иначе древко уходит внутрь кнопки штаба. */
    margin-top: calc(var(--u) * -4.6);
    display: block;
    width: max(20px, calc(var(--u) * 2.8));
    height: max(34px, calc(var(--u) * 5));
    pointer-events: none;
    /* Древко. Полотнище — отдельный слой, иначе флаг нечем поднимать. */
    background: linear-gradient(to right,
        transparent 0 38%, var(--ink) 38% 62%, transparent 62% 100%);
    z-index: 2;
}

/*
 * Полотнище: цвет фракции по умолчанию, цвет НАДЕТОГО знамени — если оно
 * своё (война, босс, кампания, сезон). Ключей ровно два, и оба про цвет:
 * словарь `CosmeticService::STYLE_KEYS` другого знамени не описывает.
 */
.island-flag__cloth {
    position: absolute;
    left: 55%;
    top: 6%;
    width: 92%;
    height: 46%;
    display: grid;
    place-items: center;
    border: 2px solid var(--ink);
    background: var(--flag-tint, var(--brand));
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 12% 50%);
    transform-origin: left center;
}

.island-flag[data-faction="DOGS"] { --flag-tint: var(--dogs-main); --flag-accent: var(--dogs-orange); }
.island-flag[data-faction="CATS"] { --flag-tint: var(--cats-main); --flag-accent: var(--gold); }

/* Знак игрока: форма, а не текст — букв на флагах в этой игре нет. */
.island-flag__emblem {
    display: block;
    width: 46%;
    height: 58%;
    background: var(--flag-accent, var(--cream));
    clip-path: polygon(50% 0, 100% 38%, 78% 100%, 22% 100%, 0 38%);
}

/*
 * ЗНАМЯ КЛАНА — отдельный и МЕНЬШИЙ флаг (ТЗ, часть 09, раздел 298).
 *
 * Оно стоит рядом со своим флагом, а не на том же древке: клан это вторая
 * принадлежность, а не другой хозяин. Смещение влево такое, чтобы полотнище
 * кончалось раньше главного древка — иначе на 320 px два полотнища слипаются
 * в одно пятно, и «отдельный флаг» перестаёт читаться отдельным.
 *
 * Оно ниже и уже: оба древка стоят на земле штаба, поэтому короткое древко
 * само уводит полотнище под главное. Своего цвета у клана нет — цвет
 * фракционный, и различаются кланы ФОРМОЙ знака (`docs/ACCESSIBILITY.md`).
 */
.island-flag--clan {
    width: max(14px, calc(var(--u) * 2));
    height: max(23px, calc(var(--u) * 3.4));
    /*
     * Смещение считается от ШИРИНЫ главного флага, а не своим числом в `--u`:
     * оба размера упираются в пиксельный минимум на телефоне, и доля канваса
     * дала бы отступ меньше самого флага — первая версия положила клановое
     * полотнище прямо на главное древко (видно кадром, не чтением кода).
     */
    margin-left: calc(-2px - max(20px, var(--u) * 2.8));
}

/*
 * Знак клана крупнее знака игрока по доле полотнища: у своего знамени есть
 * ещё и цвет, а клан отличается ТОЛЬКО формой — значит формы должно быть
 * видно больше.
 */
.island-flag__mark {
    display: block;
    width: 74%;
    height: 82%;
    background: var(--flag-accent, var(--cream));
}

/*
 * ЗНАКИ КЛАНОВ. Шесть форм, ни одна не повторяет другую силуэтом.
 *
 * Лежат в ядре, а не в пакете раздела: знак висит на знамени у штаба, то есть
 * на первом кадре острова, куда `css/screens/clan.css` не приезжает. Селектор
 * только по атрибуту — потому что мест два (полотнище флага и список кланов),
 * а форма у знака одна: вторая копия полигонов разошлась бы с первой молча.
 */
[data-emblem="BOLT"]   { clip-path: polygon(58% 0, 20% 52%, 46% 52%, 34% 100%, 82% 42%, 54% 42%); }
[data-emblem="FANG"]   { clip-path: polygon(6% 6%, 94% 6%, 62% 46%, 50% 100%, 38% 46%); }
[data-emblem="ANCHOR"] { clip-path: polygon(44% 0, 56% 0, 56% 26%, 84% 26%, 84% 38%, 56% 38%, 56% 82%, 92% 56%, 100% 68%, 50% 100%, 0 68%, 8% 56%, 44% 82%, 44% 38%, 16% 38%, 16% 26%, 44% 26%); }
[data-emblem="CROWN"]  { clip-path: polygon(0 22%, 22% 54%, 50% 10%, 78% 54%, 100% 22%, 100% 100%, 0 100%); }
[data-emblem="GEAR"]   { clip-path: polygon(40% 0, 60% 0, 64% 14%, 82% 8%, 92% 24%, 82% 36%, 92% 50%, 82% 64%, 92% 76%, 82% 92%, 64% 86%, 60% 100%, 40% 100%, 36% 86%, 18% 92%, 8% 76%, 18% 64%, 8% 50%, 18% 36%, 8% 24%, 18% 8%, 36% 14%); }
[data-emblem="WING"]   { clip-path: polygon(96% 8%, 62% 46%, 78% 48%, 44% 74%, 58% 76%, 22% 96%, 6% 62%, 30% 22%); }

/*
 * Момент вехи: флаг ПОДНИМАЕТСЯ, штаб подсвечен, открывшаяся зона
 * подчёркнута. Длину момента держит сервер (3–6 с), здесь только подача.
 */
#island[data-moment="tier"] .island-flag__cloth {
    animation: flag-raise 1200ms var(--ease) both;
}

#island[data-moment="tier"] .zone[data-code="COMMAND_HQ"] {
    border-color: var(--gold);
    box-shadow: 0 0 0 4px rgba(253, 197, 1, 0.4), 0 0 22px rgba(253, 197, 1, 0.55);
}

/* Открывшееся место: пунктир становится золотым и дышит один раз. */
.zone[data-reveal="new"] {
    border-color: var(--gold);
    border-style: solid;
    opacity: 1;
    animation: zone-reveal 900ms var(--ease) 2 both;
}

@keyframes flag-raise {
    from { transform: scaleY(0.1) translateY(120%); }
    to   { transform: none; }
}

@keyframes zone-reveal {
    from { box-shadow: 0 0 0 0 rgba(253, 197, 1, 0.6); }
    to   { box-shadow: 0 0 0 10px rgba(253, 197, 1, 0); }
}

/*
 * Просьба убрать движение снимает подъём и пульс, но не сам флаг и не
 * подсветку: это информация, а не украшение (`docs/ACCESSIBILITY.md`).
 */
@media (prefers-reduced-motion: reduce) {
    #island[data-moment="tier"] .island-flag__cloth,
    .zone[data-reveal="new"] {
        animation: none;
    }
}

/*
 * Карточка мира: разбор зоны, реакция героя, гость. Не модалка поверх мира
 * (ТЗ, раздел 58), а полоса внизу — НАД навигацией, чтобы ни одно действие
 * не оказалось под ней (раздел 166). Всплывает поверх острова, а не выдавливает
 * его из потока.
 */
.world-card {
    position: absolute;
    left: var(--safe-left);
    right: var(--safe-right);
    bottom: calc(var(--nav-total) + var(--gap-1));
    z-index: calc(var(--z-hud) - 1);
    display: grid;
    gap: var(--gap-1);
    padding: calc(var(--u) * 1.6) calc(var(--u) * 2);
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius);
    /* Подложка непрозрачная: карточка висит над артом острова, и полупрозрачный
       фон превращал текст в кашу из букв и кошачьих башен. */
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--brand) 42%, rgba(26, 20, 64, 0.97)) 0%,
        rgba(26, 20, 64, 0.97) 62%);
    text-align: left;
    animation: world-card-in var(--t-fast) var(--ease) both;
}

.world-card__text { display: grid; gap: 2px; min-width: 0; }

.world-card .lead {
    font-size: max(13px, calc(var(--u) * 1.6));
    line-height: 1.35;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Куда растёт база: строка прогресса у штаба (ТЗ, часть 09, раздел 33).
   Тише описания — это не то, ради чего сюда нажали, а справка о завтрашнем
   дне; на 320 px обрезается по второй строке, как и всё в этой карточке. */
.world-card .lead--soft {
    font-size: max(12px, calc(var(--u) * 1.45));
    color: rgba(237, 223, 227, 0.62);
    -webkit-line-clamp: 2;
}

/* Что даст следующий уровень. Это не примечание, а число, по которому
   принимают решение, — до шага 126 кнопка называла одну цену, и отличить
   выгодный уровень от ловушки было нечем. */
.world-card .lead--soft[data-kind="gain"] {
    color: var(--gold);
    font-weight: 800;
}

/* Действия в карточке: в ряд, каждое не уже пальца. Их до трёх. */
.world-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gap-1);
}

.world-card__actions .btn {
    flex: 1 1 auto;
    min-height: 48px;
    padding: 0 calc(var(--u) * 2);
    font-size: max(13px, calc(var(--u) * 1.7));
}

.world-card__close { flex: 0 0 auto; }

@keyframes world-card-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

/* ============================================================
   LIVING HOME — ЖИВОЙ ОСТРОВ
   Слой поверх сцены острова и ПОД кнопками зон: герой может ходить перед
   зданием, но никогда не перехватит нажатие по нему (ТЗ, часть 17, разделы 335, 365).
   Всё движение отсюда выключается `prefers-reduced-motion`.
   ============================================================ */

.home {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.home__traffic,
.home__props,
.home__actors,
.home__fx {
    position: absolute;
    inset: 0;
}

/* Фоновый трафик некликабелен и обязан выглядеть некликабельным (раздел 108). */
.home__traffic { pointer-events: none; }
.home__fx      { pointer-events: none; }

/* --- Актёр --- */

.actor {
    position: absolute;
    transform: translate(-50%, -100%);
    width: max(30px, calc(var(--u) * 8.4));
    height: max(30px, calc(var(--u) * 8.4));
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    pointer-events: auto;
    /* Плавность движения задаётся из JS на каждое ребро маршрута. */
    transition: left 0.6s linear, top 0.6s linear;
}

.actor__body {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: var(--stroke) solid var(--ink);
    background-color: var(--panel-deep);
    background-image: var(--portrait);
    background-size: cover;
    background-position: center 22%;
    box-shadow: 0 3px 0 var(--ink-shadow);
}

/* Кольцо фракции: сторона читается формой и цветом без единой буквы. */
.actor[data-faction="CATS"] .actor__body { box-shadow: 0 0 0 3px var(--cats-main), 0 3px 0 var(--ink-shadow); }
.actor[data-faction="DOGS"] .actor__body { box-shadow: 0 0 0 3px var(--dogs-main), 0 3px 0 var(--ink-shadow); }

.actor__shadow {
    position: absolute;
    left: 50%;
    bottom: calc(var(--u) * -0.7);
    width: 70%;
    height: calc(var(--u) * 1.1);
    transform: translateX(-50%);
    border-radius: 50%;
    background: rgba(24, 19, 31, 0.35);
    filter: blur(1px);
}

/* Герой смотрит туда, куда идёт. */
.actor[data-dir="left"] .actor__body { transform: scaleX(-1); }

/*
 * Говорящий герой поднимается над предметами острова.
 *
 * Живой слой лежит НИЖЕ кнопок зон намеренно (ТЗ, часть 17, разделы 335, 365),
 * и это правило про НАЖАТИЕ, а не про то, что реплику можно закрыть вывеской:
 * облачко, наполовину спрятанное за кнопкой «На борт», читается как баг. Своим
 * z-index облачко подняться не может — у `.actor` есть `transform`, то есть
 * собственный контекст наложения, и высота считается по герою целиком.
 *
 * Поэтому на две секунды реплики поднимается сам герой — и ровно на это время
 * перестаёт быть целью для пальца. Нажатие уходит зданию за ним, как и задумано.
 */
.actor--talking {
    z-index: calc(var(--z-world) + 1);
    pointer-events: none;
}

.actor__bubble {
    position: absolute;
    left: 50%;
    bottom: calc(100% + var(--u) * 0.8);
    transform: translateX(-50%);
    pointer-events: none;
    /* `max-content` обязателен вместе с переносом: облачко висит на крохотном
       узле героя, и без него ширина схлопывается до самого длинного слова —
       реплика встаёт столбиком по слову в строке. */
    width: max-content;
    max-width: calc(var(--u) * 26);
    padding: calc(var(--u) * 0.6) calc(var(--u) * 1.2);
    border: 3px solid var(--ink);
    border-radius: var(--radius-sm);
    background: var(--cream);
    color: var(--ink);
    font-family: var(--font-text);
    font-weight: 900;
    font-size: max(10px, calc(var(--u) * 1.25));
    line-height: 1.2;
    /* Реплика длиннее облачка ПЕРЕНОСИТСЯ, а не обрезается. С `nowrap` строка
       в двадцать знаков теряла хвост прямо посреди слова, и сцена читалась как
       баг. Коротко писать всё равно надо — но обрезанный текст этому не учит,
       он просто врёт игроку. */
    white-space: normal;
    text-align: center;
    animation: bubble-in 0.2s var(--ease) both;
}

@keyframes bubble-in {
    from { opacity: 0; transform: translate(-50%, 6px) scale(0.9); }
    to   { opacity: 1; transform: translateX(-50%) scale(1); }
}

/* --- Библиотека анимаций ---
   Модульная и переиспользуемая (раздел 253): сцена просит `work`, а не «Пиксель
   чинит дрон». Нет анимации — включается ближайшая совместимая, сцена не ломается. */

.actor[data-anim="idle"]      .actor__body { animation: anim-idle 3.4s ease-in-out infinite; }
.actor[data-anim="walk"]      .actor__body { animation: anim-walk 0.5s ease-in-out infinite; }
.actor[data-anim="run"]       .actor__body { animation: anim-walk 0.28s ease-in-out infinite; }
.actor[data-anim="look"]      .actor__body { animation: anim-look 2.4s ease-in-out infinite; }
.actor[data-anim="point"]     .actor__body { animation: anim-point 0.9s ease-in-out infinite; }
.actor[data-anim="wave"]      .actor__body { animation: anim-wave 0.7s ease-in-out infinite; }
.actor[data-anim="laugh"]     .actor__body { animation: anim-laugh 0.5s ease-in-out infinite; }
.actor[data-anim="angry"]     .actor__body { animation: anim-angry 0.32s ease-in-out infinite; }
.actor[data-anim="sit"]       .actor__body { animation: none; transform: translateY(12%) scale(0.92); }
.actor[data-anim="sleep"]     .actor__body { animation: anim-sleep 3.6s ease-in-out infinite; }
.actor[data-anim="work"]      .actor__body { animation: anim-work 0.6s ease-in-out infinite; }
.actor[data-anim="carry"]     .actor__body { animation: anim-carry 1.4s ease-in-out infinite; }
.actor[data-anim="celebrate"] .actor__body { animation: anim-celebrate 0.6s ease-in-out infinite; }
.actor[data-anim="surprised"] .actor__body { animation: anim-surprised 0.5s ease-out 2; }

@keyframes anim-idle      { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4%); } }
@keyframes anim-walk      { 0%, 100% { transform: translateY(0) rotate(-3deg); } 50% { transform: translateY(-8%) rotate(3deg); } }
@keyframes anim-look      { 0%, 100% { transform: rotate(-5deg); } 50% { transform: rotate(5deg); } }
@keyframes anim-point     { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(12%); } }
@keyframes anim-wave      { 0%, 100% { transform: rotate(-9deg); } 50% { transform: rotate(9deg); } }
@keyframes anim-laugh     { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-6%) scale(1.04); } }
@keyframes anim-angry     { 0%, 100% { transform: translateX(-4%); } 50% { transform: translateX(4%); } }
@keyframes anim-sleep     { 0%, 100% { transform: translateY(8%) scale(0.94) rotate(-4deg); } 50% { transform: translateY(11%) scale(0.94) rotate(-6deg); } }
@keyframes anim-work      { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-12deg) translateY(4%); } }
@keyframes anim-carry     { 0%, 100% { transform: translateY(-2%) rotate(2deg); } 50% { transform: translateY(2%) rotate(-2deg); } }
@keyframes anim-celebrate { 0%, 100% { transform: translateY(0) scale(1); } 40% { transform: translateY(-16%) scale(1.05); } }
@keyframes anim-surprised { 0% { transform: scale(1); } 30% { transform: scale(1.16) translateY(-8%); } 100% { transform: scale(1); } }

/* Несомый груз виден: сценка «передают ящик» без ящика не читается. */
.actor[data-carry="1"]::after {
    content: '';
    position: absolute;
    right: calc(var(--u) * -1.2);
    bottom: calc(var(--u) * 0.6);
    width: calc(var(--u) * 2.4);
    height: calc(var(--u) * 2.4);
    border: 3px solid var(--ink);
    border-radius: 4px;
    background: var(--dogs-orange);
}

/* --- Объекты мира: гости, трофеи, груз --- */

.home-prop {
    position: absolute;
    transform: translate(-50%, -50%);
    width: calc(var(--u) * 5);
    height: calc(var(--u) * 5);
    padding: 0;
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-sm);
    background: var(--panel);
    box-shadow: 0 3px 0 var(--ink-shadow);
    cursor: pointer;
    pointer-events: auto;
}

/* Кликабельный груз отличается мягким поведением, а не огромным маркером
   (раздел 110): он покачивается и слегка светится. Красной точки у него нет. */
.home-prop--interactive {
    background: var(--gold);
    animation: prop-sway 2.6s ease-in-out infinite;
}

.home-prop--interactive[data-object="balloon"] { border-radius: 50% 50% 45% 45%; background: var(--cats-neon); }
.home-prop--interactive.is-taken { animation: none; opacity: 0; transition: opacity 0.4s var(--ease); }

.home-prop--visitor {
    width: calc(var(--u) * 9);
    height: calc(var(--u) * 4.6);
    border-radius: 999px 999px 40% 40%;
    background: linear-gradient(180deg, var(--cream) 0%, var(--sky-mid) 100%);
    animation: prop-float 4.2s ease-in-out infinite;
}

.home-prop--visitor[data-state="DEPARTING"] { animation: prop-float 1.6s ease-in-out infinite; }

/*
 * Кто прилетел, видно ФОРМОЙ, а не цветом (правило доступности этапа 40,
 * шаг 6). До шага 74 своё правило было у двух видов из шести: дирижабль
 * друга, торговец, чужой разведчик и герольд арены рисовались одним белым
 * силуэтом — то есть «к тебе зашёл друг» и «тебя разведывают» выглядели
 * одинаково. Своё правило обязано быть у каждого кода каталога, и это
 * проверяет `tests/VisitIslandTest.php`.
 */
.home-prop--visitor[data-visitor="PATROL"] {
    /* Строй: короткий и плотный, с килем. */
    border-radius: 999px 999px 20% 20%;
    background: linear-gradient(180deg, var(--cream), var(--dogs-main));
}

.home-prop--visitor[data-visitor="COURIER"] {
    /* Посылка под баллоном: прямоугольный низ. */
    border-radius: 999px 999px 6px 6px;
    background: linear-gradient(180deg, var(--cream), var(--cats-purple));
}

.home-prop--visitor[data-visitor="MERCHANT"] {
    /* Пузатый: возит товар. */
    width: calc(var(--u) * 10);
    height: calc(var(--u) * 5.6);
    border-radius: 999px;
    background: linear-gradient(180deg, var(--cream), var(--gold));
}

.home-prop--visitor[data-visitor="FRIEND"] {
    /* Свой: тот же силуэт, что у борта игрока, — вытянутый и с носом. */
    width: calc(var(--u) * 11);
    height: calc(var(--u) * 4);
    border-radius: 999px 40% 40% 999px;
    background: linear-gradient(180deg, var(--cream), var(--sky));
}

.home-prop--visitor[data-visitor="ENEMY_SCOUT"] {
    /* Чужой: узкий клин, острый нос. */
    width: calc(var(--u) * 9);
    height: calc(var(--u) * 3.2);
    border-radius: 4px;
    clip-path: polygon(0 50%, 22% 0, 100% 18%, 100% 82%, 22% 100%);
    background: linear-gradient(180deg, var(--panel), var(--red));
}

.home-prop--visitor[data-visitor="ARENA_HERALD"] {
    /* Герольд: вымпел под баллоном. */
    border-radius: 999px 999px 50% 50%;
    background: linear-gradient(180deg, var(--cream), var(--cats-main));
    clip-path: polygon(0 0, 100% 0, 100% 62%, 62% 62%, 50% 100%, 38% 62%, 0 62%);
}

.home-prop--memory {
    width: calc(var(--u) * 3.2);
    height: calc(var(--u) * 4.4);
    border-radius: 6px 6px 2px 2px;
    background: linear-gradient(180deg, var(--gold) 0%, #C89A02 100%);
}

.home-prop--memory[data-icon="banner"] { border-radius: 4px 4px 50% 50%; background: linear-gradient(180deg, var(--brand, var(--cats-main)), var(--panel-deep)); }
.home-prop--memory[data-icon="oddity"] { border-radius: 50%; background: linear-gradient(180deg, var(--sky), var(--cats-purple)); }

/* Полка различается ФОРМОЙ, а не цветом (правило доступности этапа 40, шаг 6):
   у каждого значка каталога свой силуэт, иначе три золотых прямоугольника —
   это «победа, десять побед и штаб» одним предметом. Анимаций здесь нет
   ни одной: предмет памяти стоит, а не привлекает внимание. */
.home-prop--memory[data-icon="trophy"] {
    border-radius: 2px;
    clip-path: polygon(20% 0, 80% 0, 78% 34%, 96% 22%, 96% 44%, 62% 60%, 62% 84%,
                       84% 84%, 84% 100%, 16% 100%, 16% 84%, 38% 84%, 38% 60%,
                       4% 44%, 4% 22%, 22% 34%);
}

.home-prop--memory[data-icon="medal"] {
    border-radius: 0;
    clip-path: polygon(28% 0, 72% 0, 62% 40%, 84% 56%, 76% 88%, 50% 100%,
                       24% 88%, 16% 56%, 38% 40%);
}

/* Табличка: низкая и со срезанным углом. Ширина общая с полкой намеренно —
   шире соседа предмет вылезал бы на него: шаг ряда считается в долях сцены. */
.home-prop--memory[data-icon="plate"] {
    height: calc(var(--u) * 2.6);
    border-radius: 2px;
    clip-path: polygon(0 0, 88% 0, 100% 34%, 100% 100%, 0 100%);
}

/* Что враг ОСТАВИЛ на острове (ТЗ, часть 25, раздел 233). Взятое у него лежит
   на корабле, здесь — след его работы: крюк, которым он цеплялся за причал,
   и разметочный колышек бригады. Оба стальные и оба стоят, а не висят. */
.home-prop--memory[data-icon="hook"] {
    height: calc(var(--u) * 5);
    border-radius: 0;
    background: linear-gradient(180deg, #A8B4BC 0%, #5A6870 100%);
    clip-path: polygon(38% 0, 62% 0, 62% 52%, 92% 52%, 92% 100%, 68% 100%,
                       68% 74%, 30% 74%, 30% 100%, 6% 100%, 6% 52%, 38% 52%);
}

.home-prop--memory[data-icon="stake"] {
    width: calc(var(--u) * 2.6);
    height: calc(var(--u) * 5);
    border-radius: 0;
    background: linear-gradient(180deg, var(--dogs-orange) 0%, #6A6A28 100%);
    clip-path: polygon(38% 0, 62% 0, 62% 18%, 100% 26%, 100% 52%, 62% 60%,
                       62% 88%, 50% 100%, 38% 88%);
}


@keyframes prop-sway  { 0%, 100% { transform: translate(-50%, -50%) rotate(-4deg); } 50% { transform: translate(-50%, -54%) rotate(4deg); } }
@keyframes prop-float { 0%, 100% { transform: translate(-50%, -50%); } 50% { transform: translate(-50%, -58%); } }

@keyframes smoke-rise {
    0%   { opacity: 0.8; transform: translateY(0) scale(0.7); }
    100% { opacity: 0; transform: translateY(calc(var(--u) * -4)) scale(1.4); }
}

/* --- Трафик неба на трёх слоях глубины --- */

.traffic {
    position: absolute;
    left: -12%;
    width: calc(var(--u) * 5);
    height: calc(var(--u) * 2);
    border-radius: 999px;
    background: rgba(237, 223, 227, 0.7);
    animation: traffic-cross var(--travel, 20s) linear both;
}

.traffic[data-depth="1"] { opacity: 0.35; transform: scale(0.5); }
.traffic[data-depth="2"] { opacity: 0.55; transform: scale(0.75); }
.traffic[data-depth="3"] { opacity: 0.8; }

.traffic[data-kind="cargo"]   { background: color-mix(in srgb, var(--dogs-orange) 65%, var(--cream)); }
.traffic[data-kind="balloon"] { border-radius: 50% 50% 40% 40%; height: calc(var(--u) * 3); width: calc(var(--u) * 2.4); background: var(--cats-purple-soft); }
.traffic[data-kind="bird"]    { height: calc(var(--u) * 0.8); width: calc(var(--u) * 2); background: rgba(24, 19, 31, 0.5); }
.traffic[data-kind="patrol"]  { background: color-mix(in srgb, var(--sky) 70%, var(--cream)); }

@keyframes traffic-cross {
    from { left: -12%; }
    to   { left: 112%; }
}

/* --- Эффекты сцен --- */

.home-fx {
    position: absolute;
    transform: translate(-50%, -50%);
    pointer-events: none;
}

/* Дым над причалом: состояние корабля показывается поверх нарисованного
   дирижабля, а не вторым, сделанным из прямоугольника (раздел 152). */
.home-fx[data-kind="damage"] {
    width: calc(var(--u) * 5);
    height: calc(var(--u) * 5);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(24, 19, 31, 0.5), transparent 70%);
    animation: smoke-rise 3.2s ease-out infinite;
}

.home-fx[data-kind="smoke"],
.home-fx[data-kind="crate_fall"],
.home-fx[data-kind="install"] {
    width: calc(var(--u) * 4);
    height: calc(var(--u) * 4);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(237, 223, 227, 0.7), transparent 70%);
    animation: smoke-rise 2.4s ease-out both;
}

/*
 * Пар и искры (ТЗ, часть 09, раздел 260: «new machinery activates»,
 * «small fireworks/steam»). На корабле эти два эффекта нарисованы с этапа 32,
 * а на острове их не было вовсе — сцена, которая их назовёт, до вехи штаба
 * не заводилась. Форма та же, что у корабля: одно имя эффекта — одна вещь.
 */
.home-fx[data-kind="steam"] {
    width: calc(var(--u) * 5);
    height: calc(var(--u) * 5);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(237, 223, 227, 0.75), transparent 70%);
    animation: smoke-rise 2.8s ease-out both;
}

.home-fx[data-kind="sparks"] {
    width: calc(var(--u) * 6);
    height: calc(var(--u) * 6);
    border-radius: 50%;
    background:
        radial-gradient(circle at 50% 50%, rgba(253, 197, 1, 0.9), transparent 42%),
        radial-gradient(circle at 22% 34%, rgba(248, 80, 232, 0.85), transparent 26%),
        radial-gradient(circle at 78% 30%, rgba(64, 200, 248, 0.85), transparent 26%);
    animation: fx-burst 1600ms ease-out both;
}

@keyframes fx-burst {
    from { opacity: 0; transform: translate(-50%, -50%) scale(0.35); }
    35%  { opacity: 1; }
    to   { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
}

.home-fx[data-kind="rocket_launch"],
.home-fx[data-kind="rocket_return"] {
    width: calc(var(--u) * 1.4);
    height: calc(var(--u) * 3.4);
    border-radius: 999px;
    background: linear-gradient(180deg, var(--danger), var(--gold));
    animation: fx-rocket 2.2s ease-in both;
}

.home-fx[data-kind="rocket_return"] { animation-direction: reverse; }

.home-fx[data-kind="sky_fish"] {
    width: calc(var(--u) * 4.4);
    height: calc(var(--u) * 2);
    border-radius: 50% 20% 20% 50%;
    background: var(--sky);
    animation: fx-drift 5.5s ease-in-out both;
}

.home-fx[data-kind="cargo_pass"],
.home-fx[data-kind="flag_away"] {
    width: calc(var(--u) * 3);
    height: calc(var(--u) * 2);
    background: var(--dogs-orange);
    border: 2px solid var(--ink);
    animation: fx-drift 3.6s ease-in both;
}

.home-fx[data-kind="paw_prints"] {
    width: calc(var(--u) * 6);
    height: calc(var(--u) * 2);
    background: repeating-linear-gradient(90deg, rgba(24, 19, 31, 0.4) 0 6px, transparent 6px 16px);
    animation: fx-fade 3s ease-out both;
}

.home-fx[data-kind="fridge_light"] {
    width: calc(var(--u) * 6);
    height: calc(var(--u) * 6);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(253, 197, 1, 0.55), transparent 70%);
    animation: fx-fade 2.6s ease-out both;
}

.home-fx[data-kind="arrival"] {
    width: calc(var(--u) * 8);
    height: calc(var(--u) * 8);
    border-radius: 50%;
    border: 3px solid var(--gold);
    animation: fx-ring 1.6s ease-out both;
}

.home__focus {
    position: absolute;
    transform: translate(-50%, -50%);
    width: calc(var(--u) * 16);
    height: calc(var(--u) * 16);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(253, 197, 1, 0.22), transparent 68%);
    animation: fx-fade 2.4s ease-out both;
}

/* ============================================================
   СТРАННОСТИ НЕБА (этап 35)
   Всё здесь подчиняется одному правилу ТЗ (часть 20, разделы 10, 50–54):
   объект в небе НИКОГДА не подсвечивается, не подписывается и не показывается
   целиком. Игрок либо заметил, либо нет — и это нормальный исход.
   ============================================================ */

.home__sky {
    position: absolute;
    inset: 0;
    pointer-events: none;
    /* Полоса неба у мастера острова — верхняя треть кадра. Ниже начинается
       сама скала, и странность туда не заходит: она далеко, а не рядом. */
    overflow: hidden;
}

.home-sky-fx {
    position: absolute;
    pointer-events: none;
    will-change: transform, opacity;
}

/* Спина существа. Размер — несколько островов, видно только кусок, края
   размыты облаками. Ни глаз, ни морды: раннее полное раскрытие запрещено. */
.home-sky-fx[data-kind="whale_back"] {
    top: 9%;
    width: calc(var(--u) * 78);
    height: calc(var(--u) * 13);
    border-radius: 50% 50% 42% 42% / 100% 100% 22% 22%;
    /* Плотный тёмно-фиолетовый силуэт, а не дымка: мир нарисован плоскими
       заливками с толстым контуром, и полупрозрачное пятно с сильным блюром
       в нём просто не читается — проверено на кадре. Дальние острова на арте
       решены так же: тёмный цвет, чёткий край, никакой живописи. */
    background: linear-gradient(180deg, rgba(45, 32, 96, 0.72), rgba(45, 32, 96, 0.42));
    filter: blur(2px);
    /* Низ силуэта уходит в облака, а не обрывается линейкой: прямая нижняя
       кромка читалась как полоса интерфейса, а не как спина в облаках. */
    -webkit-mask-image: linear-gradient(180deg, #000 52%, transparent 96%);
    mask-image: linear-gradient(180deg, #000 52%, transparent 96%);
    animation: sky-pass 12s ease-in-out both;
}

/* Старый силуэт остаётся тем же явлением, только теперь он действительно
   в небе, а не у ног героя на смотровой. */
.home-sky-fx[data-kind="big_silhouette"] {
    top: 11%;
    width: calc(var(--u) * 52);
    height: calc(var(--u) * 10);
    border-radius: 50%;
    background: rgba(45, 32, 96, 0.5);
    filter: blur(3px);
    animation: sky-pass 10s ease-in-out both;
}

/* Чужой дирижабль: без огней и без флага. Просто кто-то прошёл мимо. */
.home-sky-fx[data-kind="unknown_ship"] {
    top: 9.5%;
    width: calc(var(--u) * 14);
    height: calc(var(--u) * 4);
    border-radius: 46% 46% 40% 40% / 70% 70% 30% 30%;
    background: rgba(36, 29, 87, 0.78);
    filter: blur(0.8px);
    animation: sky-pass 9s linear both;
}

/* Три вспышки в ритме сигнала. Ровно три и всегда одинаково — мотив,
   который позже отзовётся звуком (разделы 271–276). */
.home-sky-fx[data-kind="light_pattern"] {
    top: 10%;
    left: 44%;
    width: calc(var(--u) * 6);
    height: calc(var(--u) * 6);
    margin: calc(var(--u) * -3) 0 0 calc(var(--u) * -3);
    border-radius: 50%;
    /* Холодное бело-бирюзовое свечение — цвет сети, а не фракции: у котов
       magenta, у псов синий, у неба голубой, а это не принадлежит никому. */
    background: radial-gradient(circle, rgba(214, 255, 244, 0.95) 18%, rgba(143, 240, 216, 0.72) 38%, transparent 70%);
    animation: sky-blink 3.6s steps(1, end) both;
}

/* Пустое место в звёздном небе. Не свечение, а его отсутствие. */
.home-sky-fx[data-kind="missing_star"] {
    top: 6%;
    left: 30%;
    width: calc(var(--u) * 9);
    height: calc(var(--u) * 9);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(6, 8, 20, 0.55), transparent 70%);
    animation: sky-hold 5.2s ease-in-out both;
}

/* Облака встают кольцом. Явление погоды, а не объект. */
.home-sky-fx[data-kind="cloud_ring"] {
    top: 8%;
    left: 42%;
    width: calc(var(--u) * 34);
    height: calc(var(--u) * 12);
    margin-left: calc(var(--u) * -17);
    border-radius: 50%;
    border: calc(var(--u) * 1.6) solid rgba(237, 223, 227, 0.5);
    filter: blur(3px);
    animation: sky-hold 6.4s ease-in-out both;
}

/* ------------------------------------------------------------
   СТРАННОСТЬ НА ЗАГРУЗОЧНОМ ЭКРАНЕ (ТЗ, часть 21, разделы 200–204)
   Своей формы у неё нет намеренно: в небе загрузки проходит ТОТ ЖЕ силуэт,
   что изредка проходит над островом. Две разные картинки на одно явление —
   это два явления, и мотив, ради которого всё затевалось, пропал бы.
   ------------------------------------------------------------ */

.boot__sky {
    position: absolute;
    inset: 0;
    /* Тот же слой, что у сцены и затемнения: картинка (::before) рисуется
       первой, силуэт после неё, затемнение (::after) последним — поэтому
       объект лежит ПОД дымкой кадра, а не поверх логотипа. */
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
}

/* Оба класса в селекторе не для красоты: форма неба задана как
   `.home-sky-fx[data-kind="…"]`, и одиночный класс её не перебил бы. */
.home-sky-fx.boot-oddity {
    /* Верхняя полоса чистого неба у арта загрузки: ниже начинаются острова,
       радуга и логотип, и объект там читался бы как деталь картинки. */
    top: 5%;
    left: calc(var(--u) * 14);
    /* Место и плотность взяты не с острова, а измерены кадром: под ярким
       небом загрузочного арта силуэт в островной плотности не виден вовсе
       (проверено на 390×844). Форма при этом та же самая — иначе повтор,
       ради которого всё и затевалось, заметить было бы нельзя. */
    width: calc(var(--u) * 88);
    height: calc(var(--u) * 18);
    background: linear-gradient(180deg, rgba(28, 20, 70, 0.8), rgba(28, 20, 70, 0.45));
    /* Проход через весь кадр (`sky-pass`, 9–12 секунд) здесь не годится:
       загрузка длится секунды, и игрок увидел бы пустое небо, а объект
       ушёл бы вместе с экраном, так и не войдя в него. Поэтому силуэт
       уже в кадре и только слегка сносится — «оно там всё это время было». */
    animation: boot-oddity 11s ease-out both;
}

@keyframes boot-oddity {
    from     { transform: translateX(0) rotate(-2deg); opacity: 0; }
    24%, 82% { opacity: 0.92; }
    to       { transform: translateX(calc(var(--u) * 7)) rotate(-1.4deg); opacity: 0.7; }
}

/* Тень от чего-то, что прошло выше кадра. Остаётся на земле, не в небе. */
.home-fx[data-kind="cloud_shadow"] {
    width: calc(var(--u) * 26);
    height: calc(var(--u) * 8);
    border-radius: 50%;
    background: rgba(24, 19, 31, 0.34);
    filter: blur(3px);
    animation: fx-drift 5.4s linear both;
}

@keyframes sky-pass {
    /* Лёгкий наклон: объект не летит по линейке, он проходит мимо. */
    from { transform: translateX(var(--sky-from, -32%)) rotate(-2.5deg); opacity: 0; }
    18%, 78% { opacity: 1; }
    to { transform: translateX(var(--sky-to, 112%)) rotate(-1deg); opacity: 0; }
}

@keyframes sky-blink {
    0%, 100% { opacity: 0; }
    8%, 16%  { opacity: 1; }
    12%      { opacity: 0; }
    26%, 34% { opacity: 1; }
    30%      { opacity: 0; }
    44%, 52% { opacity: 1; }
}

@keyframes sky-hold {
    from { opacity: 0; }
    25%, 70% { opacity: 1; }
    to { opacity: 0; }
}

@keyframes fx-rocket { from { transform: translate(-50%, -50%); opacity: 1; } to { transform: translate(-50%, -420%); opacity: 0; } }
@keyframes fx-drift  { from { transform: translate(-160%, -50%); opacity: 0; } 30%, 70% { opacity: 1; } to { transform: translate(160%, -80%); opacity: 0; } }
@keyframes fx-fade   { from { opacity: 0.9; } to { opacity: 0; } }
@keyframes fx-ring   { from { transform: translate(-50%, -50%) scale(0.4); opacity: 1; } to { transform: translate(-50%, -50%) scale(1.6); opacity: 0; } }

/* --- Погода и время суток: оверлеи, не влияющие на геймплей --- */

/*
 * ФОРМА, А НЕ КОД (этап 40, шаг 72). Правила написаны на то, ЧЕМ погода
 * рисуется (`data-particles`, `data-tint`), а не на её имя. До этого каждое
 * состояние было перечислено здесь по коду, а колонка `overlay` в каталоге
 * не читалась вовсе: заведённая миграцией погода не рисовала ничего,
 * а «Ветрено» (15 % времени) выглядело в точности как «Ясно».
 *
 * Граница ровно та же, что у сезонных частиц (раздел 281): новое СОСТОЯНИЕ —
 * строка в базе, новая ФОРМА — правка вёрстки. Формы перечислены
 * в `HomeService::WEATHER_PARTICLES` и `WEATHER_TINTS`, и у каждой обязано
 * быть правило здесь — это под тестом.
 *
 * Осадки едут ТРАНСФОРМОМ, а не `background-position`. Разница не в красоте:
 * сдвиг фона перерисовывает весь остров каждый кадр, и на слабом устройстве
 * это половина кадра. Измерено при замедлении ×12: с движущимся фоном
 * 37.0 FPS и 35.6 % кадров длиннее 33 мс, с тем же дождём на трансформе — 55.5
 * и 3.3 %.
 *
 * `inset` с запасом — плата за способ: слой ездит целиком, и без запаса
 * его край заезжал бы в кадр. Сцена режет по себе (`overflow: hidden`).
 *
 * Длительность петли ДЕЛИТСЯ на ветер: ветер — это скорость того, что уже
 * едет, а не отдельная картинка. При `wind: 0.5` дождь идёт прежние 0.7 с
 * (0.35 / 0.5), при 0.9 он злее. Переменную пишет `dressWeather()`, границы
 * (0.1–1.0) держит сервер — деления на ноль сюда не приезжает.
 */
.island__scene[data-particles="rain"]::before,
.island__scene[data-particles="clouds"]::before,
.island__scene[data-particles="gusts"]::before {
    content: '';
    position: absolute;
    inset: -32px;
    z-index: 1;
    pointer-events: none;
}

/*
 * Дождь: КАПЛИ в клетке, а не косая штриховка во весь кадр.
 *
 * `repeating-linear-gradient` мостится сам с собой, и полоса идёт через ВЕСЬ
 * слой: на кадре это не дождь, а царапины поверх острова — ровно та ошибка,
 * которую нашли глазами у сезонных частиц (`.island__event`) и закрыли там же.
 * У погоды она оставалась незакрытой: капля рисуется в клетке
 * (`radial-gradient` + `background-size`), а наклон даёт поворот всего слоя.
 *
 * Шаг петли кратен клетке (192 = 2 × 96 = 4 × 48), иначе узор не совпадает
 * сам с собой и на каждом повторе виден рывок — это под тестом. Слой повёрнут,
 * поэтому он шире кадра: иначе угол обрезается пустым треугольником.
 *
 * Здесь ДАЛЬНИЙ план — мелкая частая взвесь цвета неба. Ближний идёт своим
 * слоем ниже.
 */
.island__scene[data-particles="rain"]::before {
    /* ПЛОТНОСТЬ — это и есть разница между дождём и царапинами (шаг 135).
       До неё дальний план был синим по синему (alpha 0.24–0.34 цветом неба):
       над небом он не отличался от неба, над островом читался пылью. Замер
       по трём полосам кадра назвал цену вслух — 6.7 % пикселей острова при
       контроле 2.1 %, то есть втрое больше жизни острова и всё равно горсть
       отдельных чёрточек. Дождь не бывает горстью: он завеса, и завесу делает
       ЧИСЛО марок, а не размер каждой.

       Капля светлее неба, а не темнее: над тёмной скалой и над светлым небом
       работает одна и та же марка, иначе половина кадра остаётся сухой. */
    inset: -45%;
    background-image:
        radial-gradient(ellipse 1.7px 11px at 50% 50%,
            rgba(212, 240, 255, 0.74) 0 56%, rgba(24, 19, 31, 0.30) 60% 90%, transparent 94%),
        radial-gradient(ellipse 1.3px 8px at 50% 50%,
            rgba(140, 208, 248, 0.60) 0 56%, rgba(24, 19, 31, 0.22) 60% 90%, transparent 94%);
    background-size: 44px 48px, 61px 32px;
    background-position: 0 0, 23px 11px;
    background-repeat: repeat;
    animation: rain-fall calc(0.62s / var(--wind, 0.5)) linear infinite;
}

/*
 * Ближний план дождя (шаг 125). Капля крупнее, светлее и едет вдвое быстрее —
 * только разницей и читается расстояние: одна плоскость одинаковых капель
 * выглядит текстурой, наклеенной на картинку, а не погодой перед ней.
 *
 * У капли ТЁМНЫЙ КРАЙ вторым стопом того же градиента — это канон стиля
 * (cel-shading с толстым контуром), а не украшение: светлая капля без края
 * пропадает на светлом небе, то есть ровно там, где дождь заметнее всего.
 * Обводка нарисована В градиенте, потому что `filter` над движущимся запрещён.
 *
 * Шаг петли общий с дальним слоем — вторые кадры ради одной цифры не нужны.
 */
.island__scene[data-particles="rain"] .island__precip {
    /* Капель ДВЕ разной величины и на разных клетках (шаг 135). Одна марка
       на одной клетке — это решётка, а решётку глаз читает как текстуру,
       наклеенную на картинку: марки стоят ровными рядами, и ряды видно
       раньше, чем дождь. Две клетки, которые не делятся друг на друга,
       дают узор без видимого шага — тот же приём уже работал у ветра,
       и ровно поэтому ветер выглядел живее дождя.

       Обе высоты — делители шага петли (192), это под тестом. */
    inset: -45%;
    background-image:
        radial-gradient(ellipse 2.7px 17px at 50% 50%,
            rgba(240, 250, 255, 0.96) 0 54%, rgba(24, 19, 31, 0.62) 58% 90%, transparent 94%),
        radial-gradient(ellipse 2px 12px at 50% 50%,
            rgba(237, 223, 227, 0.82) 0 54%, rgba(24, 19, 31, 0.46) 58% 90%, transparent 94%);
    background-size: 74px 96px, 58px 64px;
    background-position: 0 0, 33px 27px;
    background-repeat: repeat;
    animation: rain-fall calc(0.33s / var(--wind, 0.5)) linear infinite;
}

/*
 * Облачность: тени облаков, ползущие по острову. Не сами облака — они
 * нарисованы на мастере: тень от того, что выше кадра, честнее второго
 * ряда облаков поверх нарисованного.
 */
.island__scene[data-particles="clouds"]::before {
    background-image:
        radial-gradient(38% 22% at 22% 26%, rgba(24, 19, 31, 0.13), transparent 70%),
        radial-gradient(30% 18% at 68% 44%, rgba(24, 19, 31, 0.10), transparent 70%);
    animation: cloud-drift calc(18s / var(--wind, 0.3)) linear infinite;
}

/*
 * Ветер: редкие длинные полосы, летящие поперёк кадра почти горизонтально.
 * Форма отличается от дождя не цветом, а НАКЛОНОМ и частотой — различать
 * погоду оттенком запрещено тем же правилом, что и статусы боя (шаг 6).
 */
.island__scene[data-particles="gusts"]::before {
    /* РЕДКИЙ — это число, а не слово (шаг 125). Прошлая клетка 176 × 84 давала
       на кадре 390 × 844 около шестидесяти длинных росчерков и больше двухсот
       коротких: ровными рядами поперёк острова это читалось не ветром,
       а царапинами по картинке — с этого правка и началась.

       Шаг 128 померил и обратную стену. Клетка 260 × 168 оставляла в НЕБЕ
       (а небо и есть то место, где погоду видно) один росчерк на кадр:
       сверка снимков «ясно / ветрено» дала 0.6 % отличающихся пикселей против
       4.9 % у дождя, то есть пятнадцать процентов времени «Ветрено» выглядело
       ровно как «Ясно» — тот же дефект, который закрывал шаг 72, только с
       другой стороны. Клетка вдвое плотнее: росчерков в кадре около двадцати,
       и работу по-прежнему делает мелкая труха, летящая с ними в одну сторону.

       Шаг 129 померил ТРЕТЬЮ стену. Плотность была верной, а форма — нет:
       росчерк в 92 точки на кадре шириной 390 идёт через четверть экрана,
       и ровные ряды таких штрихов поверх зданий читаются царапиной по
       картинке, а не воздухом. Ветер показывают не линии, а ТО, ЧТО ОН
       НЕСЁТ: штрих укорочен вчетверо (24 точки против 92) и стал следом
       за трухой, а не самостоятельной чертой, труха — крупнее и заметно
       гуще. Меняется ФОРМА, не количество: перепад со «Ясно» по небу
       остался в классе дождя.

       Тёмный край — как у капли: без него светлый штрих пропадает на светлом
       небе. Ширина клетки обязана делить шаг петли (520) — это под тестом.

       Шаг 135 померил четвёртую стену и назвал её тем же словом, что и у дождя:
       ПЛОТНОСТЬ. Форма уже была верной (короткий штрих плюс труха), но марок
       в кадре было около сорока — воздух не бывает из сорока предметов, и горсть
       точек над картинкой читается пылью, а не ветром. Марок стало втрое больше,
       и большинство из них — ШТРИХИ: направление показывает черта, а точка
       показывает только себя.

       Шаг 146 нашёл ПЯТУЮ стену, и она была не про плотность вовсе: ветер
       уезжал ЗА КРАЙ КАДРА. Петля везёт слой на 520 точек влево, а слоя
       хватало на 454 — то есть к концу каждого цикла дальнего плана в кадре
       не оставалось НИ ОДНОЙ марки, и ветер мигал целиком. Отсюда и четыре
       правки плотности подряд: замер снимал случайную фазу и честно говорил
       «мало», а мало было не марок, а слоя.

       Запас считается, а не назначается: слой обязан накрывать сцену
       в КРАЙНЕЙ точке петли и с учётом поворота (полукадр × sin 7°).
       На 390 × 844 это 765 точек полуширины — отсюда −580px по горизонтали.
       По вертикали ветер не едет вовсе, и −32px базового блока хватает:
       лишняя высота — это лишние пиксели на композиторе, а не запас. */
    inset: -32px -580px;
    background-image:
        radial-gradient(ellipse 14px 2.2px at 50% 50%,
            rgba(237, 223, 227, 0.92) 0 54%, rgba(24, 19, 31, 0.66) 58% 88%, transparent 92%),
        radial-gradient(ellipse 4.2px 3.2px at 50% 50%,
            rgba(237, 223, 227, 0.96) 0 52%, rgba(24, 19, 31, 0.78) 56% 88%, transparent 92%),
        radial-gradient(ellipse 3.2px 2.4px at 50% 50%,
            rgba(216, 120, 32, 0.92) 0 52%, rgba(24, 19, 31, 0.72) 56% 88%, transparent 92%);
    background-size: 40px 58px, 65px 74px, 52px 92px;
    background-position: 0 0, 20px 33px, 9px 21px;
    background-repeat: repeat;
    animation: gust-blow calc(1.9s / var(--wind, 0.9)) linear infinite;
}

/*
 * Ближний порыв: крупная труха, летящая заметно быстрее дальней. Она и делает
 * из ряби ветер — глазу нужно за что-то зацепиться, а два одинаковых слоя
 * дают только рябь.
 *
 * До шага 129 здесь лежал росчерк в 220 точек — больше половины кадра.
 * Он и был тем, что видно на снимке как белые линии поперёк зданий: длинная
 * ровная черта поверх рисунка читается царапиной, потому что в самом рисунке
 * таких линий нет ни одной. Ближним планом стал предмет, а не линия: щепка
 * с тёмным краем и коротким следом за ней.
 */
.island__scene[data-particles="gusts"] .island__precip {
    /* Тот же запас, что у дальнего плана, и по той же причине: слой едет
       на 520 точек, а −45 % ширины кадра это 175. Ближний план уезжал
       не целиком, но к концу цикла от него оставалась левая шестая часть
       кадра — щепки сдувало в угол и возвращало рывком. */
    inset: -32px -580px;
    background-image:
        radial-gradient(ellipse 26px 2.4px at 62% 50%,
            transparent 0 6%, rgba(237, 223, 227, 0.82) 30% 78%, rgba(24, 19, 31, 0.42) 84%, transparent 92%),
        radial-gradient(ellipse 5.6px 4px at 50% 50%,
            rgba(237, 223, 227, 0.98) 0 50%, rgba(24, 19, 31, 0.88) 54% 88%, transparent 92%);
    /* Ширина клетки — делитель шага петли (520), иначе на каждом повторе
       узор не совпадает сам с собой и в кадре виден рывок. 520 давало один
       ближний предмет на три кадра — зацепиться глазу было не за что.
       Клетка 130 × 118 давала около двадцати марок на кадр — вдвое больше
       и всё равно горсть; клетки стали разными (65 и 104), поэтому решётки
       не видно: один узор не совпадает с другим до пятого повтора. */
    background-size: 65px 96px, 104px 62px;
    background-position: 0 0, 22px 0;
    background-repeat: repeat;
    animation: gust-blow calc(1.0s / var(--wind, 0.9)) linear infinite;
}

/*
 * Вспышка дальней грозы. Была фильтром на всей сцене — и это третья строка
 * той же болезни: фильтр на родителе не даёт композитору работать со всем,
 * что под ним движется, а под сценой движется живой остров целиком. Стоило
 * это дорого и постоянно, хотя сама вспышка занимает 2 % цикла: анимация
 * `filter` идёт каждый кадр все девять секунд, даже когда значение `none`.
 * Измерено при ×12: со вспышкой-фильтром 50.1 FPS и 11.6 % кадров длиннее
 * 33 мс, без неё — 58.2 и 2.0 %.
 *
 * Честно про цену замены: цвет НЕ совпадает, в отличие от неба. `brightness`
 * умножает, слой поверх — подмешивает, и одно другим точно не выражается.
 * Вспышка стала чуть мягче в светлом и заметнее в тени. Вне вспышки кадр
 * прежний до пикселя, и это проверено.
 */
.island__flash {
    position: absolute;
    inset: 0;
    /* Выше всего, что живёт ВНУТРИ сцены (следы набега — 10, говорящий герой —
       11, подсказка острова — 30): фильтр накрывал сцену целиком, и слой обязан
       накрывать столько же. За края сцены вспышка не выходит — шапка и навигация
       лежат снаружи. */
    z-index: calc(var(--z-hud) + 2);
    pointer-events: none;
    background: var(--cream);
    opacity: 0;
}

/* Вспышку объявляет САМА погода (`lightning` в её строке), а не код состояния:
   вторая гроза, заведённая миграцией, засверкает без правки стилей. */
.island__scene[data-lightning="1"] .island__flash {
    animation: storm-flash 9s ease-in-out infinite;
}

/*
 * ТОН. Отдельный слой, а не псевдоэлемент сцены: `::before` занят осадками,
 * `::after` — временем суток, и «облачно с дождём» иначе не собирается.
 *
 * Тон лежит НАД картинкой и ПОД кнопками зон: подкрашивать нажимаемое
 * запрещено правилом доступности (шаг 6) — вид меняется, читаемость нет.
 * Плотность нарочно мала: раздел 345 требует, чтобы здания оставались
 * узнаваемыми в любую погоду.
 */
/*
 * Слой следов набега. Как и слой события ниже, он стоит в разметке ВСЕГДА —
 * пустым у того, кого не грабили, — поэтому его пустая геометрия принадлежит
 * ядру, а не факту (этап 42, шаг 124): без правила слой встаёт в поток
 * обычным `div` и берёт нажатия. Сами следы приезжают по факту
 * в `css/screens/island-raided.css`.
 */
.island__damage {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: var(--z-world);
}

/*
 * Слой сезонного оформления. Сама РАЗМЕТКА слоя стоит в кадре всегда, поэтому
 * его пустая геометрия — ядро, а не факт (этап 42, шаг 124): без правила он
 * встаёт в поток как обычный `div` и берёт нажатия. Всё, что слой РИСУЕТ,
 * приезжает по факту в `css/screens/island-event.css`.
 *
 * Своего `z-index` здесь нет намеренно: порядок задаёт разметка — слой стоит
 * после живого острова и ПЕРЕД кнопками зон, поэтому частицы летят перед
 * героями и за зданиями, которые игрок нажимает.
 */
.island__event {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
}

.island__weather {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
}

/*
 * Ближний план осадков. Слой стоит в разметке ВСЕГДА — пустым в ясную погоду
 * и в облачность, — поэтому его пустая геометрия принадлежит ядру (правило
 * шага 124): без неё он встал бы в поток обычным `div` и взял бы нажатия.
 * Что он рисует, объявляет форма погоды выше.
 *
 * Лежит ПОСЛЕ тона и до вспышки: ближайшее к камере тон атмосферы не красит,
 * а гроза бьёт по всему кадру.
 */
.island__precip {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
}

.island__scene[data-tint="cool"] .island__weather {
    background: linear-gradient(180deg, rgba(64, 200, 248, 0.16), transparent 60%);
}

.island__scene[data-tint="rain"] .island__weather {
    background: linear-gradient(180deg, rgba(36, 29, 87, 0.22), rgba(64, 200, 248, 0.08) 70%);
}

.island__scene[data-tint="storm"] .island__weather {
    background: linear-gradient(180deg, rgba(36, 29, 87, 0.34), rgba(24, 19, 31, 0.12) 65%);
}

/* Наклон живёт В ПЕТЛЕ, а не в отдельном `transform`: второе объявление
   перебило бы первое, и капли шли бы отвесно. Шаг — две клетки. */
@keyframes rain-fall {
    from { transform: rotate(12deg) translate3d(0, -192px, 0); }
    to   { transform: rotate(12deg) translate3d(0, 0, 0); }
}
/* Тень облака ползёт вбок и чуть вниз — так же, как едут облака на мастере. */
@keyframes cloud-drift { from { transform: translate3d(-6%, 0, 0); } to { transform: translate3d(6%, 2%, 0); } }
/* Порыв идёт поперёк кадра: почти горизонтально и заметно дальше дождя. */
@keyframes gust-blow {
    from { transform: rotate(-7deg) translate3d(0, 0, 0); }
    to   { transform: rotate(-7deg) translate3d(-520px, 0, 0); }
}
@keyframes storm-flash { 0%, 92%, 100% { opacity: 0; } 94% { opacity: 0.22; } 96% { opacity: 0; } 97% { opacity: 0.13; } }

/* Ночь — не синий фильтр (раздел 126): окна светятся, зоны подсвечены изнутри,
   здания остаются узнаваемыми. */
.island__scene[data-time-of-day="NIGHT"] .zone[data-state="built"] {
    box-shadow: 0 4px 0 var(--ink-shadow), 0 0 calc(var(--u) * 2) rgba(253, 197, 1, 0.45);
}

.island__scene[data-time-of-day="NIGHT"] .actor__body { filter: brightness(0.82) saturate(0.85); }

/* Вечер — тёплые лампы (раздел 65). Не ночь: свет уже включили, но остров
   ещё виден целиком, поэтому свечение слабее и оранжевее ночного. */
.island__scene[data-time-of-day="DUSK"] .zone[data-state="built"] {
    box-shadow: 0 4px 0 var(--ink-shadow), 0 0 calc(var(--u) * 1.2) rgba(216, 120, 32, 0.4);
}

/* Реактор светится тем сильнее, чем полнее хранилище (раздел 136). */
.zone[data-activity="PRODUCING"] { box-shadow: 0 4px 0 var(--ink-shadow), 0 0 calc(var(--u) * 1.4) rgba(64, 200, 248, 0.5); }
.zone[data-activity="FULL"]      { box-shadow: 0 4px 0 var(--ink-shadow), 0 0 calc(var(--u) * 2.2) rgba(253, 197, 1, 0.75); }
.zone[data-activity="WORKING"]::after {
    content: '';
    position: absolute;
    left: 50%;
    top: calc(var(--u) * -1.6);
    width: calc(var(--u) * 2);
    height: calc(var(--u) * 2);
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(237, 223, 227, 0.5), transparent 70%);
    animation: smoke-rise 2.8s ease-out infinite;
}

/*
 * ЗДАНИЕ ЗОВЁТ (ТЗ, часть 09, раздел 323: «Когда World War — горит War Room.
 * Когда Event — оживает Event Platform. Когда его атаковали — Defense Control
 * показывает последствия»).
 *
 * Три правила подачи:
 *   · РАЗЛИЧИЕ ФОРМОЙ, А НЕ ЦВЕТОМ (docs/ACCESSIBILITY.md): у каждого повода
 *     свой силуэт значка, и обводка у всех одна — чернильная;
 *   · НИ ОДНОЙ БЕСКОНЕЧНОЙ АНИМАЦИИ: значок стоит. Мигающая метка — это петля
 *     на главном экране, а раздел 234 запрещает постоянную работу GPU в фоне;
 *   · ЗНАЧОК НЕ БЕРЁТ НАЖАТИЙ: кнопка здесь одна, и это само здание.
 */
.zone[data-activity="WAR_SPOILS"],
.zone[data-activity="BOSS_SKY"],
.zone[data-activity="BOSS_SPOILS"],
.zone[data-activity="EVENT_LIVE"],
.zone[data-activity="QUEST_READY"],
.zone[data-activity="CLAN_REQUESTS"],
.zone[data-activity="HERO_READY"],
.zone[data-activity="RAID_NEWS"] {
    border-color: var(--gold);
    box-shadow: 0 4px 0 var(--ink-shadow), 0 0 calc(var(--u) * 1.8) rgba(253, 197, 1, 0.55);
}

.zone[data-activity="WAR_SPOILS"]::after,
.zone[data-activity="BOSS_SKY"]::after,
.zone[data-activity="BOSS_SPOILS"]::after,
.zone[data-activity="EVENT_LIVE"]::after,
.zone[data-activity="QUEST_READY"]::after,
.zone[data-activity="CLAN_REQUESTS"]::after,
.zone[data-activity="HERO_READY"]::after,
.zone[data-activity="RAID_NEWS"]::after {
    content: '';
    position: absolute;
    top: calc(var(--u) * -0.9);
    right: calc(var(--u) * -0.9);
    width: max(14px, calc(var(--u) * 1.8));
    height: max(14px, calc(var(--u) * 1.8));
    background: var(--gold);
    pointer-events: none;
}

/* Итог кампании ждёт — ромб медали. */
.zone[data-activity="WAR_SPOILS"]::after {
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

/* Босс в небе — клин, направленный вниз: над островом что-то висит. */
.zone[data-activity="BOSS_SKY"]::after {
    clip-path: polygon(0 0, 100% 0, 50% 100%);
    background: var(--danger);
}

/*
 * Разобранный босс оставил незабранное — гайка: в небе его больше нет,
 * а детали лежат. Форма своя, а не тот же клин другого цвета: различать
 * состояния цветом нельзя.
 */
.zone[data-activity="BOSS_SPOILS"]::after {
    clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}

/* Событие идёт — флажок на площадке. */
.zone[data-activity="EVENT_LIVE"]::after {
    clip-path: polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%);
}

/*
 * Приказ выполнен, награда ждёт — сложенный лист с загнутым углом: бумага,
 * которую забирают в штабе. Форма своя: медаль кампании, гайка босса и флажок
 * события уже заняты, а различать поводы цветом нельзя.
 */
.zone[data-activity="QUEST_READY"]::after {
    clip-path: polygon(0 0, 68% 0, 100% 32%, 100% 100%, 0 100%);
}

/*
 * В экипаж просятся — крест прибавления: кто-то хочет встать в состав,
 * и решение ждёт капитана. Форма своя и не поворот чужой: клин босса
 * и ромб кампании читались бы тем же силуэтом под другим углом.
 */
.zone[data-activity="CLAN_REQUESTS"]::after {
    clip-path: polygon(38% 0, 62% 0, 62% 38%, 100% 38%, 100% 62%, 62% 62%,
        62% 100%, 38% 100%, 38% 62%, 0 62%, 0 38%, 38% 38%);
}

/*
 * Герой готов к улучшению — стрелка вверх: «стало больше». Форма своя и
 * не поворот чужой: клин босса смотрит вниз и означает висящее в небе,
 * а здесь речь о росте.
 */
.zone[data-activity="HERO_READY"]::after {
    clip-path: polygon(50% 0, 100% 48%, 72% 48%, 72% 100%, 28% 100%, 28% 48%, 0 48%);
}

/* Пришли гости — след когтя: три полосы. */
.zone[data-activity="RAID_NEWS"]::after {
    clip-path: polygon(0 0, 26% 0, 26% 100%, 0 100%, 0 0, 37% 0, 63% 0, 63% 100%, 37% 100%,
        37% 0, 74% 0, 100% 0, 100% 100%, 74% 100%);
    background: var(--danger);
}

/* --- Карточка мира и тост --- */

/*
 * Карточка живого мира и карточка зоны — это один и тот же объект интерфейса
 * (.world-card). Отдельной геометрии у них нет намеренно: на острове должна быть
 * ровно одна нижняя полоса, иначе игрок учится двум разным интерфейсам подряд.
 */

.home-toast {
    position: absolute;
    left: 50%;
    top: calc(var(--hud-total) + var(--gap-1));
    z-index: var(--z-hud);
    transform: translateX(-50%);
    margin: 0;
    padding: calc(var(--u) * 0.9) calc(var(--u) * 2);
    border: var(--stroke) solid var(--ink);
    border-radius: 999px;
    background: var(--gold);
    color: var(--ink);
    font-family: var(--font-text);
    font-weight: 900;
    font-size: max(12px, calc(var(--u) * 1.5));
    animation: bubble-in var(--t-fast) var(--ease) both;
}

.btn--small {
    /* Пол под палец (шаг 146). «Маленькая» она по НАДПИСИ, а не по мишени:
       этим классом сделаны почти все «Назад» игры, и сорок точек — промах
       мимо мерки, которой меряются маркеры зон. */
    min-height: max(44px, calc(var(--u) * 4.2));
    padding: 0 calc(var(--u) * 1.8);
    font-size: max(12px, calc(var(--u) * 1.4));
}

/* HUD притухает в тишине и возвращается по касанию (ТЗ, часть 17, разделы 281–282).
   Ничего не прячется совсем: это прозрачность, а не удаление. */
body.is-ambient .hud,
body.is-ambient .nav {
    /* Притухание мягкое: на 0.45 главная кнопка боя начинала читаться
       как выключенная, а это уже не «HUD стал менее навязчивым», а поломка. */
    opacity: 0.68;
    transition: opacity 0.6s var(--ease);
}

/*
 * Режим фото: интерфейс убирается по явной кнопке (раздел 283).
 *
 * Прячем видимостью, а не прозрачностью: у карточек мира есть анимация появления
 * с `fill-mode: both`, и она перебивает любое `opacity` из обычного правила —
 * панель оставалась на снимке. Выход из режима остаётся на экране всегда:
 * режим без единого способа выйти — это не режим, а ловушка.
 */
body.is-photo-mode .hud,
body.is-photo-mode .nav,
body.is-photo-mode .zone,
body.is-photo-mode .world-card {
    visibility: hidden;
    pointer-events: none;
}

.photo-exit {
    position: absolute;
    right: var(--safe-right);
    bottom: calc(var(--safe-bottom) + var(--gap-2));
    z-index: calc(var(--z-hud) + 1);
}

body:not(.is-photo-mode) .photo-exit { display: none; }

/* Пропуск возвращения (ТЗ, часть 27, раздел 39). Это единственный элемент
   интерфейса, который вообще появляется на возвращении, поэтому он стоит
   ПОД шапкой, а не в её углу: кошелёк и уровень никуда не деваются на время
   сцены, и накрывать их кнопкой нельзя. */
.return-skip {
    position: absolute;
    right: calc(var(--safe-right) + var(--u) * 2);
    top: calc(var(--hud-total) + var(--u));
    z-index: calc(var(--z-hud) + 1);
    padding: calc(var(--u) * 0.8) calc(var(--u) * 1.8);
    border: 3px solid var(--ink);
    border-radius: 999px;
    background: rgba(36, 29, 87, 0.78);
    font-family: var(--font-display);
    font-size: max(10px, calc(var(--u) * 1.3));
    color: var(--cream);
    cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
    .actor,
    .actor__body,
    .home-prop,
    .traffic,
    .home-fx,
    .home-sky-fx,
    .island__scene[data-particles],
    /* Осадки живут на ::before, а вспышка грозы — отдельным слоем: правило
       на самой сцене их не останавливало. Дождь остаётся текстурой,
       гроза — небом без стробоскопа (разделы 240, 255, 256). Тон погоды
       останавливать не нужно: он никогда и не двигался. */
    .island__scene[data-particles]::before,
    /* Ближний план осадков — второй слой той же погоды, и останавливается
       он тем же правилом: капли остаются текстурой, а не стоят на месте
       только наполовину. */
    .island__scene[data-particles] .island__precip,
    .island__flash { animation: none !important; transition: none !important; }
}

/* Узкая рамка: реплики героев не имеют права вылезти за край кадра. */
@media (max-width: 360px) {
    .actor { width: 28px; height: 28px; }
    .actor__bubble { font-size: 10px; max-width: calc(var(--u) * 34); }
    .world-card { padding: calc(var(--u) * 1.2) calc(var(--u) * 1.6); }
}

/* Меньше движения — меньше движения. Сцены при этом не играют вовсе
   (это решает js/raid-scene.js), а следы остаются статичными метками. */
@media (prefers-reduced-motion: reduce) {
    .raid-scene__cloud,
    .raid-scene__ship,
    .raid-scene__thrust,
    .raid-scene__banner,
    .raid-scene__hero,
    .raid-scene__cargo,
    .island-cargo__crate,
    .damage::before,
    .damage::after {
        animation: none !important;
    }
}

/* --- Ресурс летит от здания в шапку --- */

.fly-coin {
    position: fixed;
    z-index: calc(var(--z-hud) + 1);
    width: calc(var(--u) * 3.2);
    height: calc(var(--u) * 3.2);
    margin: calc(var(--u) * -1.6) 0 0 calc(var(--u) * -1.6);
    border: 2px solid var(--ink);
    border-radius: 50%;
    background: radial-gradient(circle at 40% 35%, #FFE47A, var(--gold));
    animation: fly-coin 0.75s var(--ease) var(--delay, 0ms) both;
    pointer-events: none;
}

@keyframes fly-coin {
    from { opacity: 0; transform: translate(0, 0) scale(0.6); }
    20%  { opacity: 1; }
    to   { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.5); }
}

/* Меньше движения — обучение остаётся, движение уходит. Сцены при этом
   сокращаются целиком в js/ftue.js. */
@media (prefers-reduced-motion: reduce) {
    .intro__cloud,
    .intro__tracers i,
    .intro__transport,
    .intro__thrust,
    .intro__smoke,
    .intro__hold::after,
    .ftue-reveal__burst,
    .ftue-pointer::after,
    .ftue-world__goal,
    [data-ftue="glow"]::after,
    [data-ftue="point"]::after,
    body.is-ftue-reveal .island__scene {
        animation: none !important;
    }
}

/* Подсказка в шторке целей: строка пояснения под названием. */
.sheet__hint {
    flex-basis: 100%;
    color: rgba(237, 223, 227, 0.72);
    font-family: var(--font-text);
    font-size: max(11px, calc(var(--u) * 1.5));
    line-height: 1.3;
}

.sheet__row[data-main] { border-color: var(--gold); }

.sheet__note {
    margin: 0;
    color: var(--sky);
    font-family: var(--font-text);
    font-weight: 700;
    font-size: max(11px, calc(var(--u) * 1.5));
    line-height: 1.35;
}

/* Трофей первой недели и контейнер с грузом — те же объекты памяти острова. */
.home-prop--memory[data-icon="crate"] {
    border-radius: 4px;
    background: linear-gradient(180deg, var(--dogs-orange), #8A4A10);
}

@media (prefers-reduced-motion: reduce) {
    .reveal,
    .shield-beacon {
        animation: none !important;
    }
}

/* --- Вход на борт с острова ------------------------------------------------ */

.dock-board {
    position: absolute;
    z-index: var(--z-world);
    transform: translate(-50%, -100%);
    padding: calc(var(--u) * 0.5) calc(var(--u) * 1.4);
    border: 3px solid var(--ink);
    border-radius: 999px;
    background: var(--gold);
    font-family: var(--font-display);
    font-size: max(9px, calc(var(--u) * 1.1));
    color: var(--ink);
    cursor: pointer;
    animation: dock-board-bob 3.2s ease-in-out infinite;
}

/*
 * Мишень (шаг 146). Дверь на КОРАБЛЬ — целый раздел игры — была 54 × 20 точек
 * при поле в 44: пилюля мелкая потому, что её размер считается от `--u`,
 * а надпись в ней всего два слова. Растить саму пилюлю нельзя — золотое пятно
 * в 44 точки высотой закрыло бы половину причала, — поэтому растёт ЗАПАС,
 * и растёт он ВВЕРХ: снизу в двенадцати точках стоит маркер зоны, а сверху
 * до ближайшего сорок три. Прозрачный `::after` — часть кнопки для попадания
 * пальцем и ничто для глаза.
 */
.dock-board::after {
    content: '';
    position: absolute;
    /* 28, а не 24: пилюля считается от `--u`, и на самом тесном телефоне
       она 19 точек высотой — двадцати четырёх не хватало на один пиксель. */
    inset: -28px 0 0 0;
}

@keyframes dock-board-bob {
    0%, 100% { transform: translate(-50%, -100%); }
    50%      { transform: translate(-50%, -118%); }
}

@media (prefers-reduced-motion: reduce) {
    .dock-board { animation: none; }
}

/* --- Пустой причал --------------------------------------------------------- */

/* Корабля дома нет. Не «недоступно», не серая кнопка: место, где его сейчас
   нет, и строка о том, куда он ушёл (раздел 232). Нажимать нечего. */
.dock-empty {
    position: absolute;
    z-index: var(--z-world);
    transform: translate(-50%, -100%);
    margin: 0;
    padding: calc(var(--u) * 0.5) calc(var(--u) * 1.2);
    border: 3px solid var(--ink);
    border-radius: 999px;
    background: rgba(36, 29, 87, 0.86);
    font-family: var(--font-display);
    font-size: max(9px, calc(var(--u) * 1.05));
    color: rgba(237, 223, 227, 0.82);
    text-align: center;
    white-space: nowrap;
    pointer-events: none;
}

.dock-empty__where {
    display: block;
    font-family: var(--font-text);
    font-weight: 700;
    font-size: max(8px, calc(var(--u) * 0.95));
    color: var(--sky);
}

/* ============================================================
   ВИД ПЕРЕСКАЗА (этап 37, шаг 20; ТЗ, часть 27, разделы 128–134)

   Прошедшее обязано выглядеть не так, как настоящее, — это единственный
   способ их не спутать (раздел 131). Язык взят из раздела 132 поимённо:
   бумага, снимок, скотч, слегка сниженная интенсивность. Чёрно-белого здесь
   нет и не будет — раздел 134 запрещает его прямым текстом и требует
   сохранить цвета персонажей.

   Класс ставит одна функция `markRecap()` (`js/recap.js`), и только по слову
   сервера. Живой мир полноцветный (раздел 133); умолчания «приглушить»
   не существует.
   ============================================================ */

.is-recap {
    /* Тёплая бумага, а не выцветшая плёнка: цвет остаётся цветом,
       падает только интенсивность. */
    filter: sepia(0.24) saturate(0.9) contrast(0.95) brightness(1.03);
}

/* Скотч по верхним углам разворота. Псевдоэлементы вынесены из потока —
   иначе grid книги посчитал бы их строками и разъехался. */
.logbook.is-recap .logbook__book { position: relative; }

.logbook.is-recap .logbook__book::before,
.logbook.is-recap .logbook__book::after {
    content: "";
    position: absolute;
    top: calc(var(--u) * -1.4);
    width: calc(var(--u) * 12);
    height: calc(var(--u) * 3.4);
    background: rgba(237, 223, 227, 0.55);
    box-shadow: inset 0 0 0 1px rgba(24, 19, 31, 0.18);
    pointer-events: none;
}

.logbook.is-recap .logbook__book::before {
    left: calc(var(--u) * -2);
    transform: rotate(-24deg);
}

.logbook.is-recap .logbook__book::after {
    right: calc(var(--u) * -2);
    transform: rotate(24deg);
}

/* Бумага разворота из реестра ассетов (раздел 134: «reusable visual
   treatment»). Светлая подложка поверх — чтобы текстура осталась текстурой,
   а не спорила с рукописью. */
.logbook__page[data-art="yes"] {
    background:
        linear-gradient(180deg, rgba(243, 231, 214, 0.58) 0%, rgba(226, 210, 188, 0.68) 100%),
        var(--recap-paper) center / cover no-repeat;
}

/* ЗВУК В «ЕЩЁ» (этап 38, шаг 3).

   Ползунок сделан пухлым и толстым не ради красоты: настройку звука крутят
   пальцем на ходу, и тонкая полоска шириной в волос промахивается. Высота
   строки та же, что у остальных пунктов шторки, — меню не рассыпается. */
.sheet__row--slider {
    cursor: pointer;
    gap: var(--gap-2);
}

.sheet__slider {
    flex: 1 1 auto;
    min-width: calc(var(--u) * 24);
    height: calc(var(--u) * 3);
    margin: 0;
    appearance: none;
    background: linear-gradient(90deg, var(--sky) 0%, var(--gold) 100%);
    border: var(--stroke) solid var(--ink);
    border-radius: 999px;
}

.sheet__slider::-webkit-slider-thumb {
    appearance: none;
    width: calc(var(--u) * 4);
    height: calc(var(--u) * 4);
    border: var(--stroke) solid var(--ink);
    border-radius: 50%;
    background: var(--cream);
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.85);
}

.sheet__slider::-moz-range-thumb {
    width: calc(var(--u) * 4);
    height: calc(var(--u) * 4);
    border: var(--stroke) solid var(--ink);
    border-radius: 50%;
    background: var(--cream);
}

/* ============================================================
   УРОВНИ КАЧЕСТВА (этап 40, шаг 5; ТЗ, часть 03, раздел 35; часть 05, 230–234)
   Уровень ставит js/quality.js атрибутом на <html>; сервер про него не знает.
   Правило главы: гаснет ДВИЖЕНИЕ, а не информация. Дождь остаётся текстурой,
   маяк щита — маяком, дым повреждения — дымом; замирает только их петля.
   Одноразовое (полёт монет, проходящий дирижабль, странности неба) не
   трогается: оно заканчивается само и постоянной цены не имеет.
   Что именно сколько стоило — docs/perf/PERF_AUDIT.md, главы 5 и 9.
   ============================================================ */

/*
 * БАЛАНС: снимается то, что измеримо роняло кадры на замедлении ×12,
 * — осадки (3.3% длинных кадров даже трансформом), вспышка грозы (2.0%)
 * и ближний, самый быстрый слой облаков. Характер мира на месте: дальние
 * облака едут, живой слой ходит, бой подаётся целиком.
 */
html[data-quality="balanced"] .sky__layer--near,
html[data-quality="battery"] .sky__layer--near,
/* Осадки перечислены ФОРМОЙ, а не кодом состояния: заведённая завтра погода
   обязана гаснуть в балансе сама, без правки этого списка (шаг 72). Текстура
   остаётся видна — гаснет движение, а не информация. */
html[data-quality="balanced"] .island__scene[data-particles]::before,
html[data-quality="battery"] .island__scene[data-particles]::before,
/* Ближний план осадков — та же петля и та же цена: гаснет вместе с дальним,
   иначе баланс снимал бы половину измеренного. */
html[data-quality="balanced"] .island__scene[data-particles] .island__precip,
html[data-quality="battery"] .island__scene[data-particles] .island__precip,
html[data-quality="balanced"] .island__scene[data-lightning="1"] .island__flash,
html[data-quality="battery"] .island__scene[data-lightning="1"] .island__flash,
/* Сезонные частицы — те же осадки: бесконечная петля поверх всей сцены.
   Текстура остаётся, и событие по-прежнему видно (раздел 282 требует, чтобы
   без оформления оставался обычный остров, — а не чтобы событие пропадало). */
html[data-quality="balanced"] .island__event[data-preset]::before,
html[data-quality="battery"] .island__event[data-preset]::before {
    animation: none;
}

/*
 * ЭКОНОМИЯ: ничего не крутится бесконечно. Раздел 234 запрещает постоянную
 * работу GPU в фоне, а всё перечисленное — петли `infinite`, которые не
 * останавливаются никогда. Их базовое состояние видно и без анимации:
 * `animation: none` возвращает элементу его собственные opacity и transform.
 */
html[data-quality="battery"] .sky__layer,
html[data-quality="battery"] .home-prop--interactive,
html[data-quality="battery"] .home-prop--visitor,
html[data-quality="battery"] .home-prop--trace[data-object="flag"],
html[data-quality="battery"] .home-prop--trace[data-object="enemy_flag"],
html[data-quality="battery"] .home-prop--trace[data-object="pennant"],
html[data-quality="battery"] .home-fx[data-kind="damage"],
html[data-quality="battery"] .zone[data-activity="WORKING"]::after,
html[data-quality="battery"] .damage[data-code="SMOKE_PLUME"]::before,
html[data-quality="battery"] .damage[data-code="SMOKE_PLUME"]::after,
html[data-quality="battery"] .damage[data-code="SPARK_CABLE"]::before,
html[data-quality="battery"] .damage[data-code="SPARK_CABLE"]::after,
html[data-quality="battery"] .island-cargo__crate,
html[data-quality="battery"] .shield-beacon,
/* Облака за бортом корабля — такая же бесконечная петля, как небо острова
   (ТЗ, часть 23, раздел 257: в слабом режиме гасятся фоновые объекты). */
html[data-quality="battery"] .airship__cloud,
html[data-quality="battery"] .dock-board {
    animation: none;
}
