/* ============================================================
   «Звенья истории» — дизайн-код «Спокойный интроверт» v19
   Базовые переменные, сброс и игровое поле
   ============================================================ */

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Глобальный запрет выделения текста в интерфейсе игры.
   Раньше текст выделялся на канвасе/таймлайне/кнопках — при перетаскивании
   поля мышкой случайно захватывался текст, и панорама «застревала».
   Выделение разрешено ТОЛЬКО в теле статьи (.wiki-body) и редакторе фактов
   (.rt-editor) — там оно нужно для копирования. Поля ввода ниже разрешены явно. */
html, body, #viewport, #world, #svg-lines, svg, #nodes-layer, #epoch-labels,
.node, .conn-label, #timeline, #timeline-track, .tl-epoch, .tl-name, .tl-years,
.tl-item, .tl-face, .epoch-label, #topbar, .toast, .gate-badge,
.admin-panel, .admin-float, #edge-popover, .canvas-menu, .help-window {
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
/* Поля ввода и редактируемый контент — выделение обязательно */
input, textarea, select, [contenteditable] {
    -webkit-user-select: text;
    -moz-user-select: text;
    -ms-user-select: text;
    user-select: text;
    -webkit-touch-callout: default;
}
/* Тело статьи и редактор фактов — пользователь копирует текст статьи */
.wiki-body, .rt-editor {
    -webkit-user-select: text;
    -moz-user-select: text;
    -ms-user-select: text;
    user-select: text;
    -webkit-touch-callout: default;
}

/* Сенсорные устройства: manipulation убирает задержку клика и двойной
   тап-зум на кнопках/окнах (iOS игнорирует user-scalable=no в meta
   viewport), overscroll-behavior гасит pull-to-refresh при прокрутке
   статьи. Само игровое поле (#viewport) сохраняет touch-action: none —
   жесты карты не должны доставаться браузеру. */
html, body {
    touch-action: manipulation;
    overscroll-behavior: none;
    -webkit-tap-highlight-color: transparent;
}

:root {
    /* Нейтрали */
    --bg: #F7F4EF;
    --bg-elevated: #FFFDF9;
    --bg-soft: #F9F6F1;
    --text: #2C2A26;
    --text-secondary: #6B6560;
    --border: #E8E2D9;
    --ink: #2C2A26;
    --muted: #6B6560;

    /* Масштаб кругов */
    --node-scale: 1.25;

    /* Акценты сфер */
    --sp-political: #C4A574;
    --sp-foreign: #7A9BB5;
    --sp-military: #B07A6A;
    --sp-cultural: #8B7B9E;
    --sp-economic: #8AAA6E; /* Общественно-экономическая (объединённая) */
    --sp-popular: #D32F2F; /* Народные движения — ярко-красный */
    --sp-social: #8AAA6E; /* Общественно-экономическая — тот же цвет */

    /* Состояния */
    --ghost-line: rgba(180, 175, 165, 0.9);
    --read-gold: #D4B896;
    --selected-ink: #3A3530;
    --gold: #C9A96E;
    --green: #3E8F57;

    /* Кнопки и UI */
    --btn-primary: #3A3530;
    --btn-secondary: #EDE8E0;
    --accent: #7A9BB5;

    --font: 'Noto Sans', 'Segoe UI', Roboto, Arial, sans-serif;
    --shadow-card: 0 2px 12px rgba(44, 42, 38, 0.06);
    --shadow-lift: 0 6px 20px rgba(44, 42, 38, 0.10);
    --shadow-modal: 0 20px 50px rgba(44, 42, 38, 0.16);
}

body {
    font-family: var(--font);
    background: var(--canvas-bg, var(--bg));
    overflow: hidden;
    color: var(--text);
    font-size: 15px;
}

/* ---------- Поле ---------- */
#viewport {
    position: fixed; top: 56px; left: 0; right: 0; bottom: 0;
    overflow: hidden; touch-action: none;
    /* Тёплая «атласная» бумага терра-инкогниты: возрастные разводы сепии */
    background-color: var(--canvas-bg, var(--bg));
    background-image:
        radial-gradient(ellipse at 20% 30%, rgba(196, 165, 116, 0.11), transparent 55%),
        radial-gradient(ellipse at 80% 70%, rgba(160, 130, 85, 0.09), transparent 50%),
        radial-gradient(ellipse at 65% 15%, rgba(150, 118, 74, 0.06), transparent 45%),
        radial-gradient(ellipse at 50% 50%, rgba(255, 255, 255, 0.35), transparent 70%);
}
#world {
    position: absolute; top: 0; left: 0;
    width: 12000px; height: 12000px;
    transform-origin: 0 0;
    background: transparent;
    transition: none;
}
#world.cam-direct { transition: none; }

/* Слои фона/динамики блобов — DOM-канвасы внутри #world: мировые
   координаты через CSS left/top/width/height, камера двигает их общим
   трансформом #world (GPU-композит, нулевая canvas-работа в движении) */
/* Компактные карточки (средне-дальний зум, класс compact из _applyCam):
   карточки остаются на поле, но без иконки и даты — цветная рамка сферы
   + заголовок. ПЕРЕХОД ПЛАВНЫЙ (v69.14): иконка и дата СХЛОПЫВАЮТСЯ
   анимацией ~0.22с — раньше они гасли display:none мгновенно, и при
   пересечении порога зума все карточки «резко худели» — это и было
   «уменьшается и резко возвращается на место». */
