SHiNE-server/shine-UI/styles/network-graph.css
Pixel f19f7b0ec4 Связи (13.06): орбы по референсу, линии по категории, постоянная вселенная
Орбы:
- материал «хрусталь»: чистое лицо (виньетка 0.5→0.22), диффузный блик окна
  вместо «капли» (+мягкий blur sf), стекло прозрачнее (тело 0.38→0.3),
  полупрозрачная преломляющая кромка (blur + opacity 0.25→0.2).
- размер +11.5% (node-dot 52→58px); единый ORB_R=29 как источник радиуса.
- убран значок * у общих узлов (логика is-common цела).

Линии:
- цвет по категории на ВСЕХ рёбрах; плазма только сияющим.
- общий узел наследует сияние исходного человека (не серый).
- контакт линий ровно на кромке сферы орба (ORB_R), без зазора, все уровни.

Навигация:
- констелляция (паутина 2-3 уровней) — постоянный режим; кнопка «Вселенная»
  убрана; Семья/Друзья/Сияющие остаются фильтрами. Чистка осиротевшего CSS.

Версия 1.2.161.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:31:46 +03:00

679 lines
23 KiB
CSS
Raw 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.
========================================================================== */
.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;
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) */
/* 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;
}
.fg-orb-svg {
position: absolute;
width: 119%;
height: 119%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: block;
overflow: visible;
}
.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.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(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; }
/* Доступность: визуально скрытый список графа для скринридеров (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: rgba(255, 214, 120, 0.95);
box-shadow: 0 0 14px rgba(255, 200, 90, 0.4);
}