six-degrees 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +14 -14
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.rsc +20 -19
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +20 -19
  15. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +16 -15
  16. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/update/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/import/page.js +1 -1
  50. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/import.html +1 -1
  53. package/.next/standalone/.next/server/app/import.rsc +24 -23
  54. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +24 -23
  55. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  56. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +18 -17
  57. package/.next/standalone/.next/server/app/index.html +1 -1
  58. package/.next/standalone/.next/server/app/index.rsc +22 -21
  59. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +18 -17
  60. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +22 -21
  61. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  62. package/.next/standalone/.next/server/app/launch/page.js +1 -1
  63. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/launch.html +2 -2
  66. package/.next/standalone/.next/server/app/launch.rsc +24 -23
  67. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +24 -23
  68. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +18 -17
  70. package/.next/standalone/.next/server/app/page.js +1 -1
  71. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/paths/page.js +1 -1
  74. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/paths.html +1 -1
  77. package/.next/standalone/.next/server/app/paths.rsc +24 -23
  78. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +24 -23
  79. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +18 -17
  81. package/.next/standalone/.next/server/app/profile/page.js +1 -1
  82. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/app/profile.html +1 -1
  85. package/.next/standalone/.next/server/app/profile.rsc +24 -23
  86. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +24 -23
  87. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  88. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +18 -17
  89. package/.next/standalone/.next/server/app/queue/page.js +1 -1
  90. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/queue.html +1 -1
  93. package/.next/standalone/.next/server/app/queue.rsc +24 -23
  94. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +24 -23
  95. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +18 -17
  97. package/.next/standalone/.next/server/app/scores/page.js +1 -1
  98. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  99. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  100. package/.next/standalone/.next/server/app/scores.html +1 -1
  101. package/.next/standalone/.next/server/app/scores.rsc +24 -23
  102. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -23
  103. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  104. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +18 -17
  105. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  106. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/settings.html +1 -1
  109. package/.next/standalone/.next/server/app/settings.rsc +24 -23
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -23
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +18 -17
  113. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  114. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  116. package/.next/standalone/.next/server/app/setup.html +1 -1
  117. package/.next/standalone/.next/server/app/setup.rsc +24 -23
  118. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +24 -23
  119. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  120. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +18 -17
  121. package/.next/standalone/.next/server/app/social/page.js +1 -1
  122. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/social.html +1 -1
  125. package/.next/standalone/.next/server/app/social.rsc +24 -23
  126. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +24 -23
  127. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +18 -17
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05_3-22._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13dx4sd._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18bcnu4._.js +3 -0
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +2 -2
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  160. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -2
  161. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +5 -5
  162. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +5 -5
  163. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +5 -5
  164. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +9 -9
  165. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08oxiqh._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_08g31gm._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_0gk_ofx._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/_0nwsf93._.js +3 -0
  170. package/.next/standalone/.next/server/chunks/ssr/_0vho1lb._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/{_0jdkqgi._.js → _1-658fq._.js} +2 -2
  172. package/.next/standalone/.next/server/chunks/ssr/_1bc46kz._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +9 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +9 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_2105jz8._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +13 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +13 -0
  181. package/.next/standalone/.next/server/chunks/ssr/{_0gtv9jb._.js → app_components_CompanyScores_1g0mytz.js} +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -12
  185. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  186. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -6
  188. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  189. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
  190. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
  191. package/.next/standalone/.next/server/chunks/ssr/{lib_0i8v-s3._.js → lib_1c4ieuw._.js} +2 -2
  192. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  193. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  194. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  195. package/.next/standalone/.next/server/pages/404.html +1 -1
  196. package/.next/standalone/.next/server/pages/500.html +1 -1
  197. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  198. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  199. package/.next/standalone/.next/static/chunks/02pq76u7y4v9-.js +22 -0
  200. package/.next/standalone/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
  201. package/.next/{static/chunks/2wgz9ietkqqe6.js → standalone/.next/static/chunks/0i02yxppzds1o.js} +1 -1
  202. package/.next/standalone/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
  203. package/.next/standalone/.next/static/chunks/0oyslj62jkubd.js +1 -0
  204. package/.next/standalone/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
  205. package/.next/{static/chunks/3sowbvbaj1s6n.js → standalone/.next/static/chunks/0ws7zy0w57ufi.js} +1 -1
  206. package/.next/standalone/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
  207. package/.next/standalone/.next/static/chunks/172e10mi-tpo_.js +34 -0
  208. package/.next/standalone/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
  209. package/.next/standalone/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
  210. package/.next/standalone/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
  211. package/.next/standalone/.next/static/chunks/1m3017gp790pw.js +45 -0
  212. package/.next/standalone/.next/static/chunks/1t20e8u0st7ep.js +1 -0
  213. package/.next/standalone/.next/static/chunks/2-ch9rm27eril.js +11 -0
  214. package/.next/standalone/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
  215. package/.next/standalone/.next/static/chunks/3bp284gjf7dyq.js +1 -0
  216. package/.next/standalone/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
  217. package/.next/standalone/.next/static/chunks/3l4t8je6iguzc.js +1 -0
  218. package/.next/standalone/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
  219. package/.next/standalone/.next/static/chunks/42m0bqr8pbovz.js +11 -0
  220. package/.next/standalone/CHANGELOG.md +61 -0
  221. package/.next/standalone/README.md +22 -8
  222. package/.next/standalone/app/api/scraper/route.js +40 -3
  223. package/.next/standalone/app/api/update/route.js +12 -6
  224. package/.next/standalone/app/components/AppHeader.js +257 -0
  225. package/.next/standalone/app/components/Avatar.js +2 -1
  226. package/.next/standalone/app/components/ChainView.js +12 -12
  227. package/.next/standalone/app/components/FilterPanel.js +20 -12
  228. package/.next/standalone/app/components/ForceGraph.js +163 -25
  229. package/.next/standalone/app/components/GalaxyLab.js +43 -12
  230. package/.next/standalone/app/components/GridView.js +4 -4
  231. package/.next/standalone/app/components/LinkedInLimits.js +2 -1
  232. package/.next/standalone/app/components/ListView.js +3 -3
  233. package/.next/standalone/app/components/NoteDetail.js +2 -1
  234. package/.next/standalone/app/components/OnboardingGate.js +2 -2
  235. package/.next/standalone/app/components/OutlinkQuest.js +2 -1
  236. package/.next/standalone/app/components/PathsAnalyzer.js +3 -2
  237. package/.next/standalone/app/components/PeopleMap.js +2 -1
  238. package/.next/standalone/app/components/RingsView.js +3 -3
  239. package/.next/standalone/app/components/ScanTrail.js +85 -0
  240. package/.next/standalone/app/components/SeparationView.js +100 -77
  241. package/.next/standalone/app/components/Sidebar.js +4 -4
  242. package/.next/standalone/app/components/TheirCircle.js +2 -1
  243. package/.next/standalone/app/components/ThemeLoader.js +9 -0
  244. package/.next/standalone/app/components/UpdatePanel.js +20 -2
  245. package/.next/standalone/app/components/settings/AppearanceSection.js +226 -0
  246. package/.next/standalone/app/components/settings/NetworkHealthSection.js +2 -1
  247. package/.next/standalone/app/components/ui.js +2 -1
  248. package/.next/standalone/app/globals.css +70 -2
  249. package/.next/standalone/app/import/page.js +3 -3
  250. package/.next/standalone/app/layout.js +11 -2
  251. package/.next/standalone/app/page.js +69 -215
  252. package/.next/standalone/app/paths/page.js +8 -21
  253. package/.next/standalone/app/profile/page.js +11 -10
  254. package/.next/standalone/app/queue/page.js +8 -21
  255. package/.next/standalone/app/scores/page.js +1 -1
  256. package/.next/standalone/app/settings/page.js +7 -3
  257. package/.next/standalone/app/setup/page.js +83 -88
  258. package/.next/standalone/app/social/Crm.js +2 -1
  259. package/.next/standalone/app/social/SocialHub.js +2 -2
  260. package/.next/standalone/app/social/page.js +1 -1
  261. package/.next/standalone/lib/data-import.js +1 -0
  262. package/.next/standalone/lib/galaxy-export.js +2 -1
  263. package/.next/standalone/lib/galaxy-lab.js +24 -8
  264. package/.next/standalone/lib/level.js +51 -0
  265. package/.next/standalone/lib/release.js +13 -7
  266. package/.next/standalone/lib/scanner-python.js +38 -8
  267. package/.next/standalone/lib/scraper-client.js +2 -1
  268. package/.next/standalone/lib/separation.js +4 -2
  269. package/.next/standalone/lib/theme-store.js +61 -0
  270. package/.next/standalone/lib/themes.js +186 -0
  271. package/.next/standalone/lib/tiers.js +2 -8
  272. package/.next/standalone/package-lock.json +2 -2
  273. package/.next/standalone/package.json +4 -4
  274. package/.next/standalone/scripts/build-desktop.mjs +325 -0
  275. package/.next/standalone/scripts/build-site.mjs +85 -15
  276. package/.next/standalone/scripts/gen-test-network.mjs +93 -0
  277. package/.next/standalone/scripts/next.mjs +20 -0
  278. package/.next/standalone/scripts/readme_buttons.py +3 -0
  279. package/.next/standalone/scripts/requirements.txt +18 -1
  280. package/.next/standalone/scripts/site-news.mjs +54 -9
  281. package/.next/standalone/scripts/smoke-desktop.mjs +177 -0
  282. package/.next/standalone/scripts/windows/six-degrees.iss +100 -0
  283. package/.next/static/chunks/02pq76u7y4v9-.js +22 -0
  284. package/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
  285. package/.next/{standalone/.next/static/chunks/2wgz9ietkqqe6.js → static/chunks/0i02yxppzds1o.js} +1 -1
  286. package/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
  287. package/.next/static/chunks/0oyslj62jkubd.js +1 -0
  288. package/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
  289. package/.next/{standalone/.next/static/chunks/3sowbvbaj1s6n.js → static/chunks/0ws7zy0w57ufi.js} +1 -1
  290. package/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
  291. package/.next/static/chunks/172e10mi-tpo_.js +34 -0
  292. package/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
  293. package/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
  294. package/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
  295. package/.next/static/chunks/1m3017gp790pw.js +45 -0
  296. package/.next/static/chunks/1t20e8u0st7ep.js +1 -0
  297. package/.next/static/chunks/2-ch9rm27eril.js +11 -0
  298. package/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
  299. package/.next/static/chunks/3bp284gjf7dyq.js +1 -0
  300. package/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
  301. package/.next/static/chunks/3l4t8je6iguzc.js +1 -0
  302. package/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
  303. package/.next/static/chunks/42m0bqr8pbovz.js +11 -0
  304. package/CHANGELOG.md +61 -0
  305. package/README.md +22 -8
  306. package/lib/data-import.js +1 -0
  307. package/lib/galaxy-export.js +2 -1
  308. package/lib/galaxy-lab.js +24 -8
  309. package/lib/level.js +51 -0
  310. package/lib/release.js +13 -7
  311. package/lib/scanner-python.js +38 -8
  312. package/lib/scraper-client.js +2 -1
  313. package/lib/separation.js +4 -2
  314. package/lib/theme-store.js +61 -0
  315. package/lib/themes.js +186 -0
  316. package/lib/tiers.js +2 -8
  317. package/package.json +4 -4
  318. package/scripts/requirements.txt +18 -1
  319. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +0 -3
  322. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +0 -3
  324. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +0 -9
  325. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +0 -3
  326. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +0 -3
  327. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +0 -3
  328. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +0 -9
  329. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
  330. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +0 -3
  331. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +0 -1
  332. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
  333. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
  334. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +0 -34
  335. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
  336. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +0 -2
  337. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +0 -1
  338. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +0 -1
  339. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +0 -43
  340. package/.next/standalone/.next/static/chunks/2dwtoszeq91us.js +0 -1
  341. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +0 -1
  342. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +0 -1
  343. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +0 -1
  344. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
  345. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +0 -1
  346. package/.next/static/chunks/03bziijw5srol.js +0 -1
  347. package/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
  348. package/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
  349. package/.next/static/chunks/0g_fy2034bp0x.js +0 -34
  350. package/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
  351. package/.next/static/chunks/0w-byiwb2zax8.js +0 -2
  352. package/.next/static/chunks/1g7jcovdejc9-.js +0 -1
  353. package/.next/static/chunks/1i51yylxdzht1.js +0 -1
  354. package/.next/static/chunks/1zf8swaasjf1e.js +0 -43
  355. package/.next/static/chunks/2dwtoszeq91us.js +0 -1
  356. package/.next/static/chunks/2rqtpccruavf2.js +0 -1
  357. package/.next/static/chunks/30x3oq_st7a5r.js +0 -1
  358. package/.next/static/chunks/355ryio1b7sm1.js +0 -1
  359. package/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
  360. package/.next/static/chunks/3fci6qi39-1n5.js +0 -1
  361. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
  362. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
  363. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
  364. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
  365. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
  366. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { initialsFor } from '../../lib/tiers';
