/* ============================================================================ Force-directed карта связей (.fg-*) — интерактивный граф на странице «Связи». Узлы позиционируются трансформами (GPU), рёбра — отдельный SVG-слой. Отдельный модуль, чтобы не раздувать components.css. ========================================================================== */ .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; /* плавное появление линий при перестройке */ } /* Сияющая связь = двухслойный неоновый «световод» (Neon Layering): изящно, но объёмно (как OLED). GLOW — широкий размытый ореол неонового оттенка под линией; CORE — тонкий чёткий светлый контур. */ .fg-edge-glow { fill: none; stroke: rgba(110, 225, 255, 1); stroke-width: 4; stroke-linecap: round; filter: blur(2px); /* мягкое объёмное свечение вокруг нити */ /* синхро-пульс: нить «дышит» толщиной/размытием в том же ритме (3.6с), что и ободок сияющего узла — в покое SVG не перерисовывается, поэтому все нити стартуют синхронно и пульсируют вместе. */ animation: fg-edge-pulse 3.6s ease-in-out infinite; } .fg-edge-core { fill: none; stroke: #e0f7fc; /* ультра-светлый голубой — чёткий контур луча */ stroke-width: 1.5; stroke-linecap: round; animation: fg-edge-core-pulse 3.6s ease-in-out infinite; } /* пульс «световода» в такт дыханию сияющего ободка (та же длительность 3.6с) */ @keyframes fg-edge-pulse { 0%, 100% { stroke-width: 3.4; filter: blur(1.6px); } 50% { stroke-width: 5.2; filter: blur(2.8px); } } @keyframes fg-edge-core-pulse { 0%, 100% { stroke-width: 1.3; } 50% { stroke-width: 1.9; } } @media (prefers-reduced-motion: reduce) { .fg-edge-glow, .fg-edge-core { animation: none; } } .fg-node { position: absolute; left: 0; top: 0; width: 52px; height: 52px; border: 0; padding: 0; background: transparent; color: inherit; 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) */ .fg-node .node-dot { width: 52px; height: 52px; margin: 0; font-size: 16px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; } .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; } } /* «Сияние» — мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи. Многослойная анимированная box-shadow + размытый радиальный ореол (через внешний SVG-фильтр). Пульсация очень медленная и плавная (3.6с): радиус и прозрачность «дышат» 0.5 ↔ 1.0 — как мягкое свечение живого организма в темноте, а не «жирный маркер». */ .fg-node.is-shine .node-dot { border-color: rgba(150, 240, 255, 0.62); animation: fg-shine-glow 3.6s ease-in-out infinite; } /* размытый радиальный ореол позади аватарки; внешний SVG-фильтр даёт мягкое гауссово размытие */ .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; animation: fg-shine-halo 3.6s ease-in-out infinite; } /* пульсация многослойной тени: компактное приглушённое → широкое мягкое свечение */ @keyframes fg-shine-glow { 0%, 100% { box-shadow: 0 0 5px rgba(125, 232, 255, 0.30), 0 0 11px rgba(112, 226, 255, 0.18), 0 0 20px rgba(100, 220, 255, 0.10); } 50% { box-shadow: 0 0 9px rgba(150, 245, 255, 0.62), 0 0 20px rgba(122, 236, 255, 0.42), 0 0 36px rgba(100, 220, 255, 0.26); } } /* ореол дышит размером и прозрачностью синхронно с тенью (мягко, без рывков) */ @keyframes fg-shine-halo { 0%, 100% { transform: scale(0.9); opacity: 0.5; } 50% { transform: scale(1.12); opacity: 1; } } @media (prefers-reduced-motion: reduce) { .fg-node.is-shine .node-dot { animation: none; } .fg-node.is-shine .node-dot::before { animation: none; } } /* мягкое свечение вокруг фокуса (статичное; «дышит» вместе с размером узла ниже) */ .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.48–1.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; } } /* Чип-переключатель «Вселенная» — активное состояние наследует стиль .fg-filter-chip.is-active */ .fg-deep-chip.is-active { background: rgba(150, 130, 255, 0.18); border-color: rgba(190, 170, 255, 0.6); box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(150, 120, 255, 0.3); color: #efeaff; } /* «Призрак» старой карты при 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(54px, calc(env(safe-area-inset-top) + 50px)); 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); } /* Контекстное меню узла (долгое нажатие) — в #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; color: #dfe9ff; font-size: 14px; padding: 9px 10px; border-radius: 9px; cursor: pointer; } .fg-menu-item:hover { background: rgba(77, 160, 255, 0.16); } .fg-menu-item.is-stub { color: #7f8aa3; cursor: default; } .fg-menu-item.is-stub:hover { background: transparent; } /* Нижний сниппет (bottom sheet) */ .fg-sheet { position: absolute; left: 12px; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 13; display: none; padding: 12px 14px 14px; background: rgba(16, 24, 40, 0.95); border: 1px solid rgba(166, 196, 245, 0.26); border-radius: 16px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); } .fg-sheet.is-open { display: block; animation: fg-sheet-in 200ms ease; } @keyframes fg-sheet-in { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .fg-sheet-close { position: absolute; top: 8px; right: 10px; background: transparent; border: 0; color: #9fb6e0; font-size: 16px; cursor: pointer; line-height: 1; } .fg-sheet-title { font-weight: 700; font-size: 15px; color: #eaf1ff; display: flex; gap: 8px; align-items: center; } .fg-sheet-badge { font-size: 10px; background: rgba(130, 235, 255, 0.2); color: #bff0ff; padding: 2px 8px; border-radius: 999px; font-weight: 600; } .fg-sheet-rel { font-size: 12px; color: #9fb6e0; margin-top: 3px; } .fg-sheet-actions { display: flex; gap: 8px; margin-top: 12px; } .fg-sheet-actions > button { flex: 1; } /* === Партия 2: бейдж-счётчик связей, поиск, хлебные крошки, цветовые кластеры ============ */ /* Бейдж числа связей — маленькая пилюля в правом-верхнем углу аватарки */ .fg-node-badge { position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: rgba(16, 24, 40, 0.92); border: 1px solid rgba(150, 200, 255, 0.5); color: #d9ecff; font-size: 9px; font-weight: 700; line-height: 14px; text-align: center; pointer-events: none; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); } .fg-node.is-focus .fg-node-badge { background: rgba(61, 196, 223, 0.95); border-color: rgba(220, 245, 255, 0.8); color: #06131c; } .fg-node.is-tier2 .fg-node-badge { transform: scale(0.85); } /* Цветовые кластеры: мягкая аура узла по типу связи (визуально группирует «семью/друзей/бизнес») */ .fg-node.is-family .node-dot { box-shadow: 0 0 16px rgba(255, 159, 94, 0.20); } .fg-node.is-friend .node-dot { box-shadow: 0 0 16px rgba(120, 179, 255, 0.20); } .fg-node.is-business .node-dot { box-shadow: 0 0 16px rgba(190, 150, 255, 0.20); } .fg-node.is-contact .node-dot { box-shadow: 0 0 16px rgba(170, 190, 220, 0.16); } /* сияющие/фокус — свой эффект свечения (см. выше), ауру кластера не навязываем */ .fg-node.is-shine .node-dot, .fg-node.is-focus .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; }