.node-icon { transition: width 0.22s ease; overflow: hidden; }
.node-icon svg { flex: 0 0 auto; }
.node-date { transition: opacity 0.22s ease; }
.node-body { transition: padding 0.22s ease; }
#nodes-layer.compact .node:not(.kind-primary) .node-icon { width: 0; }
#nodes-layer.compact .node:not(.kind-primary) .node-icon svg { visibility: hidden; }
#nodes-layer.compact .node:not(.kind-primary) .node-date { opacity: 0; }
#nodes-layer.compact .node-body { padding: 4px 10px 4px 8px; }
#nodes-layer.compact .node-text { gap: 0; }
#nodes-layer.compact .node-procs { display: none; }
/* Главные факты и в компактном виде сохраняют иконку — ориентир на поле */
#nodes-layer.compact .node.kind-primary .node-icon { display: flex; }

/* Полоса «только форма» (между блоб-видом и LOD_TEXT_MIN): текст не
   РАСТЕРИЗУЕТСЯ, но карточка сохраняет свой размер — visibility:hidden
   держит layout-бокс заголовка/даты. ГЛАВНЫЕ факты (kind-primary) текст
   сохраняют всегда — ориентир на поле на любом расстоянии */
#nodes-layer.no-text .node:not(.kind-primary) .node-title,
#nodes-layer.no-text .node:not(.kind-primary) .node-date { visibility: hidden; }
/* Компакт (<0.3) прячет дату у всех — главные факты её сохраняют */
#nodes-layer.compact .node.kind-primary .node-date { display: block; }

#nodes-layer.canvas-cards {
    /* Карточки-в-канвас (средние зумы): режим ОТКЛЮЧЁН (LOD_CANVAS_CARDS=0),
       правило оставлено спящим на случай возврата порога */
    display: none;
}

#bg-layer, #dyn-layer {
    position: absolute; left: 0; top: 0;
    transform-origin: 0 0;
    pointer-events: none;
}

/* Гашение украшений на время движения камеры (класс gesture на #nodes-layer
   из кадрового цикла): тени значков процессов и выделения, пульс ворот —
   плоский вид в движении (дешевле растер), полный декор в покое.
   Заливка цветом сферы ОСТАЁТСЯ и в движении (пожелание автора). */
#nodes-layer.gesture .node.hover,
#nodes-layer.gesture .node.selected,
#nodes-layer.gesture .node:active { box-shadow: none; }
#nodes-layer.gesture .node-proc { box-shadow: none; }
#nodes-layer.gesture .node-proc.active { box-shadow: none; }
#nodes-layer.gesture .node.gate-node { animation: none; }
/* Транзишены карточек гасим в движении тоже: анимация border/background
   при переключении gesture-класса гоняла бы repaint на всех карточках */
#nodes-layer.gesture .node { transition: none; }

/* Шкала расстояния: масштабная линейка + координаты камеры (левый низ) */
#scale-bar {
    position: absolute; left: 14px; bottom: 12px; z-index: 20;
    pointer-events: none;
    display: flex; align-items: center; gap: 8px;
    font: 500 11px 'Noto Sans', sans-serif;
    color: rgba(122, 98, 62, 0.9);
    letter-spacing: 0.2px;
}
#scale-bar .sb-bar {
    display: block; height: 6px;
    border: 1px solid rgba(122, 98, 62, 0.8);
    border-top: none;
    background: rgba(122, 98, 62, 0.12);
}
#scale-bar .sb-txt { white-space: nowrap; text-shadow: 0 1px 0 rgba(247, 244, 239, 0.8); }

/* Копирайт-строка внизу поля: полупрозрачная, клики не перехватывает,
   таймлайн (z-index 1000) при открытии её перекрывает */
#footer-copy {
    position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%);
    max-width: calc(100% - 90px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 10.5px; letter-spacing: 0.2px; color: #8A7A5E; opacity: 0.6;
    pointer-events: none; z-index: 16;
    text-shadow: 0 1px 0 rgba(247, 244, 239, 0.7);
}

/* ---------- Лоадер ---------- */
.loader-overlay {
    position: absolute; inset: 0; z-index: 500;
    background: var(--bg);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 16px; transition: opacity 0.4s ease;
}
.loader-overlay.hidden { opacity: 0; pointer-events: none; }
.loader-spinner {
    width: 34px; height: 34px; border-radius: 50%;
    border: 3px solid var(--border); border-top-color: var(--gold);
    animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-text { font-size: 13px; color: var(--text-secondary); }

/* ---------- SVG-слои связей ---------- */
#svg-lines, #svg-layer {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    z-index: 5; pointer-events: none; overflow: visible;
}
#svg-labels {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    z-index: 15; pointer-events: none; overflow: visible;
}

/* Связи. Толщина ×1.25 (было 2.5 → 3.125). Цвета:
   - бордовая (#8E1B2E) — «Бордовая стрелка» (causal)
   - чёрная (#26211B) — «Чёрная стрелка» (succession), выбрана по умолчанию
   - зелёная/синяя/оранжевая — варианты для обозначения направления чтения */