4
4
  import { localPhoto } from '../../lib/photos';
5
+ import { TIER_COLORS as THEME_TIERS } from '../../lib/themes';
5
6
 
6
- const FALLBACK = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
7
+ const FALLBACK = THEME_TIERS; // the theme's dot colours (lib/themes.js)
7
8
 
8
9
  // A round face with a tier-coloured ring.
9
10
  //
@@ -42,8 +42,8 @@ import { keyFor, score } from '../../lib/separation';
42
42
  import { hasRequest } from '../../lib/requests-client';
43
43
  import { watchScanner, scannerNow, isCircleScan } from '../../lib/scraper-client';
44
44
  import useRequests from './useRequests';
45
+ import { TIER_COLORS } from '../../lib/themes';
45
46
 
46
- const TIER_COLORS = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
47
47
  const DEGREE_COLORS = { 1: '#FFD700', 2: '#FF6B35', 3: '#3498DB', 4: '#9B59B6', 5: '#00ff88', 6: '#ff5050' };
48
48
  const TIER_RANK = { S: 0, A: 1, B: 2, C: 3, D: 4 };
49
49
  const GREEN = '#00ff88';
@@ -309,7 +309,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
309
309
  // === ONE CIRCLE: a bridge's, or anyone's in it, opened in place ===
