/* ============================================================================ 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); /* мягкое объёмное свечение вокруг нити */ } .fg-edge-core { fill: none; stroke: #e0f7fc; /* ультра-светлый голубой — чёткий контур луча */ stroke-width: 1.5; stroke-linecap: round; } .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: 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); } .fg-node:focus-visible .node-dot, .fg-node:hover .node-dot { border-color: rgba(166, 218, 255, 0.95); box-shadow: 0 0 0 3px rgba(77, 160, 255, 0.22), 0 8px 16px rgba(4, 8, 15, 0.35); } /* «Сияние» — мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи. Многослойная анимированная 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); } /* «Прицел» в центре экрана (зона фокуса) — позади узлов */ .fg-reticle { position: absolute; left: 50%; top: 50%; width: 64px; height: 64px; margin: -32px 0 0 -32px; border-radius: 50%; border: 2px dashed rgba(150, 190, 255, 0.3); pointer-events: none; z-index: 0; opacity: 0.45; transition: width 200ms ease, height 200ms ease, margin 200ms ease, border-color 200ms ease, opacity 200ms ease; } .fg-reticle.is-locked { width: 94px; height: 94px; margin: -47px 0 0 -47px; border-color: rgba(130, 235, 255, 0.65); opacity: 0.85; } /* «Призрак» старой карты при 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-reticle.is-pulse { animation: fg-reticle-pulse 0.6s ease; } @keyframes fg-reticle-pulse { 0% { transform: scale(1); } 40% { transform: scale(1.22); border-color: rgba(130, 235, 255, 0.9); } 100% { transform: scale(1); } } /* Панель фильтров слоёв (оверлей под шапкой) */ .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; }