.connection-line {
    stroke: #8E1B2E; stroke-width: 3.125; fill: none;
    pointer-events: none; /* клик ловит невидимый капюшон поверх (engine/lines.js) */
    transition: stroke 0.2s ease;
}
.connection-line:hover { stroke: #6D1221; }
.connection-line.canonical { stroke: #8E1B2E; }
.connection-line.rel-effect { stroke-dasharray: 3 5; opacity: 0.9; }
/* Чёрная стрелка — «Чёрная стрелка» (succession), по умолчанию */
.connection-line.rel-succession {
    stroke: #26211B !important; stroke-width: 3.125;
    stroke-dasharray: none; opacity: 0.9;
}
/* Бордовая стрелка (causal) — СКРЫТА у игрока, видна только админу:
   «факт-спавн» — цель возникает после прочтения источника (22.08.2026) */
.connection-line.rel-causal { stroke: #8E1B2E !important; stroke-width: 3.125; stroke-dasharray: 6 4; }
/* Цветные варианты стрелок (направление чтения) */
.connection-line.rel-green  { stroke: #2E7D32 !important; }
.connection-line.rel-blue   { stroke: #1565C0 !important; }
.connection-line.rel-orange { stroke: #E65100 !important; }
/* Стрелка К дополнительному факту (kind:supplement) или rel:effect —
   вся линия пунктиром. !important обязателен: .rel-succession задаёт
   stroke-dasharray:none, а CSS перебивает SVG-атрибуты */
.connection-line.to-supplement { stroke-dasharray: 3 5 !important; opacity: 0.9; }

/* Наконечники стрелок — СКЛЕЕННЫЕ path по классу стиля (engine/lines.js):
   линии одного класса рисуются одним path, поэтому SVG-маркер (он ставится
   только на конец path) не подходит — треугольники рисуются вручную и
   красятся ЗАЛИВКОЙ в цвет своей линии (пары к правилам выше) */
.conn-arrows { fill: #8E1B2E; stroke: none; pointer-events: none; }
.conn-arrows.canonical { fill: #8E1B2E; }
.conn-arrows.rel-succession { fill: #26211B !important; opacity: 0.9; }
.conn-arrows.rel-causal { fill: #8E1B2E !important; }
.conn-arrows.rel-green  { fill: #2E7D32 !important; }
.conn-arrows.rel-blue   { fill: #1565C0 !important; }
.conn-arrows.rel-orange { fill: #E65100 !important; }
.conn-arrows.rel-effect,
.conn-arrows.to-supplement { opacity: 0.9; }

/* Подписи связей */
.conn-label { cursor: pointer; pointer-events: all; }
.conn-label rect {
    fill: rgba(255, 253, 249, 0.88); stroke: var(--border); rx: 8;
}
.conn-label text {
    font-size: 11px; fill: var(--text-secondary); font-family: inherit;
}
.conn-label.canonical rect { fill: rgba(251, 246, 234, 0.92); stroke: #DCC9A0; }
.conn-label.canonical text { fill: #8A6F3D; }
.conn-label:hover rect { stroke: var(--gold); }
.conn-label.rel-succession text { fill: #A39A8A !important; font-size: 10px; font-style: italic; }
.conn-label.rel-succession rect { fill: rgba(255, 253, 249, 0.7); stroke: #E2DBCF; }

/* ============================================================
   Карточки фактов (.node)  —  v3 compact card
   ============================================================ */
.node {
    position: absolute;
    /* Позиция карточки — CSS-переменные --nx/--ny (движок пишет их в
       _placeNode): запись left/top на сотнях карточек при драге блоба
       вызывает reflow каждого кадра, смена transform пропускает layout.
       Все правила с transform (.node:active, непрочитанные, keyframes
       nodeAppear) обязаны сохранять translate-префикс! */
    transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(var(--node-scale, 1));
    transform-origin: top left;
    display: flex;
    width: max-content; min-width: 120px; max-width: 260px;
    /* Левый отступ .node-body = 0: иконка прижата к рамке вплотную
       (решение автора 22.08.2026, песочница _icon_map.html). */
    background: var(--bg-elevated);
    /* Рамку задаём ПОДСВОЙСТВАМИ (не сокращением border), иначе оно
       перебивает border-style: dashed у .boundary/.kind-supplement/.user-node.
       Цвет/толщина задаются ниже по сфере; стиль по умолчанию solid. */
    border-style: solid;
    border-width: var(--node-border-w, 1.5px);
    border-color: var(--border);
    border-radius: 10px;
    /* box-shadow УБРАН с базовой карточки: на 1262 узлах каждый box-shadow
       создавал отдельный compositor-layer с raster = постоянная paint-
       нагрузка. Тень остаётся только на .hover/.selected (JS-управляемые,
       единичный узел под курсором). Карточки без тени выглядят плоско,
       но это окупается плавностью. */
    cursor: grab;
    user-select: none; -webkit-user-select: none;
    font-size: 14px; line-height: 1.4; font-weight: 400;
    color: var(--text);
    z-index: 10;
    touch-action: none;
    white-space: normal; overflow-wrap: break-word; hyphens: none;
    transition: border-color 0.25s ease, background-color 0.25s ease;
    /* Изоляция слоёв: layout/style карточки не зависят от соседей и
       от transform #world. paint-сдерживание убрано намеренно: золотая
       звезда непрочитанного должна рисоваться целиком и выпирать за край
       карточки/круга, а contain: paint обрезал её по границе .node. */
    contain: layout style;
    overflow: visible;
}
/* ВНИМАНИЕ: намеренно НЕТ .node:hover. CSS :hover на 1265 карточках
   заставлял браузер на каждый mousemove делать hit-test по всем узлам
   плюс ставить paint-операции (box-shadow) в очередь на каждом входе/
   выходе курсора. Наведение теперь управляется из JS (input.js): класс
   .hover вешается только на единственный узел под курсором. */
/* Тени карточек УБРАНЫ полностью (решение автора): базовой тени нет давно,
   здесь убраны и hover/active/selected — вместо тени hover даёт только
   рамка, выделение — цвет рамки + тонкое кольцо (не тень) */
.node.hover { border-color: var(--selected-ink); }

/* Круг-«ворота» (граница эпох): пунктирная рамка — живёт на краю блоба */
/* Круг-«ворота» (граница эпох): пунктир УБРАН — теперь пунктирная рамка
   только у доп. фактов (.kind-supplement). Ворота отличаются позицией
   на краю блоба и пунктиром контура блоба в _paintZone. */
.node.boundary { }
.node:active {
    cursor: grabbing;
    transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(calc(var(--node-scale, 1) * 0.985));
    transform-origin: top left;
}

/* Полоска сферы (.node-stripe) УБРАНА полностью: лишний крашеный элемент
   на каждой карточке, сфера читается по цвету иконки и рамки. Правила
   оставлены пустыми намеренно — JS её больше не создаёт. */

/* Тело карточки: [иконка][заголовок + год] — обёртки сведены к одной
   .node-body (бывшая колонка .node-header удалена). */
/* Сфера факта — ТОНКАЯ рамка ЦВЕТА СФЕРЫ по всему контуру (решение
   автора 22.08.2026) */
.node { border-color: var(--sp-political); --ring: var(--sp-political); }
.node.sphere-foreign   { border-color: var(--sp-foreign);   --ring: var(--sp-foreign); }
.node.sphere-military  { border-color: var(--sp-military);  --ring: var(--sp-military); }
.node.sphere-cultural  { border-color: var(--sp-cultural);  --ring: var(--sp-cultural); }
.node.sphere-economic  { border-color: var(--sp-economic);  --ring: var(--sp-economic); }
.node.sphere-popular   { border-color: var(--sp-popular);   --ring: var(--sp-popular); }
.node.sphere-social    { border-color: var(--sp-social);    --ring: var(--sp-social); }

.node-body {
    display: flex; flex-direction: row; gap: 0; align-items: center;
    /* Иконка вплотную к левому краю тела (было 8px 14px 8px 10px) */
    padding: 8px 14px 8px 0;
    flex: 1 1 auto; min-width: 0;
}

/* Иконка вкладывается прямо в .node-body — без промежуточной колонки */

.node-icon {
    flex: 0 0 auto;
    width: 50px; height: 50px;
    color: var(--text-secondary);
    display: flex; align-items: center; justify-content: center;
    position: relative;
}
.node-icon svg { width: 50px; height: 50px; display: block; }
/* Детальные иконки: сам элемент больше (+8px), содержимое — ещё ×1.32
   (iconSvg, overflow visible) → визуально ~×1.5 от обычной */
.node-icon.node-icon-big { width: 58px; height: 58px; }
.node-icon.node-icon-big svg { width: 58px; height: 58px; }
.node.sphere-political .node-icon { color: var(--sp-political); }
.node.sphere-foreign   .node-icon { color: var(--sp-foreign); }
.node.sphere-military  .node-icon { color: var(--sp-military); }
.node.sphere-cultural  .node-icon { color: var(--sp-cultural); }
.node.sphere-economic  .node-icon { color: var(--sp-economic); }
.node.sphere-popular   .node-icon { color: var(--sp-popular); }
.node.sphere-social    .node-icon { color: var(--sp-social); }

/* Год факта — отдельной строкой под заголовком, крупный чёрный жирный
   текст БЕЗ белой таблетки и без кружка сферы (минус крашеный элемент).
   Точная дата XX века пишется словами. */
.node-date {
    display: block;
    align-self: flex-start;
    margin-top: 2px;
    font-size: 16px; font-weight: 800;
    letter-spacing: 0.5px;
    color: #1A1A1A;
    line-height: 1.3; white-space: nowrap;
}
.node.ghost .node-date { color: #8B857A; }
/* Длинные диапазоны («28 июля 1914 – 11 ноября 1918») переносятся,
   а не выпирают за край карточки */
.node-date.wide { white-space: normal; line-height: 1.25; }

/* Текст плотно к иконке: −2px компенсируют внутреннее поле PNG-иконки
   (знак занимает ~92% квадрата слота). Решение автора 22.08.2026. */
.node-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; margin-left: -2px; }
.node-title { font-weight: 500; font-size: 14.5px; }

/* Главный факт — ВДВОЕ крупнее обычного: резко выделяется на поле */
.node.kind-primary { font-size: 28px; font-weight: 800; max-width: 620px; min-width: 300px; }
.node.kind-primary .node-title { font-weight: 800; font-size: 28px; line-height: 1.2; }
.node.kind-primary .node-icon { width: 76px; height: 76px; }
.node.kind-primary .node-icon svg { width: 76px; height: 76px; }
.node.kind-primary .node-icon.node-icon-big { width: 86px; height: 86px; }
.node.kind-primary .node-icon.node-icon-big svg { width: 86px; height: 86px; }
.node.kind-primary .node-date { font-size: 22px; }
.node.kind-extra   { font-size: 14px; font-weight: 400; }
/* Дополнительный факт: пунктирная рамка отличает его от основных и обычных
   фактов — визуально показывает «вспомогательный» статус. Размер как у extra. */
.node.kind-supplement { font-size: 15px; font-weight: 400; border-style: dashed; }

/* --- Состояния карточек --- */

/* Окраска карточки в цвет сферы — НАСТРАИВАЕМАЯ через переменную --node-tint
   (0 = без окраски, 1 = полная). Меняйте в :root или через JS для теста.
   По умолчанию 0.18 — лёгкий оттенок. Используем background-image, чтобы не
   конфликтовать с box-shadow (hover/selected) и background-color состояний. */
:root { --node-tint: 0.18; }
.node.sphere-political:not(.ghost) { background-image: linear-gradient(color-mix(in srgb, var(--sp-political) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-political) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-foreign:not(.ghost)   { background-image: linear-gradient(color-mix(in srgb, var(--sp-foreign) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-foreign) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-military:not(.ghost)  { background-image: linear-gradient(color-mix(in srgb, var(--sp-military) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-military) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-cultural:not(.ghost)  { background-image: linear-gradient(color-mix(in srgb, var(--sp-cultural) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-cultural) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-economic:not(.ghost)  { background-image: linear-gradient(color-mix(in srgb, var(--sp-economic) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-economic) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-popular:not(.ghost)   { background-image: linear-gradient(color-mix(in srgb, var(--sp-popular) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-popular) calc(var(--node-tint) * 100%), transparent)); }
.node.sphere-social:not(.ghost)    { background-image: linear-gradient(color-mix(in srgb, var(--sp-social) calc(var(--node-tint) * 100%), transparent), color-mix(in srgb, var(--sp-social) calc(var(--node-tint) * 100%), transparent)); }

/* РАМКА В ЦВЕТ КРУГА. Цвет берётся из сферы факта (толщина/стиль заданы
   в базовом .node выше). Только color — не трогаем style/width, чтобы
   не сбросить пунктир у .boundary/.kind-supplement/.user-node. */
.node.sphere-political { border-color: var(--sp-political); }
.node.sphere-foreign   { border-color: var(--sp-foreign); }
.node.sphere-military  { border-color: var(--sp-military); }
.node.sphere-cultural  { border-color: var(--sp-cultural); }
.node.sphere-economic  { border-color: var(--sp-economic); }
.node.sphere-popular   { border-color: var(--sp-popular); }
.node.sphere-social    { border-color: var(--sp-social); }

/* Главный факт (kind:primary): золотая рамка + внешний золотой контур (двойное
   обрамление через outline), свой силуэт «плашки» (скруглены только верхние
   углы) и золотая плашка года. Правило НИЖЕ блока .sphere-* (та же
   специфичность) — иначе сферовый border-color перебил бы золото. Сфера всё
   равно видна по полоске (.node-stripe) и иконке. Толще базовой (3.5px против
   2.5px) — рамка не создаёт compositor-layer, поэтому дёшево даже при
   множестве таких узлов. Размер +20% задаёт .kind-primary выше.
   Outline вместо второй рамки/тени: тоже без compositor-layer. Радиус задаём
   ЯВНО, не через --node-radius: смена формы кругов в теме не должна стирать
   отличие главного (в теме «Обрезанные углы» силуэт переподтверждён ниже,
   у body.shape-notch .node.kind-primary). */
.node.kind-primary {
    border-color: #C9A96E;
    border-width: 5px;
    border-radius: 26px 26px 6px 6px;
    outline: 2.5px solid rgba(201, 169, 110, 0.6);
    outline-offset: 4px;
}
/* Золотая заливка карточки вместо сферового оттенка: та же специфичность
   (0-3-0), что у .sphere-*:not(.ghost) выше, но ниже по файлу — перебивает.
   Призраки (бывают и primary) исключены: у них свой полупрозрачный фон. */
.node.kind-primary:not(.ghost) {
    background-image: linear-gradient(rgba(201, 169, 110, 0.30), rgba(201, 169, 110, 0.30));
}
/* НЕПРОЧИТАННЫЙ = ТОНКАЯ РАМКА С ВНЕШНИМ КОЛЬЦОМ, как у главного факта
   (.kind-primary): бордер 2.5px цвета сферы + outline-кольцо с отступом —
   двойное обрамление без «жирной» web2.0-рамки (решение автора 22.08.2026).
   Outline не создаёт compositor-layer — дёшево даже при множестве узлов.
   Цвет не переопределяем: его задаёт класс сферы (.node.sphere-*).
   Карточка по-прежнему крупнее (×1.13). Звезда — статичная (ниже .node-star). */
.node:not(.read):not(.ghost) {
    background-color: var(--bg-elevated);
    border-width: 2.5px;
    outline: 2px solid color-mix(in srgb, var(--ring, var(--sp-political)) 60%, transparent);
    outline-offset: 3px;
    transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(calc(var(--node-scale, 1) * 1.13));
    z-index: 11;
}
/* Главный факт остаётся золотым даже непрочитанным: его фирменное
   обрамление (5px + золотое кольцо) сильнее сферового кольца непрочитанного. */
.node.kind-primary:not(.read):not(.ghost) {
    border-width: 5px;
    border-color: #C9A96E;
    outline: 2.5px solid rgba(201, 169, 110, 0.6);
    outline-offset: 4px;
}

/* Прочитано: монотонный нейтральный цвет (БЕЗ grayscale-фильтра — он
   «выцветал» и выглядел переливом рядом с непрочитанными). Теперь цвет
   ровный, отличие только в отсутствии звезды. */
.node.read {
    background-color: var(--bg-elevated);
}

/* Призрачная: полупрозрачная, пунктир, красный крестик. Серый фильтр здесь
   ОСТАВЛЕН — призраки и должны выглядеть тусклыми/чужими. */
.node.ghost {
    pointer-events: none;
    opacity: 0.40;
    border-color: #B5AEA0;
    border-width: 1.5px;
    /* Простой фон вместо repeating-linear-gradient: штриховка была
       paint-нагрузкой на каждом призраке. Теперь плоский фон + opacity
       (GPU-композиция) — призраки выглядят тусклыми без фильтров. */
    background: rgba(195, 190, 180, 0.22);
    box-shadow: none;
}
.node.ghost .node-icon { color: #B4AFA5; }
.node.ghost.gate-ghost { opacity: 0.6; border-width: 2px; }

/* Красный крестик на призрачных */
.node-ghost-x {
    position: absolute; top: 6px; right: 6px;
    width: 18px; height: 18px;
    color: #C47A6A; opacity: 0.7;
    pointer-events: none; z-index: 2;
}
.node-ghost-x svg { width: 18px; height: 18px; display: block; }

/* Выделение */
.node.selected {
    border-color: var(--selected-ink);
    box-shadow: 0 0 0 1.5px var(--selected-ink);
}
.node.user-node { border-style: dashed; opacity: 0.92; }

/* Блок вне экрана */
.node.offview { visibility: hidden; }

/* Заглушка-точка: узел вне close-зоны (но ещё видимый) упрощается до точки
   цветом сферы. Нет иконки, текста, года, звезды — только маленький круг.
   Это радикально уменьшает DOM-дерево при панорамировании: вместо 9 дочерних
   элементов на каждом из сотен узлов — один пустой div. */
.node.simplified {
    width: 14px !important; min-width: 0 !important; max-width: 14px !important;
    height: 14px !important; padding: 0 !important;
    border-radius: 50%;
    overflow: hidden;
    /* Внешний золотой контур главного факта (kind-primary) на точке-заглушке
       превращается в кольцо вокруг 14px кружка — снимаем. */
    outline: none;
}
.node.simplified .node-body, .node.simplified .node-star,
.node.simplified .node-ghost-x { display: none; }

/* === Дальний зум: только блобы и названия эпох ===
   При отдалении камеры полностью отключаем факты (#nodes-layer) и стрелки
   (#svg-lines): display:none убирает их из layout/paint/composite. На экране
   остаются блобы (на канвасе) и подписи эпох — обзор как на карте в стратегиях.
   Это кардинально разгружает браузер при отдалении: вместо 1262 карточек +
   сотен связей — ни одного DOM-узла факта. */
#nodes-layer.far { display: none; }
#nodes-layer.far .node { display: none; } /* подстраховка */
/* svg-lines скрывается отдельным классом на #world — см. _applyCam.
   Прямой селектор ниже на случай, если класс повешен на сам svg. */
#svg-lines.far { display: none; }

@keyframes nodeAppear {
    from { opacity: 0; transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(0.96); }
    to   { opacity: 1; transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(1); }
}
.node.pop { animation: nodeAppear 0.3s ease-out; }

/* Маркер «непрочитанное»: СТАТИЧНАЯ золотая звезда (решение автора 22.08.2026) —
   классический пятилучевой силуэт, инлайн-SVG в data URI, без спрайтов и
   анимаций: один дешёвый растровый blit, никакого «web2.0»-горения.
   Центр звезды — НА РЕБРЕ карточки (угол рамки): ползвезды торчит наружу. */
.node-star {
    position: absolute; top: -15px; right: -15px;
    width: 32px; height: 32px;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.8l2.95 6.25 6.75.72-5.05 4.6 1.4 6.65L12 16.7l-6.05 3.32 1.4-6.65-5.05-4.6 6.75-.72z' fill='%23F0B93F' stroke='%23C88A1E' stroke-width='1.1' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    opacity: 1;
    transform: none;
    transition: opacity 0.35s ease, transform 0.35s ease, visibility 0s 0.4s;
    pointer-events: none;
    z-index: 5;
}
/* В движении камеры звезда остаётся той же статичной картинкой —
   подмена кадра больше не нужна (спрайт упразднён 22.08.2026). */

/* Значки мета-процессов факта — ряд эмодзи на цветных подложках, снизу-
   справа за пределами карточки (параллельно звезде в правом-верхнем углу).
   pointer-events: none, чтобы плашки не перехватывали drag/click узла. */
.node-procs {
    position: absolute; right: 6px; bottom: -14px;
    display: flex; gap: 3px;
    z-index: 6; pointer-events: none;
}
.node-proc {
    width: 27px; height: 27px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 15px; line-height: 1;
    color: #FFFEF8;
    background: #8B6914;
    border: 2px solid var(--bg-elevated, #FBF7EE);
    transition: transform 0.15s ease;
}
/* Подсветка значка активного процесса при фильтре на канвасе
   (класс вешает _applyProcessFilter в app-ui.js): чёткое белое кольцо
   вместо размытой тени (тени у кругов убраны). */
.node-proc.active {
    transform: scale(1.3);
    border-color: #FFFFFF;
    z-index: 7;
}
/* На упрощённых (off-screen) узлах плашек нет — _simplifyNode чистит DOM. */
.node.simplified .node-procs { display: none; }

/* CSS-пульс (opacity/scale) убран специально: видимая звезда всегда
   непрозрачная и статичная по размеру, а «живость» даёт только перелив
   stop-color внутри общего градиента #starGold в index.html. */
@media (pointer: coarse) {
    .node.gate-node { animation: none; }
}

/* Прочитано — звезда прячется, галки НЕТ */
.node.read .node-star {
    animation: none;
    opacity: 0;
    transform: scale(0.2);
    visibility: hidden;
}
.node.ghost .node-star { display: none; }

/* Форма «обрезанные углы»: clip-path убран — он резал выпирающую звезду.
   Оставляем только квадратные углы карточки. */
body.shape-notch .node {
    border-radius: 0;
}
/* Силуэт главного факта переживает и эту тему: правило выше (0-2-1) перебило
   бы базовый border-radius .kind-primary (0-2-0) — здесь специфичнее (0-3-1). */
body.shape-notch .node.kind-primary {
    border-radius: 20px 20px 4px 4px;
}
body.shape-notch .node-star { top: -6px; right: -6px; }

/* Рябь «готов к чтению» — убрана: класс ping-ready не назначается в JS
   (мёртвый код), а бесконечная анимация создавала лишнюю paint-нагрузку
   и «переливание». Факты теперь монотонного цвета. */
.node.ping-ready::after { content: none; }

/* Ворота: «дыхание» рамкой (border-color), НЕ тенью — анимированный
   box-shadow перерисовывал карточку каждый кадр (решение автора 21.08:
   тени у кругов убраны полностью). */
.node.gate-node {
    border-width: 1.5px;
    animation: gateBreath 2.8s ease-in-out infinite;
}
@keyframes gateBreath {
    0%, 100% { border-color: rgba(201, 169, 110, 0.55); }
    50%      { border-color: rgba(201, 169, 110, 1); }
}

.gate-badge {
    position: absolute;
    font-size: 17px;
    z-index: 25;
    cursor: pointer;
    pointer-events: auto;
    background: rgba(255, 253, 249, 0.92);
    border: 1px solid #DCC9A0;
    border-radius: 50%;
    width: 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    transition: transform 0.2s ease;
}
.gate-badge:hover { transform: scale(1.12); }

/* ---------- Подписи эпох ---------- */
#epoch-labels {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    pointer-events: none; z-index: 12;
}
.epoch-label {
    position: absolute;
    transform: translate(-50%, -100%);
    display: flex; align-items: center; gap: 24px;
    /* ×2 к прошлому виду (решение автора 22.08.2026): правитель + годы
       читаются издалека, медальон-«портрет» — заметный */
    font-size: 56px; font-weight: 600; letter-spacing: 0.6px;
    color: #7A6A4F;
    background: rgba(255, 253, 249, 0.9);
    padding: 16px 48px 16px 18px;
    border-radius: 84px;
    border: 2px solid var(--border);
    white-space: nowrap;
    pointer-events: none;
    box-shadow: none;
}
.epoch-label .epoch-face {
    flex: 0 0 auto;
    width: 92px; height: 92px;
    border-radius: 50%;
    border: 5px solid #D8CBB5;
    background: #F0EBE3;
    display: flex; align-items: center; justify-content: center;
    font-weight: 600; font-size: 40px; color: var(--text-secondary);
}
.epoch-label.ghost {
    color: var(--text-secondary);
    border-style: dashed;
    border-color: #D5CDBE;
    background: rgba(249, 246, 241, 0.8);
    box-shadow: none;
}
.epoch-label.ghost .epoch-face { border-color: #D5CDBE; opacity: 0.8; }

/* Дальний зум: имя эпохи СТРОГО внутри блоба — переносится по словам,
   центрируется, размер подбирается JS так, чтобы поместиться. */
.epoch-label.far {
    transform: translate(-50%, -50%);
    background: none;
    border: none;
    box-shadow: none;
    padding: 0;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: rgba(96, 76, 50, 0.85);
    text-shadow: none;
    text-transform: none;
    text-align: center;
    white-space: normal;
    word-break: normal;
    overflow-wrap: break-word;
}
.epoch-label.far.ghost {
    color: rgba(140, 132, 118, 0.6);
    border: none;
    background: none;
}

/* ---------- Уровни детализации ---------- */
#world.lod-far #svg-labels,
#world.lod-min #svg-labels { display: none; }
#world.lod-far .node,
#world.lod-min .node { transition: none; }
#world.lod-min .node { box-shadow: none; }


/* ═══════════════════════════════════════════════════════════════
   ЭТАЛОН КАРТОЧЕК v5 — сплошная заливка + штриховка (автор, 23.08.2026)
   Непрочитанные: заливка цветом сферы + светлая диагональная штриховка
   + звезда; иконка/текст — белые. Прочитанные: та же заливка, БЕЗ
   штриховки и звезды. Призраки — прежние серые. Цвета настроены
   панелью «🧪 Цвета» (test-hints.html). ТЕНЕЙ У КАРТОЧЕК НЕТ.
   ═══════════════════════════════════════════════════════════════ */
:root {
    --so-political: #b28638;
    --so-foreign:   #3880b7;
    --so-military:  #b2462e;
    --so-cultural:  #6530b5;
    --so-economic:  #5aaa31;
    --so-social:    #57694e;
    --so-popular:   #34302d;
    --so-primary:   #af8331;
    --hatch-color:  255, 253, 249;
    --hatch-alpha:  0.24;
    --hatch-width:  4.5px;
    --hatch-gap:    11px;
    --hatch-angle:  45deg;
}
/* Опора трансформа — ЦЕНТР карточки: масштаб темы/непрочитанности и
   пружинка прожатия дышат вокруг центра, не сдвигая позицию */
.node { transform-origin: 50% 50%; --onb-k: 1; }
.node:not(.read):not(.ghost) { --onb-k: 1.13; }
.node:active { transform-origin: 50% 50%; transition: transform 0.12s ease; }

/* Непрочитанные: сплошная заливка + штриховка */
.node:not(.read):not(.ghost) {
    background-color: var(--so-political);
    background-image: repeating-linear-gradient(var(--hatch-angle),
        rgba(var(--hatch-color), var(--hatch-alpha)) 0 var(--hatch-width),
        transparent var(--hatch-width) var(--hatch-gap));
    border-width: var(--node-border-w, 1.5px);
    border-color: rgba(44, 42, 38, 0.35);
    outline: none;
}
.node.sphere-foreign:not(.read):not(.ghost)  { background-color: var(--so-foreign); }
.node.sphere-military:not(.read):not(.ghost) { background-color: var(--so-military); }
.node.sphere-cultural:not(.read):not(.ghost) { background-color: var(--so-cultural); }
.node.sphere-economic:not(.read):not(.ghost) { background-color: var(--so-economic); }
.node.sphere-social:not(.read):not(.ghost)   { background-color: var(--so-social); }
.node.sphere-popular:not(.read):not(.ghost)  { background-color: var(--so-popular); }
.node.kind-primary:not(.read):not(.ghost) { background-color: var(--so-primary); border-color: #A98443; }
.node:not(.read):not(.ghost) .node-title,
.node:not(.read):not(.ghost) .node-date { color: #FFFDF9; }
.node:not(.ghost) .node-icon svg { filter: brightness(0) invert(1); }

/* Прочитанные: та же заливка, БЕЗ штриховки и звезды */
.node:not(.ghost).read {
    background-color: var(--so-political);
    background-image: none;
    border-color: rgba(44, 42, 38, 0.35);
    outline: none;
}
.node.sphere-foreign.read  { background-color: var(--so-foreign); }
.node.sphere-military.read { background-color: var(--so-military); }
.node.sphere-cultural.read { background-color: var(--so-cultural); }
.node.sphere-economic.read { background-color: var(--so-economic); }
.node.sphere-social.read   { background-color: var(--so-social); }
.node.sphere-popular.read  { background-color: var(--so-popular); }
.node.kind-primary.read { background-color: var(--so-primary); border-color: #A98443; }
.node.read .node-title,
.node.read .node-date { color: #FFFDF9; }

/* ТЕНЕЙ НЕТ: выделение — только рамкой */
.node.selected { box-shadow: none; border-color: var(--selected-ink); }

/* ── Прожатие: пружинка от центра (глубина 0.80, решение автора) ── */
.node.onb-tap { animation: onbTapPress 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes onbTapPress {
    0%, 100% { transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(calc(var(--node-scale, 1) * var(--onb-k, 1))); }
    35%      { transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(calc(var(--node-scale, 1) * var(--onb-k, 1) * 0.90)); }
    70%      { transform: translate(var(--nx, 0px), var(--ny, 0px)) scale(calc(var(--node-scale, 1) * var(--onb-k, 1) * 1.02)); }
}
/* ── Пузыри (механика bubbly-button, цвет сферы) ── */
.node.bb-anim::before,
.node.bb-anim::after {
    position: absolute; content: ''; display: block;
    width: 140%; height: 100%; left: -20%;
    z-index: -1; pointer-events: none;
    background-repeat: no-repeat; opacity: 0.92;
}
.node.bb-anim::before {
    top: -75%;
    background-image:
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, transparent 20%, var(--so-political) 20%, transparent 30%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, transparent 10%, var(--so-political) 15%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%);
    background-size: 10% 10%, 20% 20%, 15% 15%, 20% 20%, 18% 18%, 10% 10%, 15% 15%, 10% 10%, 18% 18%;
    animation: topBubbles ease-in-out 0.75s forwards;
}
.node.bb-anim::after {
    bottom: -75%;
    background-image:
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, transparent 10%, var(--so-political) 15%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%),
        radial-gradient(circle, var(--so-political) 20%, transparent 20%);
    background-size: 15% 15%, 20% 20%, 18% 18%, 20% 20%, 15% 15%, 10% 10%, 20% 20%;
    animation: bottomBubbles ease-in-out 0.75s forwards;
}
.node.sphere-foreign.bb-anim::before  { background-image: radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, transparent 20%, var(--so-foreign) 20%, transparent 30%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-foreign) 15%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%); }
.node.sphere-military.bb-anim::before { background-image: radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, transparent 20%, var(--so-military) 20%, transparent 30%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-military) 15%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%); }
.node.sphere-cultural.bb-anim::before { background-image: radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, transparent 20%, var(--so-cultural) 20%, transparent 30%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-cultural) 15%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%); }
.node.sphere-economic.bb-anim::before { background-image: radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, transparent 20%, var(--so-economic) 20%, transparent 30%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-economic) 15%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%); }
.node.sphere-popular.bb-anim::before  { background-image: radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, transparent 20%, var(--so-popular) 20%, transparent 30%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-popular) 15%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%); }
.node.sphere-foreign.bb-anim::after  { background-image: radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-foreign) 15%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%), radial-gradient(circle, var(--so-foreign) 20%, transparent 20%); }
.node.sphere-military.bb-anim::after { background-image: radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-military) 15%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%), radial-gradient(circle, var(--so-military) 20%, transparent 20%); }
.node.sphere-cultural.bb-anim::after { background-image: radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-cultural) 15%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%), radial-gradient(circle, var(--so-cultural) 20%, transparent 20%); }
.node.sphere-economic.bb-anim::after { background-image: radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-economic) 15%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%), radial-gradient(circle, var(--so-economic) 20%, transparent 20%); }
.node.sphere-popular.bb-anim::after  { background-image: radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, transparent 10%, var(--so-popular) 15%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%), radial-gradient(circle, var(--so-popular) 20%, transparent 20%); }
@keyframes topBubbles {
  0%   { background-position: 5% 90%, 10% 90%, 10% 90%, 15% 90%, 25% 90%, 25% 90%, 40% 90%, 55% 90%, 70% 90%; }
  50%  { background-position: 0% 80%, 0% 20%, 10% 40%, 20% 0%, 30% 30%, 22% 50%, 50% 50%, 65% 20%, 90% 30%; }
  100% { background-position: 0% 70%, 0% 10%, 10% 30%, 20% -10%, 30% 20%, 22% 40%, 50% 40%, 65% 10%, 90% 20%; background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%; }
}
@keyframes bottomBubbles {
  0%   { background-position: 10% -10%, 30% 10%, 55% -10%, 70% -10%, 85% -10%, 70% -10%, 70% 0%; }
  50%  { background-position: 0% 80%, 20% 80%, 45% 60%, 60% 100%, 75% 70%, 95% 60%, 105% 0%; }
  100% { background-position: 0% 90%, 20% 90%, 45% 70%, 60% 110%, 75% 80%, 95% 70%, 110% 10%; background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%; }
}