310
310
  if (focused) {
311
311
  return (
312
- <div ref={containerRef} style={{ flex: 1, background: '#0a0a1a', position: 'relative', overflow: 'hidden' }}>
312
+ <div ref={containerRef} style={{ flex: 1, background: 'var(--sd-page)', position: 'relative', overflow: 'hidden' }}>
313
313
  <CircleFocus
314
314
  key={trail.map((r) => r.id).join('>')}
315
315
  trail={trail} index={index} reach={reach} dims={dims} requests={requests}
@@ -329,7 +329,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
329
329
  const hovBridge = hovered ? bridgePos.find(b => b.id === hovered) : null;
330
330
 
331
331
  return (
332
- <div ref={containerRef} style={{ flex: 1, background: '#0a0a1a', position: 'relative', overflow: 'hidden' }}>
332
+ <div ref={containerRef} style={{ flex: 1, background: 'var(--sd-page)', position: 'relative', overflow: 'hidden' }}>
333
333
  <svg width={dims.w} height={dims.h}>
334
334
  <g transform={`translate(${cx * (1 - zoom)}, ${cy * (1 - zoom)}) scale(${zoom})`}>
335
335
  {/* A guide line for every ring of bridges */}
@@ -390,7 +390,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
390
390
  {/* How many in their circle are waiting for a scan: a small outlined number, top right */}
391
391
  {ready > 0 && (
392
392
  <g pointerEvents="none">
393
- <circle cx={b.x + 14} cy={b.y - 14} r={6} fill="#0a0a1a" stroke={GREEN} strokeWidth={1.2} />
393
+ <circle cx={b.x + 14} cy={b.y - 14} r={6} fill="var(--sd-bg)" stroke={GREEN} strokeWidth={1.2} />
394
394
  <text x={b.x + 14} y={b.y - 11.4} textAnchor="middle" fill={GREEN} fontSize={ready > 9 ? 6 : 7} fontWeight={800}>{ready > 99 ? '99+' : ready}</text>
395
395
  </g>
396
396
  )}
@@ -408,7 +408,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
408
408
  })}
409
409
 
410
410
  {/* Center: YOU */}
411
- <circle cx={cx} cy={cy} r={22} fill="#0a0a1a" stroke="#FFD700" strokeWidth={3} />
411
+ <circle cx={cx} cy={cy} r={22} fill="var(--sd-bg)" stroke="#FFD700" strokeWidth={3} />
412
412
  <text x={cx} y={cy + 4} textAnchor="middle" fill="#FFD700" fontSize={11} fontWeight={800}>
413
413
  {userName?.split(' ')[0] || 'YOU'}
414
414
  </text>
@@ -693,7 +693,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
693
693
  {(f.reached || (roomy && row.tier === 'S')) && (
694
694
  <text x={p.x} y={above ? p.y - r - 4 / k : p.y + r + 10 / k} textAnchor="middle" fontSize={8.5 / k} fontWeight={f.reached ? 700 : 400}
695
695
  fill={f.reached === 'hidden' ? '#999' : f.reached ? GREEN : '#aaa'} pointerEvents="none"
696
- stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
696
+ stroke="var(--sd-bg)" strokeWidth={2.4 / k} paintOrder="stroke">
697
697
  {firstName(row)}
698
698
  </text>
699
699
  )}
