Files

637 lines
25 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
Force-directed карта связей (.fg-*) — интерактивный граф на странице «Связи».
Узлы позиционируются трансформами (GPU), рёбра — отдельный SVG-слой.
Отдельный модуль, чтобы не раздувать components.css.
========================================================================== */
/* Канонические токены ЯЗЫКА СВЯЗЕЙ (единый источник цвета отношений для всего продукта).
Экран «Личные сообщения» наследует их через --dm-* (не дублирует hex).
(force-graph пока использует свои JS-цвета RELATION_COLORS — миграция на токены = будущая задача.) */
:root {
--rel-contact: #8C63FF; /* violet — обычная связь / контакт */
--rel-family: #F0B82E; /* gold — семья / близкий круг / важность / подтверждение */
--rel-shining: #68D8FF; /* celestial — сияющий / сильная активная связь */
--rel-link: #19E58A; /* emerald — статус «Связь» (активный канал) */
--rel-contact-glow: rgba(140, 99, 255, 0.24);
--rel-family-glow: rgba(240, 184, 46, 0.30);
--rel-shining-glow: rgba(104, 216, 255, 0.35);
--rel-link-glow: rgba(25, 229, 138, 0.24);
}
.fg-stage {
touch-action: none; /* перехватываем жесты сами (pan), без скролла страницы */
user-select: none;
-webkit-user-select: none;
cursor: grab;
background: radial-gradient(circle at 50% 42%, rgba(83, 216, 251, 0.07), rgba(255, 255, 255, 0.01) 60%);
}
.fg-stage:active {
cursor: grabbing;
}
/* Живой фон-«небула»: глубокое размытое сине-голубое облако света строго под центральным узлом.
Медленно «дышит» (радиус/яркость) и переливается индиго↔ультрамарин (hue-rotate) за 7с.
Чистый CSS на компоновщике — создаёт ощущение живой светящейся среды, не будит rAF-цикл. */
.fg-stage::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle 320px at 50% 47%, rgba(80, 150, 255, 0.30) 0%, rgba(60, 100, 220, 0.15) 42%, rgba(40, 70, 170, 0) 72%);
filter: blur(80px);
pointer-events: none;
z-index: 0; /* строго под линиями (z:0, но раньше по порядку) и узлами (z:1) */
animation: fg-nebula 7s ease-in-out infinite;
}
@keyframes fg-nebula {
0%, 100% { opacity: 0.70; filter: blur(80px) hue-rotate(-12deg); }
50% { opacity: 1.00; filter: blur(96px) hue-rotate(16deg); }
}
@media (prefers-reduced-motion: reduce) {
.fg-stage::before { animation: none; }
}
.fg-world {
position: absolute;
left: 50%;
top: 50%;
width: 0;
height: 0;
will-change: transform;
z-index: 1; /* узлы и подписи строго над линиями связей */
}
.fg-edges {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: visible;
z-index: 0; /* линии связей — под узлами */
transition: opacity 420ms ease; /* плавное появление линий при перестройке */
}
/* Сияющая связь = плазменный композитинг (3 слоя на ОДНОМ S-пути, см. renderEdges).
Настоящий НЕОН: яркое светлое ядро + ВИДИМЫЙ голубой ореол вокруг. Слои поля/трубки идут в режиме
mix-blend-mode: screen → свет складывается аддитивно с тёмным фоном (как реальное свечение), а в точках
пересечения нитей у центра — ярче (энергетический хаб). Прозрачность слоёв — inline (×spotlight/глубину). */
.fg-plasma-flare { /* нижний: широкое насыщенное голубое плазменное свечение (по референсу) */
fill: none;
stroke: #00bfff; /* глубокий голубой */
stroke-width: 16;
stroke-linecap: round;
filter: url(#fg-plasma-blur6); /* мягкое объёмное свечение (гладкие края — как на референсе) */
mix-blend-mode: screen; /* аддитивное свечение поверх тёмного фона */
/* синхро-«дыхание» поля толщиной в такт ободку сияющего узла (3.6с); прозрачность не трогаем (inline) */
animation: fg-plasma-breath 3.6s ease-in-out infinite;
}
.fg-plasma-tube { /* средний: яркая толстая неоновая трубка */
fill: none;
stroke: #00e5ff; /* яркий циан */
stroke-width: 6;
stroke-linecap: round;
filter: url(#fg-plasma-blur2); /* SVG feGaussianBlur stdDeviation=2 */
mix-blend-mode: screen; /* аддитивное свечение */
}
.fg-plasma-core { /* верхний: яркое чёткое ядро (светло-голубо-белое) */
fill: none;
stroke: #dffaff; /* светло-голубо-белое — «жидкое» ядро */
stroke-width: 2;
stroke-linecap: round;
}
/* мягкое «дыхание» плазменного облака толщиной, синхронно с пульсом сияющего ободка (3.6с) */
@keyframes fg-plasma-breath {
0%, 100% { stroke-width: 14; }
50% { stroke-width: 19; }
}
@media (prefers-reduced-motion: reduce) {
.fg-plasma-flare { animation: none; }
}
.fg-node {
position: absolute;
left: 0;
top: 0;
width: 52px;
height: 52px;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
transform-origin: center center;
will-change: transform;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none; /* iOS: не показывать системное меню по долгому тапу */
z-index: 1;
}
/* круглая аватарка узла (переиспользуем существующий .node-dot из renderUserAvatar) */
/* 58px → радиус 29 = ORB_R в force-graph.js (контакт линий берётся от этого радиуса). */
.fg-node .node-dot {
width: 58px;
height: 58px;
margin: 0;
font-size: 16px;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
/* SVG-«стеклянный орб» — масштабируем так, чтобы сфера (r42 = 84% SVG) ≈ диаметр узла → линии-связи
прилипают к краю орба, как раньше. Хост .node-dot держит размер/состояния/синхронизацию позиций. */
.fg-node .node-dot.fg-orb-host {
position: relative;
background: none;
overflow: visible; /* не срезать внешнее свечение орба */
box-shadow: none;
}
/* A/B PNG-оверлей орба (ветка glass-png-overlay): фото снизу + запечённый стеклянный PNG сверху.
Бокс = 119% от .node-dot (как .fg-orb-svg → сфера ≈ кромке node-dot, контакт линий от ORB_R сохраняется). */
/* Специфичность `.fg-orb-host …` бьёт глобальное `.node-dot img` (иначе оно гасит opacity→0
и форсит размер 100%). Поэтому opacity/размеры/радиус задаём здесь явно. */
/* Кромку даёт сам glass_overlay.png → убираем остаточный border .node-dot (синее кольцо
старого векторного орба). Только у PNG-хоста; вектор и свечение/box-shadow не трогаем. */
/* (0,4,0) — выше специфичности правил категории `.fg-node.is-family .node-dot` (0,3,0),
иначе их background/border перебивают. */
.fg-node .node-dot.fg-orb-host:has(.fg-pngorb) {
border: none;
background: none; /* фон-градиент категории не торчит из-под прозрачного стекла (та самая «обводка») */
}
.fg-orb-host .fg-pngorb {
position: absolute;
left: 50%; top: 50%;
width: 119%; height: 119%;
transform: translate(-50%, -50%);
}
.fg-orb-host .fg-pngorb-glass {
position: absolute;
left: 50%; top: 50%;
width: 100%; height: 100%;
transform: translate(-50%, -50%);
opacity: 1;
border-radius: 0;
object-fit: contain;
display: block;
pointer-events: none;
}
.fg-orb-host .fg-pngorb-photo {
position: absolute;
left: 50%; top: 50%;
width: 78%; height: 78%; /* ~78% от бокса оверлея — сидит внутри стеклянной кромки */
transform: translate(-50%, -50%);
opacity: 1;
border-radius: 50%; /* фолбэк, если mask не поддержан */
object-fit: cover;
display: block;
/* Мягкий край: фото непрозрачно до --feather-full, плавно гаснет к 0 у --feather-edge —
сливается со стеклом без жёсткого ободка. Силу растушёвки крутим этими двумя параметрами. */
--feather-full: 62%;
--feather-edge: 78%;
-webkit-mask-image: radial-gradient(circle at 50% 50%, #000 var(--feather-full), transparent var(--feather-edge));
mask-image: radial-gradient(circle at 50% 50%, #000 var(--feather-full), transparent var(--feather-edge));
}
.fg-orb-host .fg-pngorb-init {
display: flex; align-items: center; justify-content: center;
background: #454b55; color: #ffffff; font-weight: 600; font-size: 20px;
}
/* Маленький знак официального пользователя. Он находится внутри .node-dot, поэтому автоматически
масштабируется вместе с аватаркой при focus/zoom/анимациях графа. */
/* ВАЖНО: селектор намеренно специфичнее глобального `.node-dot img` из features/network.css,
где обычные фото стартуют с opacity:0 и width/height:100%. Иначе badge тоже наследует эти
правила и становится полностью невидимым. */
.fg-node .node-dot .fg-official-badge {
position: absolute;
left: -2%;
bottom: -1%;
width: 16%;
height: 16%;
min-width: 9px;
min-height: 9px;
object-fit: contain;
object-position: center;
display: block;
opacity: 1;
border-radius: 0;
transition: none;
z-index: 20;
pointer-events: none;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.42));
}
.fg-node.is-family .node-dot {
background: linear-gradient(165deg, #785038, #5f3e2c);
border-color: rgba(255, 194, 143, 0.6);
}
.fg-node.is-friend .node-dot {
background: linear-gradient(165deg, #2f4f80, #2a3f62);
border-color: rgba(150, 190, 255, 0.5);
}
.fg-node.is-business .node-dot {
background: linear-gradient(165deg, #4a3b7a, #2f2750);
border-color: rgba(196, 165, 255, 0.55);
}
.fg-node.is-contact .node-dot {
background: linear-gradient(165deg, #36435c, #283142);
border-color: rgba(180, 200, 226, 0.4);
}
.fg-node.is-focus .node-dot {
background: linear-gradient(130deg, #3a5f8e, #3dc4df);
color: #061119;
border-color: rgba(180, 230, 255, 0.85);
box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.45), 0 10px 22px rgba(4, 8, 15, 0.45);
}
/* Тактильный отклик «нажатия вглубь»: аватарка слегка вдавливается (scale 0.92), а неоновое кольцо
вспыхивает заметно ярче (~1.5×). Срабатывает при наведении, фокусе и зажатии (.is-pressed). */
.fg-node:focus-visible .node-dot,
.fg-node:hover .node-dot,
.fg-node.is-pressed .node-dot {
transform: scale(0.92);
border-color: rgba(160, 240, 255, 0.95);
box-shadow: 0 0 0 2px rgba(150, 238, 255, 0.6), 0 0 22px rgba(120, 230, 255, 0.85);
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-pressed .node-dot { transform: none; }
}
/* «Сияние» на аватарке теперь стационарное. Интенсивность зафиксирована примерно на 2/3 пути
от прежнего минимального состояния к максимальному: заметно, но без постоянного «дыхания». */
.fg-node.is-shine .node-dot {
border-color: rgba(150, 240, 255, 0.62);
box-shadow:
0 0 8.3px rgba(146, 243, 255, 0.57),
0 0 18.3px rgba(121, 235, 255, 0.39),
0 0 33px rgba(100, 220, 255, 0.24);
}
/* Статичный размытый ореол позади аватарки — та же промежуточная интенсивность ~2/3. */
.fg-node.is-shine .node-dot::before {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, rgba(140, 240, 255, 0.5) 0%, rgba(130, 235, 255, 0.18) 46%, rgba(130, 235, 255, 0) 72%);
filter: url(#fg-shine-glow);
z-index: -1;
pointer-events: none;
transform: scale(1.07);
opacity: 0.91;
}
/* мягкое свечение вокруг фокуса (статичное; «дышит» вместе с размером узла ниже) */
.fg-node.is-focus .node-dot::after {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, rgba(130, 235, 255, 0.32) 0%, rgba(130, 235, 255, 0) 70%);
z-index: -1;
pointer-events: none;
}
/* «Дыхание» фокуса — бесконечная очень мягкая пульсация РАЗМЕРА (база 1.5x → 1.481.52x),
период 4с. CSS-анимация на transform (GPU) — НЕ будит rAF-цикл физики; интерфейс «живой». */
.fg-node.is-focus .node-dot {
animation: fg-focus-breath 4s ease-in-out infinite;
}
@keyframes fg-focus-breath {
0%, 100% { transform: scale(0.987); }
50% { transform: scale(1.013); }
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-focus .node-dot { animation: none; }
}
/* подпись под узлом — абсолютная, чтобы не влиять на размер бокса (центрирование по трансформу) */
.fg-node-label {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 5px;
max-width: 110px;
font-size: 10px;
line-height: 1.1;
color: #d6e2ff;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55);
pointer-events: none;
}
.fg-node.is-secondary .fg-node-label {
opacity: 0.75;
}
/* Имя центрального узла — на подложке, чтобы линии связей не просвечивали сквозь текст */
.fg-node.is-focus .fg-node-label {
margin-top: 7px;
padding: 3px 10px;
border-radius: 9px;
background: rgba(8, 14, 24, 0.74);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
color: #f4f8ff;
font-weight: 600;
text-shadow: none;
}
/* Лёгкая точка (узлы сверх хард-лимита DOM) — без аватара/подписи */
.fg-dot {
width: 15px;
height: 15px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: #36435c;
box-shadow: 0 2px 6px rgba(4, 8, 15, 0.4);
}
.fg-dot.is-family { background: #6f4a34; border-color: rgba(255, 194, 143, 0.5); }
.fg-dot.is-friend { background: #2f4f80; border-color: rgba(150, 190, 255, 0.45); }
.fg-dot.is-business { background: #4a3b7a; border-color: rgba(196, 165, 255, 0.5); }
.fg-dot.is-contact { background: #36435c; }
.fg-dot.is-shine { box-shadow: 0 0 9px rgba(130, 235, 255, 0.75); }
/* === Глубина 2-3 уровней (прототип «Вселенная», только лаборатория) ============== */
/* 2-й уровень — «друзья друзей»: полноценная аватарка (лицо + имя), просто мельче основных.
Масштаб/прозрачность задаёт движок; здесь — читаемый ободок и подпись (не «дырка»). */
.fg-node.is-tier2 .node-dot {
border-color: rgba(170, 200, 240, 0.65);
box-shadow: 0 2px 8px rgba(4, 8, 15, 0.4);
}
.fg-node.is-tier2 .fg-node-label {
font-size: 9px;
opacity: 0.9;
top: calc(100% + 1px);
}
/* 3-й уровень — микрозвезда: светящаяся точка без картинки (эффект далёкого созвездия). */
.fg-dot.is-tier3 {
width: 9px;
height: 9px;
border: 0;
background: radial-gradient(circle, #e6f6ff 0%, rgba(150, 215, 255, 0.95) 38%, rgba(120, 200, 255, 0) 75%);
box-shadow: 0 0 6px rgba(150, 220, 255, 0.9), 0 0 13px rgba(115, 200, 255, 0.5);
/* медленное мерцание «звезды» — по box-shadow/яркости (НЕ opacity/scale: ими управляет движок при
раскрытии). У каждой звезды своя задержка (inline animation-delay) → живое созвездие. */
animation: fg-star-twinkle 3.4s ease-in-out infinite;
}
.fg-dot.is-tier3.is-shine {
background: radial-gradient(circle, #ffffff 0%, rgba(160, 240, 255, 1) 38%, rgba(120, 230, 255, 0) 75%);
box-shadow: 0 0 8px rgba(160, 240, 255, 1), 0 0 16px rgba(115, 220, 255, 0.7);
}
@keyframes fg-star-twinkle {
0%, 100% { box-shadow: 0 0 3px rgba(150, 220, 255, 0.45), 0 0 7px rgba(115, 200, 255, 0.25); filter: brightness(0.78); }
50% { box-shadow: 0 0 7px rgba(165, 235, 255, 0.95), 0 0 15px rgba(120, 210, 255, 0.6); filter: brightness(1.3); }
}
@media (prefers-reduced-motion: reduce) {
.fg-dot.is-tier3 { animation: none; }
}
/* «Призрак» старой карты при Z-переходе (эффект погружения) */
.fg-ghost-layer {
position: absolute;
inset: 0; /* полноэкранный overlay → клон линий/узлов совпадает по координатам */
pointer-events: none;
z-index: 0;
opacity: 0.5; /* стартовая прозрачность шлейфа выше → дольше читается (JS уводит в 0) */
transform-origin: 50% 50%;
/* лениво и породисто тает на месте за 1000мс — медленный дорогой шлейф истории перехода */
transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1), opacity 1000ms ease;
}
/* Панель фильтров слоёв (оверлей под шапкой) */
.fg-filter-bar {
position: absolute;
top: max(72px, calc(env(safe-area-inset-top) + 68px));
left: 0;
right: 0;
z-index: 11;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
padding: 0 12px;
pointer-events: none;
}
/* Стеклянные табы — тонкие пластины матового стекла (frosted glass) */
.fg-filter-chip {
pointer-events: auto;
border: 0.5px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #cfe0ff;
font-size: 12px;
font-weight: 600;
line-height: 1;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.08); /* лёгкий стеклянный блик сверху */
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
/* Активный таб — то же стекло, но подсвеченное сине-голубым (в тон неону графа) */
.fg-filter-chip.is-active {
background: rgba(125, 215, 255, 0.16);
border-color: rgba(160, 230, 255, 0.55);
color: #eaf7ff;
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(110, 210, 255, 0.28);
}
/* Служебные переключатели карты: история и X2 намеренно чуть компактнее основных фильтров. */
.fg-history-chip {
padding-left: 11px;
padding-right: 11px;
}
.fg-x2-chip {
min-width: 36px;
padding-left: 9px;
padding-right: 9px;
font-weight: 750;
letter-spacing: 0.02em;
}
/* Контекстное меню узла (долгое нажатие) — в #modal-root, поверх всего, не масштабируется */
.fg-menu-overlay {
position: fixed;
inset: 0;
z-index: 50;
}
.fg-menu {
position: fixed;
min-width: 210px;
padding: 8px;
display: grid;
gap: 3px;
background: rgba(16, 24, 40, 0.97);
border: 1px solid rgba(166, 196, 245, 0.28);
border-radius: 14px;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
z-index: 51;
}
.fg-menu-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
padding: 4px 8px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 3px;
}
.fg-menu-login {
font-weight: 700;
color: #eaf1ff;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-menu-rel {
font-size: 11px;
color: #9fb6e0;
flex: 0 0 auto;
}
.fg-menu-item {
text-align: left;
background: transparent;
border: 0;
font-size: 14px;
padding: 9px 10px;
border-radius: 9px;
cursor: pointer;
}
.fg-menu-item.is-stub {
cursor: default;
}
.fg-menu-item.is-stub:hover {
background: transparent;
}
/* === Партия 2: поиск, хлебные крошки, цветовые кластеры ============================== */
/* Кластерная аура по категории удалена (цветной фон/обводка узла убраны). Сияющим/фокусу
box-shadow не навязываем — у них свой эффект свечения (is-shine/is-focus выше). */
.fg-node.is-focus:not(.is-shine) .node-dot { box-shadow: none; }
/* Строка поиска (оверлей вверху, под панелью фильтров) */
.fg-search {
position: absolute;
top: max(92px, calc(env(safe-area-inset-top) + 88px));
left: 50%;
transform: translateX(-50%);
z-index: 12;
width: min(280px, 70vw);
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
border: 0.5px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.08);
}
.fg-search input {
flex: 1;
border: 0;
background: transparent;
color: #eaf2ff;
font-size: 13px;
outline: none;
}
.fg-search input::placeholder { color: #7d8aa6; }
.fg-search .fg-search-ico { color: #9fc0ff; font-size: 13px; }
/* Хлебные крошки навигации (стек погружений: Иван › Нина › Ада) */
.fg-breadcrumb {
position: absolute;
top: max(132px, calc(env(safe-area-inset-top) + 128px));
left: 0;
right: 0;
z-index: 12;
display: none;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
padding: 0 12px;
pointer-events: none;
}
.fg-breadcrumb.is-open { display: flex; }
.fg-crumb {
pointer-events: auto;
border: 0;
background: rgba(16, 24, 40, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #cfe0ff;
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 5px 10px;
border-radius: 999px;
cursor: pointer;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-crumb.is-last {
background: rgba(125, 215, 255, 0.18);
color: #eaf7ff;
cursor: default;
}
.fg-crumb-sep { color: #5f7196; font-size: 11px; align-self: center; pointer-events: none; }
/* Доступность: визуально скрытый список графа для скринридеров (sr-only, читается ассистивными технологиями) */
.fg-a11y {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* «Общая связь» (этот человек — и твой друг тоже): золотой ободок. Значок ★ убран по запросу. */
.fg-node.is-common .node-dot {
border-color: transparent;
box-shadow: 0 0 14px rgba(255, 200, 90, 0.4);
}