@@ -810,7 +810,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
810
810
  return (
811
811
  <g key={'lk-' + row.id} transform={`translate(${p.x} ${p.y})`} pointerEvents="none"
812
812
  opacity={litLinks && !lit ? 0.45 : 1}>
813
- <circle r={c.radius} fill="#0a0a1a" fillOpacity={0.9} stroke={color} strokeOpacity={lit ? 0.7 : 0.25} strokeWidth={1 / k} />
813
+ <circle r={c.radius} fill="var(--sd-bg)" fillOpacity={0.9} stroke={color} strokeOpacity={lit ? 0.7 : 0.25} strokeWidth={1 / k} />
814
814
  {/* Their people, each joined to the middle of the cluster */}
815
815
  {clusterPaths(c).map(([tier, { spokes }]) => (
816
816
  <path key={'s' + tier} d={spokes} fill="none" stroke={TIER_COLORS[tier] || '#555'}
@@ -819,14 +819,14 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
819
819
  {clusterPaths(c).map(([tier, { dots: d }]) => (
820
820
  <path key={tier} d={d} fill={TIER_COLORS[tier] || '#555'} fillOpacity={lit ? 1 : 0.9} />
821
821
  ))}
822
- <circle r={LINK_HUB} fill={color} stroke={hovLink === j ? '#fff' : '#0a0a1a'} strokeWidth={1.5} />
822
+ <circle r={LINK_HUB} fill={color} stroke={hovLink === j ? '#fff' : 'var(--sd-bg)'} strokeWidth={1.5} />
823
823
  {row.id === scanningId && (
824
824
  <circle r={c.radius + 4} fill="none" stroke={DEGREE_COLORS[3]} strokeWidth={1 / k} strokeDasharray={`${3 / k} ${2 / k}`} />
825
825
  )}
826
826
  <text y={c.radius + 12 / k} textAnchor="middle" fill="#fff" fontSize={10 / k} fontWeight={700}
827
- stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">{firstName(row)}’s circle</text>
827
+ stroke="var(--sd-bg)" strokeWidth={2.4 / k} paintOrder="stroke">{firstName(row)}’s circle</text>
828
828
  <text y={c.radius + 23 / k} textAnchor="middle" fill={color} fontSize={8 / k}
829
- stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
829
+ stroke="var(--sd-bg)" strokeWidth={2.4 / k} paintOrder="stroke">
830
830
  {c.people.length.toLocaleString('en-US')} · {ordinal(depth + link.step + 1)} degree
831
831
  </text>
832
832
  </g>
@@ -871,7 +871,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
871
871
  )}
872
872
  {/* The person in the middle, with the halo if their own circle is ready to scan */}
873
873
  {reachState(person, reach) === 'ready' && <Halo x={0} y={0} r={40} still={still} />}
874
- <circle r={28} fill="#0a0a1a" stroke={TIER_COLORS[person.tier] || '#888'} strokeWidth={hover === 'center' ? 4 : 3} filter="url(#focusGlow)" />
874
+ <circle r={28} fill="var(--sd-bg)" stroke={TIER_COLORS[person.tier] || '#888'} strokeWidth={hover === 'center' ? 4 : 3} filter="url(#focusGlow)" />
875
875
  {localPhoto(person.profile_image_url) ? (
876
876
  <>
877
877
  <clipPath id="focus-clip"><circle r={24} /></clipPath>
@@ -1077,7 +1077,7 @@ function EmptyCircle({ person, depth, reach, requests, scanning, canScan, onSele
1077
1077
  return (
1078
1078
  <div role="status" style={{
1079
1079
  position: 'absolute', left: '50%', top, transform: 'translateX(-50%)', width: 'min(360px, calc(100% - 32px))',
1080
- background: 'rgba(10,10,26,0.94)', border: `1px solid ${state === 'hidden' ? 'rgba(255,255,255,0.12)' : 'rgba(0,255,136,0.25)'}`,
1080
+ background: 'color-mix(in srgb, var(--sd-bg) 94%, transparent)', border: `1px solid ${state === 'hidden' ? 'rgba(255,255,255,0.12)' : 'rgba(0,255,136,0.25)'}`,
1081
1081
  borderRadius: 12, padding: '14px 16px', textAlign: 'center',
1082
1082
  }}>
1083
1083
  <div style={{ fontSize: 13, fontWeight: 800, color: state === 'hidden' ? '#aaa' : '#fff', marginBottom: 6 }}>
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect } from 'react';
4
4
  import GalaxyLab, { NamesSwitch } from './GalaxyLab';
5
5
  import { TIERS, GRID_DEGREES, showing, showingIn, toggleCell, toggleTier, toggleDegree, showAllTiers } from '../../lib/tier-grid';
6
+ import { TIER_COLORS } from '../../lib/themes';
6
7
 
7
8
  function useIsMobile() {
8
9
  const [m, setM] = useState(false);
@@ -10,7 +11,6 @@ function useIsMobile() {
10
11
  return m;
11
12
  }
12
13
 
13
- const TIER_COLORS = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
14
14
 
15
15
 
16
16
  const DEGREE_NAMES = ['', '1st', '2nd', '3rd', '4th', '5th', '6th'];
@@ -18,7 +18,8 @@ const DEGREE_NAMES = ['', '1st', '2nd', '3rd', '4th', '5th', '6th'];
18
18
  const DEGREE_IS = {
19
19
  network: { 1: 'your connections', 2: 'people in a scanned circle', 3: 'people found by a company scan' },
20
20
  degrees: { 1: 'your bridges', 2: 'people in their circles' },
21
- paths: { 1: 'your connections', 2: 'people in a scanned circle', 3: 'people found by a company scan' },
21
+ separation: { 2: 'the people Separation ranks' },
22
+ paths:{ 1: 'your connections', 2: 'people in a scanned circle', 3: 'people found by a company scan' },
22
23
  };
23
24
 
24
25
  /**
@@ -31,6 +32,11 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
31
32
  const dot = isMobile ? 21 : 23;
32
33
  const anyAt = (d) => TIERS.some((t) => (counts[t]?.[d] || 0) > 0);
33
34
  const what = (d) => DEGREE_IS[mode]?.[d];
35
+ // Separation: your connections are the ways in, and all of them stay, whatever their tier.
36
+ const waysIn = mode === 'separation';
37
+ const emptyTitle = (d, t) => (waysIn && d === 1
38
+ ? 'Every connection stays a way in here, whatever their tier: the tiers choose who is ranked'
39
+ : `No ${t ? `${t}-Tier ` : 'one '}at ${DEGREE_NAMES[d]} degree yet`);
34
40
  return (
35
41
  <div style={{ marginBottom: 18 }}>
36
42
  <div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
@@ -44,7 +50,7 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
44
50
  const on = any && TIERS.every((t) => grid.hidden.includes(t) || !(counts[t]?.[d] > 0) || grid.degrees[t].includes(d));
45
51
  return (
46
52
  <button key={d} type="button" disabled={!any} aria-pressed={on}
47
- title={any ? `${DEGREE_NAMES[d]} degree${what(d) ? `: ${what(d)}` : ''}. Tap to ${on ? 'hide' : 'show'} it for every tier` : `Nobody at ${DEGREE_NAMES[d]} degree yet`}
53
+ title={any ? `${DEGREE_NAMES[d]} degree${what(d) ? `: ${what(d)}` : ''}. Tap to ${on ? 'hide' : 'show'} it for every tier` : emptyTitle(d)}
48
54
  onClick={() => onChange(toggleDegree(grid, d, counts))}
49
55
  style={{
50
56
  width: dot, height: 18, padding: 0, border: 'none', background: 'none', cursor: any ? 'pointer' : 'default',
@@ -87,14 +93,14 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
87
93
  aria-label={`${t}-Tier at ${DEGREE_NAMES[d]} degree`}
88
94
  title={people
89
95
  ? `${people.toLocaleString('en-US')} ${t}-Tier at ${DEGREE_NAMES[d]} degree. Tap to ${on ? 'hide' : 'show'} them`
90
- : `No ${t}-Tier at ${DEGREE_NAMES[d]} degree yet`}
96
+ : emptyTitle(d, t)}
91
97
  onClick={() => onChange(toggleCell(grid, t, d, counts))}
92
98
  style={{
93
99
  width: dot, height: dot, padding: 0, borderRadius: '50%', boxSizing: 'border-box',
94
100
  cursor: people ? 'pointer' : 'default', fontSize: 10, fontWeight: 800, lineHeight: 1,
95
- border: !people ? '1px dashed rgba(255,255,255,0.1)' : on ? `1.5px solid ${color}` : `1.5px solid ${color}70`,
96
- background: on ? color : 'transparent',
97
- color: !people ? 'rgba(255,255,255,0.12)' : on ? '#0a0a1a' : `${color}b0`,
101
+ border: waysIn && d === 1 ? `1px solid ${color}40` : !people ? '1px dashed rgba(255,255,255,0.1)' : on ? `1.5px solid ${color}` : `1.5px solid ${color}70`,
102
+ background: on ? color : waysIn && d === 1 ? `${color}1f` : 'transparent',
103
+ color: waysIn && d === 1 ? `${color}90` : !people ? 'rgba(255,255,255,0.12)' : on ? '#0a0a1a' : `${color}b0`,
98
104
  opacity: off && people ? 0.45 : 1,
99
105
  }}>
100
106
  {d}
@@ -112,7 +118,9 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
112
118
 
113
119
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 8 }}>
114
120
  <div style={{ fontSize: 10, color: '#667', lineHeight: 1.4 }}>
115
- Tap a tier to hide it, or a dot to show that degree.
121
+ {waysIn
122
+ ? 'Tap a tier to leave it out of the ranking. Every connection stays a way in.'
123
+ : 'Tap a tier to hide it, or a dot to show that degree.'}
116
124
  </div>
117
125
  <div style={{ fontSize: 10.5, color: '#889', whiteSpace: 'nowrap', marginLeft: 8 }}>
118
126
  {total.toLocaleString('en-US')} showing
@@ -130,7 +138,7 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
130
138
 
131
139
  export default function FilterPanel({ collapsed, onToggle, mode, visualMode, grid, gridCounts = {}, onGridChange }) {
132
140
  const isMobile = useIsMobile();
133
- const isDegreesMode = mode === 'degrees';
141
+ const isDegreesMode = mode === 'degrees' || mode === 'separation';
134
142
 
135
143
  if (collapsed) {
136
144
  return (
@@ -168,8 +176,8 @@ export default function FilterPanel({ collapsed, onToggle, mode, visualMode, gri
168
176
  borderRight: '1px solid rgba(52,152,219,0.15)',
169
177
  padding: isMobile ? '12px 10px' : '16px 14px',
170
178
  overflowY: 'auto', overflowX: 'hidden',
171
- background: isMobile ? 'rgba(10,15,30,0.98)' : 'rgba(10,15,30,0.65)', fontSize: 13,
172
- backdropFilter: 'blur(24px) saturate(1.4)', WebkitBackdropFilter: 'blur(24px) saturate(1.4)',
179
+ background: isMobile ? 'rgba(10,15,30,0.98)' : 'var(--sd-panel)', fontSize: 13,
180
+ backdropFilter: 'var(--sd-panel-blur)', WebkitBackdropFilter: 'var(--sd-panel-blur)',
173
181
  boxShadow: 'inset 0 0 60px rgba(52,152,219,0.04), 4px 0 24px rgba(0,0,0,0.3)',
174
182
  }}>
175
183
  {/* Close button */}
@@ -186,7 +194,7 @@ export default function FilterPanel({ collapsed, onToggle, mode, visualMode, gri
186
194
  </button>
187
195
 
188
196
  {/* Which tiers, at which degrees: the same grid in Network Circle and in Degrees */}
189
- <TierGrid grid={grid} counts={gridCounts} onChange={onGridChange} mode={isDegreesMode ? 'degrees' : 'network'} isMobile={isMobile} />
197
+ <TierGrid grid={grid} counts={gridCounts} onChange={onGridChange} mode={mode} isMobile={isMobile} />
190
198
 
191
199
  {/* Network Circle's Galaxy: the physics that lays it out is what this panel is for. */}
192
200
  {!isDegreesMode && visualMode === 'galaxy' && (isMobile ? <NamesSwitch /> : <GalaxyLab />)}
@@ -6,8 +6,10 @@ import { localPhoto } from '../../lib/photos';
6
6
  import { recentre } from '../../lib/galaxy';
7
7
  import { reachIndex, readyByCircle, scanBars } from '../../lib/reach';
8
8
  import { ringSegments, RING } from '../../lib/dot-rings';
9
- import { LAB_DEFAULTS, labNow, watchLab, effectiveLab, clockNow, watchClock, setClock, stopReplay, bornTimes, reachCounts, colourScheme, findMatches, loadSocial, chapterAt, heatColour } from '../../lib/galaxy-lab';
9
+ import { LAB_DEFAULTS, FORCE_KEYS, labNow, watchLab, effectiveLab, clockNow, watchClock, setClock, stopReplay, bornTimes, reachCounts, colourScheme, findMatches, loadSocial, chapterAt, heatColour } from '../../lib/galaxy-lab';
10
10
  import { registerGalaxy } from '../../lib/galaxy-export';
11
+ import { MAP_LOOK } from '../../lib/themes';
12
+ import { keyFor, routeIndex } from '../../lib/separation';
11
13
 
12
14
  // Connection fields are attacker-reachable: /api/ingest and /api/update-images
13
15
  // accept writes, and a page on any other site can POST to this app on localhost.
@@ -63,6 +65,8 @@ const RING_CSS = `
63
65
  .lab-focus .dot-rings, .lab-focus .catalyst-ring { opacity: 0.15; }
64
66
  .lab-find .gn:not(.found), .lab-find .gl:not(.found) { opacity: 0.07; }
65
67
  .lab-find .gn.found { stroke: #fff; stroke-width: 2px; }
68
+ .far text:not(.hub) { opacity: 0; }
69
+ @media (prefers-reduced-motion: no-preference) { .far text, g:not(.far) > text { transition: opacity 0.25s; } }
66
70
  @keyframes lab-pop { from { opacity: 0; } }
67
71
  @media (prefers-reduced-motion: no-preference) { .lab-pop .gn, .lab-pop .gl { animation: lab-pop 0.5s ease-out; } }
68
72
  @media (prefers-reduced-motion: reduce) {
@@ -84,6 +88,10 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
84
88
  }
85
89
  return out;
86
90
  }, [connections, fullDegree1, fullDegree2, scanNotes]);
91
+ // Every circle each person two steps away is in, from every circle scanned:
92
+ // the Galaxy links them to each of those connections, not only the one their
93
+ // row came through, so a shared person sits between the hubs they join.
94
+ const routes = useMemo(() => routeIndex(fullDegree2 || []), [fullDegree2]);
87
95
  const svgRef = useRef(null);
88
96
  const ringRef = useRef(null);
89
97
  const sceneRef = useRef(null); // The scene on screen: { resize, select }
@@ -108,6 +116,7 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
108
116
  const c = clockNow();
109
117
  sceneRef.current?.setTime(c.at);
110
118
  sceneRef.current?.setFind(c.find, c.fly);
119
+ sceneRef.current?.setFit(c.fit);
111
120
  }), []);
112
121
  useEffect(() => () => stopReplay(), []);
113
122
 
@@ -179,7 +188,7 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
179
188
  const saved = transform ? recentre(transform, setFor, size) : null;
180
189
 
181
190
  const select = (d) => onSelectRef.current?.(d);
182
- const scene = renderNetworkMode(svg, ringRef.current, size, connections, select, tierColors, focusNodeRef, saved, viewRef, userName, marks, effectiveLab(labRef.current), stampRef.current, schemeRef.current);
191
+ const scene = renderNetworkMode(svg, ringRef.current, size, connections, select, tierColors, focusNodeRef, saved, viewRef, userName, marks, effectiveLab(labRef.current), stampRef.current, schemeRef.current, routes);
183
192
  sceneRef.current = scene;
184
193
  scene.select(selectedIdRef.current);
185
194
  setClock({ min: scene.range.min, max: scene.range.max, of: scene.range.of });
@@ -200,6 +209,7 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
200
209
  // the same nodes, so they fought each other and the graph shook.
201
210
  // Refreshing made it worse because nothing ever stopped the old ones.
202
211
  scene.simulation.stop();
212
+ scene.dispose();
203
213
  unregister();
204
214
  // And any glide or fly-to still under way, which would otherwise go on
205
215
  // moving the next scene's view. viewRef already holds where it was going.
@@ -211,12 +221,12 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
211
221
  // why onSelect is not a dependency. The size is read through sizeRef: a new
212
222
  // size re-fits the view rather than rebuilding.
213
223
  // eslint-disable-next-line react-hooks/exhaustive-deps
214
- }, [connections, tierColors, measured, marks]);
224
+ }, [connections, tierColors, measured, marks, routes]);
215
225
 
216
226
  return (
217
227
  <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
218
228
  <style>{RING_CSS}</style>
219
- <svg ref={svgRef} width={dimensions?.width ?? 800} height={dimensions?.height ?? 600} style={{ background: '#0a0a1a' }} />
229
+ <svg ref={svgRef} width={dimensions?.width ?? 800} height={dimensions?.height ?? 600} style={{ background: 'transparent' }} />
220
230
  {/* The selection ring. The scene places it over the selected dot. */}
221
231
  <div ref={ringRef} className="galaxy-ring" style={{
222
232
  position: 'absolute', left: 0, top: 0, display: 'none',
@@ -272,7 +282,7 @@ function easeRadius(el, r) {
272
282
  // Graph coordinates put you at 0,0, whatever the size of the box; the zoom
273
283
  // transform places that in the box. So a new size only moves the view, and a
274
284
  // rebuild can start from the view as it was.
275
- function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, focusNodeRef, savedTransform, viewRef, userName, marks = new Map(), lab = LAB_DEFAULTS, stamp = null, scheme = null) {
285
+ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, focusNodeRef, savedTransform, viewRef, userName, marks = new Map(), lab = LAB_DEFAULTS, stamp = null, scheme = null, routes = new Map()) {
276
286
  let colourOf = scheme?.of ?? ((d) => tierColors[d.tier] || '#666');
277
287
  const centerNode = {
278
288
  id: CENTER_ID, name: userName || 'You', tier: 'center', degree: 0,
@@ -323,6 +333,30 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
323
333
  const via = parentOf.get(c.id) ?? CENTER_ID;
324
334
  return { source: via, target: c.id, near: via !== CENTER_ID };
325
335
  });
336
+ // Every other way in, for someone two steps away who is in more than one
337
+ // circle on screen: a line to each of those connections too, so they sit
338
+ // between the hubs they join and tie them together, as a note linked from
339
+ // two others does in Obsidian. After the first lines, so links[i] stays
340
+ // connections[i] for the replay.
341
+ const alsoVia = new Map(); // id → the other connections they're reached through
342
+ for (const n of nodes) {
343
+ if (n.degree !== 2 || !parentOf.has(n.id)) continue;
344
+ const ways = routes.get(keyFor(n));
345
+ if (!ways || ways.size < 2) continue;
346
+ const others = [...ways].filter((id) => id != null && id !== parentOf.get(n.id) && drawnD1.has(id));
347
+ if (others.length) alsoVia.set(n.id, others);
348
+ }
349
+ for (const [id, others] of alsoVia) for (const via of others) links.push({ source: via, target: id, near: true, also: true });
350
+ // How many lines meet at each dot (Size by → Links, as Obsidian sizes a note),
351
+ // and who has a circle on screen (a hub).
352
+ const linkCount = new Map();
353
+ for (const l of links) {
354
+ linkCount.set(l.source, (linkCount.get(l.source) || 0) + 1);
355
+ linkCount.set(l.target, (linkCount.get(l.target) || 0) + 1);
356
+ }
357
+ const hubs = new Set(parentOf.values());
358
+ const circleOf = new Map(); // hub → how many hang off them
359
+ for (const p of parentOf.values()) circleOf.set(p, (circleOf.get(p) || 0) + 1);
326
360
 
327
361
  const tierRadius = (tier) => {
328
362
  switch (tier) { case 'S': return 150; case 'A': return 250; case 'B': return 350; case 'C': return 450; default: return 520; }
@@ -352,7 +386,11 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
352
386
  const reach = reachCounts(nodes, parentOf);
353
387
 
354
388
  const nodeRadius = (d) => {
355
- if (d.id === CENTER_ID) return 18;
389
+ if (d.id === CENTER_ID) return L.sizeBy === 'links' ? 24 : 18;
390
+ // Sized by links, a dot grows with the lines that meet at it: a connection
391
+ // with a scanned circle is a big hub, someone in three circles a little
392
+ // bigger than someone in one.
393
+ if (L.sizeBy === 'links') return Math.min(44, 3.5 + 2.1 * Math.sqrt(linkCount.get(d.id) || 1)) * L.dotSize;
356
394
  // Sized by reach, a dot grows with everyone who hangs off it: a 2nd-degree
357
395
  // person with a full circle behind them next to one with nobody.
358
396
  if (L.sizeBy === 'reach') return Math.min(28, 2.5 + 1.6 * Math.sqrt(reach.get(d.id) || 0)) * L.dotSize;
@@ -397,9 +435,13 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
397
435
  // through re-fits the destination, and a rebuild starts from it, rather than
398
436
  // from a point along the way.
399
437
  let heading = null;
400
- const zoomBehavior = d3.zoom().scaleExtent([0.3, 4]).on('zoom', (e) => {
438
+ // Names fade out zoomed out, as Obsidian's text does, all but the hubs'.
439
+ let labelsG = null;
440
+ const FAR = 0.85;
441
+ const zoomBehavior = d3.zoom().scaleExtent([0.1, 4]).on('zoom', (e) => {
401
442
  g.attr('transform', e.transform);
402
443
  viewRef.current = { transform: heading ?? e.transform, size: box };
444
+ labelsG?.classed('far', e.transform.k < FAR);
403
445
  placeRing();
404
446
  });
405
447
  svg.call(zoomBehavior);
@@ -490,14 +532,28 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
490
532
  // Each force is today's value times its slider in the lab (1 when it's off).
491
533
  const linkBase = isMobileGraph ? 0 : layered ? 0.06 : 0.1;
492
534
  const radialBase = layered ? 0.45 : 0.3;
493
- const linkDistance = d => (d.near ? Math.max(40, radiusOf(d.target) - radiusOf(d.source)) : radiusOf(d.target)) * L.distance;
535
+ const linkDistance = (d) => {
536
+ const base = (d.near ? Math.max(40, radiusOf(d.target) - radiusOf(d.source)) : radiusOf(d.target)) * L.distance;
537
+ if (L.sizeBy !== 'links') return base;
538
+ // Sized by links a hub is big, so its circle starts at its edge rather than
539
+ // under it; and a hub sits as far from you as its circle is wide, so the
540
+ // circles go round you like petals instead of piling up in the middle.
541
+ const petal = d.source.id === CENTER_ID ? 7 * Math.sqrt(circleOf.get(d.target.id) || 0) : 0;
542
+ return base + nodeRadius(d.source) + nodeRadius(d.target) + petal;
543
+ };
494
544
  const charge = d => (d.id === CENTER_ID ? -20 : -1) * L.push;
545
+ // The center force pulls you, your connections and every hub; someone in a
546
+ // circle mostly follows their hub, so each circle holds together round it
547
+ // rather than being drawn into one even disc with all the others.
548
+ const gravity = d => 0.15 * L.gravity * (hubs.has(d.id) ? 0.5 : d.degree >= 2 ? 0.2 : 1);
549
+ // Thicker lines show more of themselves too, so a cluster's links read at a distance.
550
+ const lineOpacity = () => Math.min(0.45, 0.1 + 0.05 * L.lines);
495
551
  const simulation = d3.forceSimulation(nodes)
496
552
  .force('link', d3.forceLink(links).id(d => d.id).distance(linkDistance).strength(Math.min(1, linkBase * L.pull)))
497
553
  .force('charge', d3.forceManyBody().strength(isMobileGraph ? 0 : charge))
498
554
  .force('center', isMobileGraph ? null : d3.forceCenter(0, 0))
499
- .force('gravityX', isMobileGraph ? null : d3.forceX(0).strength(0.15 * L.gravity))
500
- .force('gravityY', isMobileGraph ? null : d3.forceY(0).strength(0.15 * L.gravity))
555
+ .force('gravityX', isMobileGraph ? null : d3.forceX(0).strength(gravity))
556
+ .force('gravityY', isMobileGraph ? null : d3.forceY(0).strength(gravity))
501
557
  .force('collision', isMobileGraph ? null : d3.forceCollide().radius(d => nodeRadius(d) + 2))
502
558
  .force('radial', isMobileGraph ? null : d3.forceRadial(d => d.id === CENTER_ID ? 0 : radiusOf(d), 0, 0).strength(radialBase * L.rings));
503
559
  guidesG?.attr('opacity', Math.min(1, L.rings));
@@ -538,10 +594,12 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
538
594
  // forceLink has already swapped each link's ids for the nodes themselves, so
539
595
  // the colour is one step away. It used to search every node for every link:
540
596
  // about 1.8 s at 30,000 people.
597
+ // The theme says whether a line takes the colour of the person it leads to, or one colour for all (lib/themes.js).
598
+ const lineColour = (d) => (MAP_LOOK.lines === 'one' ? MAP_LOOK.line : colourOf(d.target) || '#333');
541
599
  const link = g.append('g').selectAll('line').data(links).join('line')
542
600
  .attr('class', 'gl')
543
- .attr('stroke', d => colourOf(d.target) || '#333')
544
- .attr('stroke-opacity', 0.15).attr('stroke-width', 0.5 * L.lines);
601
+ .attr('stroke', lineColour)
602
+ .attr('stroke-opacity', lineOpacity()).attr('stroke-width', 0.5 * L.lines);
545
603
 
546
604
  // Catalyst outer glow rings (rendered behind the nodes)
547
605
  // A catalyst is the green outline on their dot (design C, with the scan
@@ -593,7 +651,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
593
651
  const node = g.append('g').selectAll('circle').data(nodes).join('circle')
594
652
  .attr('class', 'gn')
595
653
  .attr('r', nodeRadius)
596
- .attr('fill', d => d.id === CENTER_ID ? '#fff' : colourOf(d))
654
+ .attr('fill', d => d.id === CENTER_ID ? MAP_LOOK.you : colourOf(d))
597
655
  .attr('fill-opacity', d => (d.degree >= 3 ? 0.55 : d.degree === 2 ? 0.75 : 1))
598
656
  .attr('stroke', d => {
599
657
  if (d.id === CENTER_ID) return '#FFD700';
@@ -688,15 +746,17 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
688
746
  const labelled = (n) => {
689
747
  if (findHit && findHit.size <= 40 && findHit.has(n.id)) return true;
690
748
  if (!L.labels) return false;
691
- return n.id === CENTER_ID || (n.degree === 1 && (L.names === 'all' || n.tier === 'S' || n.is_catalyst));
749
+ return n.id === CENTER_ID || (n.degree === 1 && (L.names === 'all' || n.tier === 'S' || n.is_catalyst || hubs.has(n.id)));
692
750
  };
693
- const labelsG = g.append('g');
751
+ labelsG = g.append('g').classed('far', d3.zoomTransform(svg.node()).k < FAR);
694
752
  let labels = labelsG.selectAll('text');
695
753
  const drawLabels = () => {
696
754
  labels = labelsG.selectAll('text').data(nodes.filter(labelled), d => d.id).join('text')
697
755
  .text(d => d.is_catalyst && d.tier !== 'S' ? d.name + ' ⚡' : d.name)
698
- .attr('font-size', d => d.id === CENTER_ID ? 14 : 10)
699
- .attr('font-weight', d => d.id === CENTER_ID ? 700 : 500)
756
+ .attr('class', d => (d.id === CENTER_ID || hubs.has(d.id) ? 'hub' : null))
757
+ // A hub's name grows with its dot, so it still reads with everyone on screen.
758
+ .attr('font-size', d => d.id === CENTER_ID ? 14 : hubs.has(d.id) ? Math.max(12, Math.min(22, nodeRadius(d) * 0.5)) : 10)
759
+ .attr('font-weight', d => d.id === CENTER_ID || hubs.has(d.id) ? 700 : 500)
700
760
  .attr('fill', d => {
701
761
  if (d.id === CENTER_ID) return '#fff';
702
762
  if (d.is_catalyst) return '#00ff88';
@@ -710,7 +770,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
710
770
 
711
771
  const catalystRings = g.selectAll('.catalyst-ring');
712
772
 
713
- simulation.on('tick', () => {
773
+ const paint = () => {
714
774
  link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
715
775
  node.attr('cx', d => d.x).attr('cy', d => d.y);
716
776
  catalystRings.attr('cx', d => d.x).attr('cy', d => d.y);
@@ -718,8 +778,77 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
718
778
  labels.attr('x', d => d.x).attr('y', d => d.y);
719
779
  if (heatFn) heatDots.attr('cx', d => d.x).attr('cy', d => d.y);
720
780
  placeRing();
781
+ };
782
+ let tickedAt = 0;
783
+ simulation.on('tick', () => {
784
+ tickedAt = performance.now();
785
+ paint();
786
+ if (fitArmed && simulation.alpha() < SETTLED) { fitArmed = false; fitView(); }
721
787
  });
722
788
 
789
+ // Fit: everyone on screen, once the layout has settled (a layout picked in
790
+ // the lab, or its Fit button). Asked for through the clock's `fit` count.
791
+ const SETTLED = 0.06;
792
+ let fitArmed = false;
793
+ let lastFit = clockNow().fit;
794
+ const fitView = () => {
795
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
796
+ nodes.forEach((n, i) => {
797
+ if (!shown[i]) return;
798
+ const r = nodeRadius(n);
799
+ x0 = Math.min(x0, n.x - r); y0 = Math.min(y0, n.y - r); x1 = Math.max(x1, n.x + r); y1 = Math.max(y1, n.y + r);
800
+ });
801
+ if (!Number.isFinite(x0) || !box.width || !box.height) return;
802
+ const k = Math.max(0.1, Math.min(2, Math.min(box.width / (x1 - x0 + 80), box.height / (y1 - y0 + 80))));
803
+ moveView(d3.zoomIdentity.translate(box.width / 2 - ((x0 + x1) / 2) * k, box.height / 2 - ((y0 + y1) / 2) * k).scale(k), 750);
804
+ };
805
+ let fitCheck = null;
806
+ const setFit = (fit) => {
807
+ if (fit === lastFit) return;
808
+ lastFit = fit;
809
+ fitArmed = true;
810
+ // A layout just picked restarts the forces a moment later; a settled one fits now.
811
+ clearTimeout(fitCheck);
812
+ fitCheck = setTimeout(() => {
813
+ if (fitArmed && simulation.alpha() < SETTLED && performance.now() - tickedAt > 100) { fitArmed = false; fitView(); }
814
+ }, 150);
815
+ };
816
+
817
+ // Orbit (the lab's slider): the whole map turns round you, everyone at once,
818
+ // so nothing moves against anything else and the layout keeps its shape
819
+ // (each circle spinning on its own hub was tried: while the forces were still
820
+ // settling it dragged every circle off its hub). Moved in place every frame
821
+ // rather than by the forces, so it costs a repaint; a big network repaints
822
+ // every second or third frame, at the same speed. Never with Reduce Motion,
823
+ // nor on a phone (whose layout has no physics).
824
+ const every = nodes.length > 12000 ? 3 : nodes.length > 3000 ? 2 : 1;
825
+ let orbitTimer = null;
826
+ let lastT = 0;
827
+ let frame = 0;
828
+ const turn = (elapsed) => {
829
+ if (++frame % every) return;
830
+ const dt = Math.min(0.1, (elapsed - lastT) / 1000);
831
+ lastT = elapsed;
832
+ if (dt <= 0) return;
833
+ const a = 0.1 * L.orbit * dt; // radians this frame
834
+ const c = Math.cos(a), sn = Math.sin(a);
835
+ for (const n of nodes) {
836
+ if (n.fx != null) continue; // you, and anyone being dragged
837
+ const { x, y } = n;
838
+ n.x = x * c - y * sn;
839
+ n.y = x * sn + y * c;
840
+ // Its momentum turns with it, or while the forces settle it drifts the old way.
841
+ if (n.vx) { const vx = n.vx; n.vx = vx * c - n.vy * sn; n.vy = vx * sn + n.vy * c; }
842
+ }
843
+ // While the forces run, their tick paints.
844
+ if (performance.now() - tickedAt > 50) paint();
845
+ };
846
+ const setOrbit = () => {
847
+ const on = !isMobileGraph && L.orbit > 0 && !reducedMotion();
848
+ if (on && !orbitTimer) { lastT = 0; frame = 0; orbitTimer = d3.timer(turn); }
849
+ if (!on && orbitTimer) { orbitTimer.stop(); orbitTimer = null; }
850
+ };
851
+
723
852
  // A slider moved: change the running layout in place and let it settle again.
724
853
  const setLab = (next) => {
725
854
  if (isMobileGraph) {
@@ -736,16 +865,17 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
736
865
  simulation.force('collision').radius(d => nodeRadius(d) + 2);
737
866
  if (ringNode) select(ringNode.id);
738
867
  }
739
- if (prev.lines !== L.lines) link.attr('stroke-width', 0.5 * L.lines);
868
+ if (prev.lines !== L.lines) link.attr('stroke-width', 0.5 * L.lines).attr('stroke-opacity', lineOpacity());
740
869
  if (sized || prev.names !== L.names || prev.labels !== L.labels) { drawLabels(); showBorn(); }
741
870
  if (!L.on || !L.branch) g.classed('lab-focus', false);
742
871
  g.classed('lab-pop', L.on && nodes.length < 5000);
743
- const forces = ['gravity', 'rings', 'push', 'pull', 'distance'].some(k => prev[k] !== L[k]);
872
+ if (prev.orbit !== L.orbit) setOrbit();
873
+ const forces = FORCE_KEYS.some(k => prev[k] !== L[k]);
744
874
  if (!forces && !sized) return;
745
875
  simulation.force('link').distance(linkDistance).strength(Math.min(1, linkBase * L.pull));
746
876
  simulation.force('charge').strength(charge);
747
- simulation.force('gravityX').strength(0.15 * L.gravity);
748
- simulation.force('gravityY').strength(0.15 * L.gravity);
877
+ simulation.force('gravityX').strength(gravity);
878
+ simulation.force('gravityY').strength(gravity);
749
879
  simulation.force('radial').strength(radialBase * L.rings);
750
880
  guidesG?.attr('opacity', Math.min(1, L.rings));
751
881
  simulation.alpha(Math.max(simulation.alpha(), 0.5)).restart();
@@ -762,6 +892,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
762
892
  dotRings.each(function (d) { extraEls.set(d.id, [this]); });
763
893
  catalystRings.each(function (d) { extraEls.set(d.id, [...(extraEls.get(d.id) || []), this]); });
764
894
  const shown = new Uint8Array(nodes.length).fill(1);
895
+ const idxOf = new Map(nodes.map((n, i) => [n.id, i]));
765
896
  const d1 = nodes.filter(n => n.degree === 1);
766
897
  const dated = d1.map(n => bornById.get(n.id)).filter(Number.isFinite);
767
898
  const range = { min: dated.length ? dated.reduce((m, t) => Math.min(m, t)) : null, max: dated.length ? dated.reduce((m, t) => Math.max(m, t)) : null, of: d1.length };
@@ -786,6 +917,11 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
786
917
  if (linkEls[i - 1]) linkEls[i - 1].style.display = display;
787
918
  for (const el of extraEls.get(nodes[i].id) || []) el.style.display = display;
788
919
  }
920
+ // A line to another way in shows while both its ends do.
921
+ for (let j = connections.length; j < links.length; j++) {
922
+ const vis = shown[idxOf.get(links[j].source.id)] && shown[idxOf.get(links[j].target.id)];
923
+ linkEls[j].style.display = vis ? '' : 'none';
924
+ }
789
925
  showBorn();
790
926
  if (!stamp) return;
791
927
  stamp.style.display = at == null ? 'none' : 'block';
@@ -801,8 +937,8 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
801
937
  colourOf = next.of;
802
938
  heatFn = next.heat || null;
803
939
  drawHeat();
804
- node.attr('fill', d => d.id === CENTER_ID ? '#fff' : colourOf(d));
805
- link.attr('stroke', d => colourOf(d.target) || '#333');
940
+ node.attr('fill', d => d.id === CENTER_ID ? MAP_LOOK.you : colourOf(d));
941
+ link.attr('stroke', lineColour);
806
942
  drawLabels();
807
943
  showBorn();
808
944
  if (ringNode) select(ringNode.id);
@@ -836,7 +972,9 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
836
972
  }
837
973
  };
838
974
 
839
- return { simulation, resize, select, setLab, setTime, setColours, setFind, range };
975
+ setOrbit();
976
+ const dispose = () => { orbitTimer?.stop(); orbitTimer = null; clearTimeout(fitCheck); };
977
+ return { simulation, resize, select, setLab, setTime, setColours, setFind, setFit, dispose, range };
840
978
  }
841
979
 
842
980
  function setupDrag(node, simulation, fixedId) {