six-degrees 0.4.11 → 0.5.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 (375) 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 +9 -9
  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.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  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 +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +3 -3
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +14 -14
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +14 -14
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  81. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/launch.html +2 -2
  84. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  85. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  86. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  87. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/paths.html +1 -1
  93. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  94. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/profile.html +1 -1
  100. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  101. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/queue.html +1 -1
  107. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  108. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/scores.html +1 -1
  114. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  115. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/settings.html +1 -1
  121. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  122. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/setup.html +1 -1
  128. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  129. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/app/social.html +1 -1
  135. package/.next/standalone/.next/server/app/social.rsc +14 -14
  136. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  137. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  138. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__13dx4sd._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  143. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  145. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  148. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  149. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  150. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  151. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  152. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  155. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  156. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  157. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  158. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  159. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  160. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  161. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  162. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  163. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  170. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  178. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  181. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  184. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  186. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -2
  187. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  190. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  191. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  192. package/.next/standalone/.next/server/pages/404.html +1 -1
  193. package/.next/standalone/.next/server/pages/500.html +1 -1
  194. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  196. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  197. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  198. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  200. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  201. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  202. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  203. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  204. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  205. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  206. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  207. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  208. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  209. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  210. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  211. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  212. package/.next/standalone/CHANGELOG.md +187 -0
  213. package/.next/standalone/app/api/ingest/route.js +40 -1
  214. package/.next/standalone/app/api/scraper/route.js +7 -1
  215. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  216. package/.next/standalone/app/components/AppTabs.js +59 -0
  217. package/.next/standalone/app/components/ChainView.js +303 -89
  218. package/.next/standalone/app/components/CirclePanel.js +162 -0
  219. package/.next/standalone/app/components/CompanyScores.js +2 -2
  220. package/.next/standalone/app/components/FieldStep.js +4 -4
  221. package/.next/standalone/app/components/FilterPanel.js +121 -183
  222. package/.next/standalone/app/components/ForceGraph.js +41 -3
  223. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  224. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  225. package/.next/standalone/app/components/MapControls.js +154 -0
  226. package/.next/standalone/app/components/NoteDetail.js +118 -0
  227. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  228. package/.next/standalone/app/components/PeopleMap.js +308 -0
  229. package/.next/standalone/app/components/ScanRadar.js +129 -0
  230. package/.next/standalone/app/components/ScanStatusBar.js +71 -27
  231. package/.next/standalone/app/components/SeparationView.js +626 -118
  232. package/.next/standalone/app/components/Sidebar.js +28 -2
  233. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  234. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  235. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  236. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  237. package/.next/standalone/app/components/views.js +15 -14
  238. package/.next/standalone/app/page.js +89 -156
  239. package/.next/standalone/app/paths/page.js +39 -27
  240. package/.next/standalone/app/profile/page.js +33 -5
  241. package/.next/standalone/app/queue/page.js +56 -34
  242. package/.next/standalone/app/scores/page.js +16 -66
  243. package/.next/standalone/app/settings/page.js +27 -13
  244. package/.next/standalone/app/setup/page.js +298 -205
  245. package/.next/standalone/app/social/SocialHub.js +374 -0
  246. package/.next/standalone/app/social/page.js +10 -362
  247. package/.next/standalone/lib/chain-layout.js +105 -35
  248. package/.next/standalone/lib/degrees.js +0 -6
  249. package/.next/standalone/lib/dot-rings.js +25 -0
  250. package/.next/standalone/lib/galaxy-lab.js +65 -6
  251. package/.next/standalone/lib/ingest.js +2 -0
  252. package/.next/standalone/lib/island.js +40 -0
  253. package/.next/standalone/lib/linkedin-limits.js +6 -2
  254. package/.next/standalone/lib/map-heat.js +20 -0
  255. package/.next/standalone/lib/network-health.js +1 -1
  256. package/.next/standalone/lib/notifications.js +121 -0
  257. package/.next/standalone/lib/people-map.js +82 -0
  258. package/.next/standalone/lib/rarity.js +45 -3
  259. package/.next/standalone/lib/reach.js +7 -6
  260. package/.next/standalone/lib/rpc.js +15 -4
  261. package/.next/standalone/lib/scan-pace.js +46 -0
  262. package/.next/standalone/lib/scoring.js +39 -8
  263. package/.next/standalone/lib/scraper-client.js +19 -6
  264. package/.next/standalone/lib/sector-focus.js +4 -4
  265. package/.next/standalone/lib/separation.js +137 -10
  266. package/.next/standalone/lib/settings-effects.js +20 -11
  267. package/.next/standalone/lib/settings.js +3 -0
  268. package/.next/standalone/lib/tier-grid.js +99 -0
  269. package/.next/standalone/lib/title-ranking.js +83 -0
  270. package/.next/standalone/package-lock.json +2 -2
  271. package/.next/standalone/package.json +1 -1
  272. package/.next/standalone/scripts/scrape.py +51 -13
  273. package/.next/static/chunks/03bziijw5srol.js +1 -0
  274. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  275. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  276. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  277. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  278. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  279. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  280. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  281. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  282. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  283. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  284. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  285. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  286. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  287. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  288. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  289. package/CHANGELOG.md +187 -0
  290. package/lib/chain-layout.js +105 -35
  291. package/lib/degrees.js +0 -6
  292. package/lib/dot-rings.js +25 -0
  293. package/lib/galaxy-lab.js +65 -6
  294. package/lib/ingest.js +2 -0
  295. package/lib/island.js +40 -0
  296. package/lib/linkedin-limits.js +6 -2
  297. package/lib/map-heat.js +20 -0
  298. package/lib/network-health.js +1 -1
  299. package/lib/notifications.js +121 -0
  300. package/lib/people-map.js +82 -0
  301. package/lib/rarity.js +45 -3
  302. package/lib/reach.js +7 -6
  303. package/lib/rpc.js +15 -4
  304. package/lib/scan-pace.js +46 -0
  305. package/lib/scoring.js +39 -8
  306. package/lib/scraper-client.js +19 -6
  307. package/lib/sector-focus.js +4 -4
  308. package/lib/separation.js +137 -10
  309. package/lib/settings-effects.js +20 -11
  310. package/lib/settings.js +3 -0
  311. package/lib/tier-grid.js +99 -0
  312. package/lib/title-ranking.js +83 -0
  313. package/package.json +1 -1
  314. package/scripts/scrape.py +51 -13
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  320. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  323. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  329. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  330. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  331. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +0 -3
  333. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
  334. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  335. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  336. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  337. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  338. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  339. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  340. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  341. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  342. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  343. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  344. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  345. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  346. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +0 -1
  347. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  348. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  349. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  350. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +0 -1
  351. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  352. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  353. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  354. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  355. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  356. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  357. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  358. package/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  359. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  360. package/.next/static/chunks/1a74dky22dxd6.js +0 -1
  361. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  362. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  363. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  364. package/.next/static/chunks/297e1z34yi--z.js +0 -1
  365. package/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  366. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  367. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  368. package/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  369. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  370. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  371. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  372. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  373. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  374. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  375. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -8,6 +8,19 @@
8
8
  // is organised by the PERSON. Someone three of your connections know is one row
9
9
  // with three routes, not three rows (lib/separation.js does that merge).
10
10
  //
11
+ // The slider (Blake, 2026-10-02: "i want it to be slider ajusted and can be
12
+ // dynmaic") reorders everything as it moves: the rarest ways in at one end,
13
+ // the easiest at the other, each person's own score alone in the middle
14
+ // (lib/rarity.js slideValue). It never changes a score, a tier or a rank.
15
+ //
16
+ // The map follows the slider too (Blake, 2026-10-02): from the rare end to the
17
+ // middle it fans out to ten people, each with the one or two doors that lead
18
+ // to them; towards the easy end it closes in on fewer, and at the end on the
19
+ // one person you're aiming at, with every connection of yours who leads to them.
20
+ //
21
+ // "Path" beside the tier chips narrows everyone to one sector or one company.
22
+ // The picker opens under the button and goes away once you've picked.
23
+ //
11
24
  // Two parts, one scroll: a small "summit map" of the top of the list, drawn
12
25
  // from You through each bridge to each person, and under it the full ranked
13
26
  // list. The list is windowed — about thirty rows are mounted whether there are
@@ -19,12 +32,14 @@
19
32
  // when it changes. The map's height never depends on its width, so a scrollbar
20
33
  // appearing cannot feed a resize loop.
21
34
 
22
- import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
23
- import { separationPeople, compareWaysIn, summitLayout, shortName, TIERS, keyFor, routeIndex } from '../../lib/separation';
24
- import { RARITY, rarityOf, rarityInfo, toggle } from '../../lib/rarity';
35
+ import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
36
+ import { separationPeople, comparePower, summitLayout, convergeLayout, mapCount, mostMutuals, shortName, TIERS, keyFor, routeIndex } from '../../lib/separation';
37
+ import { companyOf, industryOf, INDUSTRIES } from '../../lib/companies';
38
+ import { RARITY, rarityOf, rarityInfo, toggle, SLIDER_MIDDLE, slideValue, slideLabel, easeOf } from '../../lib/rarity';
25
39
  import { hasRequest } from '../../lib/requests-client';
26
40
  import useRequests from './useRequests';
27
41
  import { initialsFor } from '../../lib/tiers';
42
+ import { watchNotchShown, notchShownNow, noNotch } from '../../lib/island';
28
43
  import Avatar from './Avatar';
29
44
 
30
45
  const ORANGE = '#FF6B35';
@@ -35,6 +50,9 @@ const CANT_NAME = 'a connection we can’t name';
35
50
  // phone. This much side padding keeps them off the text at every width.
36
51
  const SIDE = 56;
37
52
  const LABEL_ROW = 28;
53
+ const TITLE_ROW = 44; // the ledger's title bar, on a computer (Blake, 2026-10-02: mock-up 2)
54
+ // The ledger's columns: rank, face, person, way in, how rare the way in, power.
55
+ const COLUMNS = '64px 36px minmax(0,1.3fr) minmax(0,1fr) 190px 132px';
38
56
  const BOTTOM_PAD = 72; // clear of the last row's reach, and the corner toggles
39
57
  const OVERSCAN = 8;
40
58
  const SCROLL_STEP = 16; // scroll is published to state in 16px steps, not every pixel
@@ -68,16 +86,26 @@ function subline(person) {
68
86
 
69
87
  export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC, preset = null }) {
70
88
  const isMobile = useIsMobile();
89
+ // Room at the top for the notch, only while it's on screen (a scan, or another tab's views).
90
+ const notch = useSyncExternalStore(watchNotchShown, notchShownNow, noNotch);
71
91
  const ROW_H = isMobile ? 68 : 56;
72
92
  const K = isMobile ? 5 : 10;
73
93
  const sidePad = isMobile ? 8 : 16;
74
94
 
75
95
  const [query, setQuery] = useState('');
76
96
  const q = useDeferredValue(query.trim().toLowerCase());
77
- const [sort, setSort] = useState('power'); // 'power' | 'ways'
97
+ // The slider, 0 (rarest ways in first) to 100 (easiest first); 50 is power alone.
98
+ // The list follows a deferred copy, so the thumb never waits for a sort.
99
+ const [at, setAt] = useState(SLIDER_MIDDLE);
100
+ const atSlow = useDeferredValue(at);
78
101
  const [tier, setTier] = useState('all');
79
102
  // Rarity beside the tier (lib/rarity.js): any bands switched on; none is everyone.
80
103
  const [rarities, setRarities] = useState(() => new Set());
104
+ // Path: one sector or one company, or none. The picker is only on screen while it's open.
105
+ const [path, setPath] = useState(null); // { kind: 'sector' | 'company', key, label, color }
106
+ const [pathOpen, setPathOpen] = useState(false);
107
+ const [pathText, setPathText] = useState('');
108
+ const [pathUsed, setPathUsed] = useState(false); // companies are only read once Path is opened
81
109
  // Filters a card's Insights asked for ("Show them", "S only"): applied once each.
82
110
  const [presetFor, setPresetFor] = useState(null);
83
111
  if (preset && preset.id !== presetFor) {
@@ -100,10 +128,6 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
100
128
 
101
129
  // ── Data: one merge per population, then order, then what's showing ──────
102
130
  const model = useMemo(() => separationPeople(degree2, connections), [degree2, connections]);
103
- const ordered = useMemo(
104
- () => (sort === 'ways' ? [...model.people].sort(compareWaysIn) : model.people),
105
- [model, sort],
106
- );
107
131
  // How rare the way in to each person is, counted across every circle you've
108
132
  // scanned (not only the ones a bridge-tier filter leaves): never a score.
109
133
  const waysAll = useMemo(() => routeIndex(fullDegree2), [fullDegree2]);
@@ -112,18 +136,50 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
112
136
  for (const p of model.people) m.set(p.key, rarityOf(p.person, waysAll.get(p.key)?.size || p.waysIn));
113
137
  return m;
114
138
  }, [model, waysAll]);
139
+ const ordered = useMemo(() => {
140
+ if (atSlow === SLIDER_MIDDLE) return model.people;
141
+ const value = new Map();
142
+ for (const p of model.people) value.set(p.key, slideValue(p.score, rarityBy.get(p.key)?.count || p.waysIn, atSlow));
143
+ return [...model.people].sort((a, b) => value.get(b.key) - value.get(a.key) || comparePower(a, b));
144
+ }, [model, rarityBy, atSlow]);
115
145
  const byRarity = useMemo(() => {
116
146
  const out = Object.fromEntries(RARITY.map((r) => [r.key, 0]));
117
147
  for (const r of rarityBy.values()) out[r.key]++;
118
148
  return out;
119
149
  }, [rarityBy]);
150
+ // Where everyone works, and the sector that suggests (inferred, as in Paths):
151
+ // read once, the first time Path is opened.
152
+ const facets = useMemo(() => {
153
+ if (!pathUsed) return null;
154
+ const by = new Map();
155
+ const sectors = new Map();
156
+ const companies = new Map();
157
+ for (const p of model.people) {
158
+ const company = companyOf(p.person) || null;
159
+ const sector = industryOf(company, p.person.headline).key;
160
+ by.set(p.key, { company, sector });
161
+ sectors.set(sector, (sectors.get(sector) || 0) + 1);
162
+ if (company) companies.set(company, (companies.get(company) || 0) + 1);
163
+ }
164
+ return {
165
+ by,
166
+ sectors: INDUSTRIES.map((i) => ({ ...i, count: sectors.get(i.key) || 0 })).filter((i) => i.count > 0).sort((a, b) => b.count - a.count),
167
+ companies: [...companies].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
168
+ };
169
+ }, [model, pathUsed]);
170
+ const onPath = useCallback((p) => {
171
+ if (!path || !facets) return true;
172
+ const f = facets.by.get(p.key);
173
+ return path.kind === 'sector' ? f?.sector === path.key : f?.company === path.key;
174
+ }, [path, facets]);
120
175
  const matches = useCallback((p, text) => (tier === 'all' || p.tier === tier)
121
176
  && (!rarities.size || rarities.has(rarityBy.get(p.key)?.key))
122
- && (!text || p.haystack.includes(text)), [tier, rarities, rarityBy]);
177
+ && onPath(p)
178
+ && (!text || p.haystack.includes(text)), [tier, rarities, rarityBy, onPath]);
123
179
  const visible = useMemo(() => {
124
- if (!q && tier === 'all' && !rarities.size) return ordered;
180
+ if (!q && tier === 'all' && !rarities.size && !path) return ordered;
125
181
  return ordered.filter((p) => matches(p, q));
126
- }, [ordered, q, tier, rarities, matches]);
182
+ }, [ordered, q, tier, rarities, path, matches]);
127
183
 
128
184
  // Who you've already asked, or already know. The map is "who to ask next",
129
185
  // so it moves on past them: send requests to its ten and the next ten come
@@ -131,9 +187,27 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
131
187
  const d1Keys = useMemo(() => new Set(fullDegree1.map(keyFor)), [fullDegree1]);
132
188
  const statusOf = useCallback((p) => (d1Keys.has(p.key) ? 'connected' : hasRequest(p.person, requests) ? 'asked' : null),
133
189
  [d1Keys, requests]);
134
- const top = useMemo(() => visible.filter((p) => !statusOf(p)).slice(0, K), [visible, K, statusOf]);
190
+ // How many the map draws follows the slider: ten, then fewer, then the one you're aiming at.
191
+ const shown = mapCount(atSlow, K);
192
+ const unasked = useMemo(() => visible.filter((p) => !statusOf(p)).slice(0, K + 1), [visible, K, statusOf]);
193
+ const top = useMemo(() => unasked.slice(0, shown), [unasked, shown]);
135
194
  const askedShown = useMemo(() => visible.reduce((n, p) => n + (statusOf(p) ? 1 : 0), 0), [visible, statusOf]);
136
195
  const selectedKey = selectedId != null ? model.rowToKey.get(selectedId) ?? null : null;
196
+ // Down to one: the person picked, if they're in what's showing, else the top of the list.
197
+ const single = shown === 1;
198
+ // Down to one, it aims at whoever you share the most mutual connections with,
199
+ // scanned or not (the ones not scanned are grey dots), unless you picked
200
+ // someone; "Next" are the runners-up.
201
+ const aimable = useMemo(() => {
202
+ if (!single) return [];
203
+ return mostMutuals(visible.filter((p) => !statusOf(p)), (p) => rarityBy.get(p.key)?.count || 0, (isMobile ? 3 : 6) + 1);
204
+ }, [single, visible, statusOf, rarityBy, isMobile]);
205
+ const target = useMemo(() => {
206
+ if (!single) return null;
207
+ return (selectedKey && visible.find((p) => p.key === selectedKey)) || aimable[0] || null;
208
+ }, [single, selectedKey, visible, aimable]);
209
+ const nextUp = useMemo(() => (single && target ? aimable.filter((p) => p.key !== target.key).slice(0, isMobile ? 3 : 6) : []),
210
+ [single, target, aimable, isMobile]);
137
211
 
138
212
  // onSelect is read through a ref so the row and map callbacks never change
139
213
  // identity, and memoised rows never re-render because a parent re-rendered.
@@ -159,10 +233,22 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
159
233
  const cw = Math.max(0, Math.min(980, box.w - 2 * sidePad));
160
234
  // The map's height comes from the layout, never the DOM, so the list below
161
235
  // it can be positioned by arithmetic alone.
162
- const layout = useMemo(() => summitLayout(top, cw || 800, isMobile), [top, cw, isMobile]);
236
+ // One map for both: ten people fanned out, or the one you're aiming at with
237
+ // everyone who leads to them. The same shape, so it glides from one to the other.
238
+ // Wide enough: connections as pills and people as cards (Blake, 2026-10-02); a phone keeps the dots.
239
+ const cards = !isMobile && cw >= 760;
240
+ const layout = useMemo(
241
+ () => (single
242
+ ? convergeLayout(target, cw || 800, isMobile, { cards, mutuals: target ? rarityBy.get(target.key)?.count || 0 : 0 })
243
+ : summitLayout(top, cw || 800, isMobile, { cards })),
244
+ [single, target, top, cw, isMobile, cards, rarityBy],
245
+ );
163
246
  const captionH = isMobile ? 58 : 40; // fixed, so HEAD is arithmetic; the phone legend takes two lines
164
- const mapBlockH = top.length ? captionH + layout.height + 12 : 0;
165
- const HEAD = mapBlockH + LABEL_ROW;
247
+ const NEXT_H = nextUp.length ? 38 : 0;
248
+ const mapH = single ? (target ? layout.height + NEXT_H : 0) : layout.height;
249
+ const mapBlockH = mapH ? captionH + mapH + 12 : 0;
250
+ const titleH = isMobile ? 0 : TITLE_ROW;
251
+ const HEAD = mapBlockH + titleH + LABEL_ROW;
166
252
  const total = HEAD + visible.length * ROW_H + BOTTOM_PAD;
167
253
 
168
254
  // ── Windowing ────────────────────────────────────────────────────────────
@@ -196,8 +282,10 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
196
282
  const changeQuery = (v) => { setQuery(v); toTop(); };
197
283
  const changeTier = (t) => { setTier(t); toTop(); };
198
284
  const changeRarity = (r) => { setRarities((set) => toggle(set, r)); toTop(); };
199
- const clearAll = () => { setQuery(''); setTier('all'); setRarities(new Set()); toTop(); };
200
- const changeSort = (s) => { setSort(s); toTop(); };
285
+ const clearAll = () => { setQuery(''); setTier('all'); setRarities(new Set()); setPath(null); toTop(); };
286
+ const openPath = () => { setPathUsed(true); setPathText(''); setPathOpen(true); };
287
+ const pickPath = (next) => { setPath(next); setPathOpen(false); toTop(); };
288
+ const changeAt = (v) => { setAt(Math.max(0, Math.min(100, Math.round(Number(v) || 0)))); toTop(); };
201
289
  const onSearchKey = (e) => {
202
290
  if (e.key === 'Enter') {
203
291
  // Read the live text, not the deferred copy the list is still catching up to.
@@ -211,7 +299,9 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
211
299
  };
212
300
 
213
301
  const { summary } = model;
214
- const filtered = !!q || tier !== 'all' || rarities.size > 0;
302
+ const filtered = !!q || tier !== 'all' || rarities.size > 0 || !!path;
303
+ const slide = slideLabel(at);
304
+ const slideColor = at < 45 ? RARE : at > 55 ? EASY : '#FFD700';
215
305
  const youLabel = !userName || /^you$/i.test(String(userName).trim()) ? 'You' : initialsFor(userName);
216
306
  const rows = visible.slice(start, end);
217
307
 
@@ -221,7 +311,7 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
221
311
  background: '#0a0a1a', color: '#fff', overflow: 'hidden',
222
312
  }}>
223
313
  {/* ── Header: what this is, how many, and what it can't say ── */}
224
- <div style={{ padding: `10px ${SIDE}px 8px`, flexShrink: 0 }}>
314
+ <div style={{ padding: `${notch ? 48 : 14}px ${SIDE}px 8px`, flexShrink: 0 }}>
225
315
  <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
226
316
  <h2 style={{
227
317
  margin: 0, fontSize: 18, fontWeight: 800,
@@ -261,13 +351,9 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
261
351
  )}
262
352
  </div>
263
353
 
264
- {/* ── Controls: search, order, and their tier ── */}
265
- <div style={{
266
- padding: `0 ${SIDE}px 10px`, flexShrink: 0,
267
- display: 'flex', gap: 8, flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'stretch' : 'center',
268
- flexWrap: isMobile ? 'nowrap' : 'wrap', // with the Sidebar open, the chips take a second line
269
- borderBottom: '1px solid rgba(255,255,255,0.06)',
270
- }}>
354
+ {/* ── Controls: search, then the slider with their tier and rarity ── */}
355
+ <div style={{ padding: `0 ${SIDE}px 10px`, flexShrink: 0, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
356
+ <style>{SLIDER_CSS}</style>
271
357
  <input
272
358
  ref={searchRef}
273
359
  type="search"
@@ -277,62 +363,116 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
277
363
  placeholder="Search people, companies, or who knows them"
278
364
  aria-label="Search people, companies, or who knows them"
279
365
  style={{
280
- flex: isMobile ? 'none' : '1 1 260px', minWidth: 0, height: isMobile ? 44 : 34, boxSizing: 'border-box',
366
+ width: '100%', minWidth: 0, height: isMobile ? 44 : 34, boxSizing: 'border-box',
281
367
  padding: '0 12px', borderRadius: 8, outline: 'none', color: '#fff',
282
368
  fontSize: isMobile ? 16 : 13, // under 16px, iOS zooms the page on focus
283
369
  border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.05)',
284
370
  }}
285
371
  />
286
372
  <div style={{
287
- display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flexWrap: isMobile ? 'nowrap' : 'wrap',
288
- // A phone scrolls this strip sideways, so the page itself never does.
289
- overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin', flexShrink: isMobile ? 0 : 1,
373
+ marginTop: 8, padding: isMobile ? '8px 10px' : '8px 14px', borderRadius: 12, position: 'relative',
374
+ border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.025)',
290
375
  }}>
291
- <div role="group" aria-label="Order" style={{ display: 'flex', flexShrink: 0, background: 'rgba(255,255,255,0.06)', borderRadius: 8, padding: 2 }}>
292
- {[['power', 'Power'], ['ways', 'Ways in']].map(([key, label]) => (
293
- <button key={key} type="button" aria-pressed={sort === key} onClick={() => changeSort(key)} style={{
294
- height: isMobile ? 40 : 30, padding: '0 12px', border: 'none', borderRadius: 6, cursor: 'pointer',
295
- fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
296
- background: sort === key ? 'linear-gradient(135deg, #FFD700, #FF6B35)' : 'transparent',
297
- color: sort === key ? '#000' : '#999',
298
- }}>{label}</button>
299
- ))}
300
- </div>
301
- <span style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5 }}>Their tier</span>
302
- {['all', ...TIERS].map((t) => {
303
- const on = tier === t;
304
- const c = t === 'all' ? '#fff' : tierColors[t] || '#888';
305
- const n = t === 'all' ? summary.people : summary.byTier[t];
306
- return (
307
- <button key={t} type="button" aria-pressed={on} onClick={() => changeTier(t)} style={{
308
- height: isMobile ? 40 : 30, padding: '0 10px', borderRadius: 15, cursor: 'pointer', flexShrink: 0,
309
- fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
310
- border: `1px solid ${on ? c : 'rgba(255,255,255,0.1)'}`,
311
- background: on ? `${c === '#fff' ? '#ffffff' : c}22` : 'rgba(255,255,255,0.03)',
312
- color: on ? c : '#999',
313
- }}>
314
- {t === 'all' ? 'All' : t}
315
- <span style={{ marginLeft: 5, fontWeight: 500, color: on ? c : '#666', opacity: 0.85 }}>{fmt(n)}</span>
316
- </button>
317
- );
318
- })}
319
- <span title={RARITY_NOTE} style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5, marginLeft: 6, cursor: 'help' }}>Rarity</span>
320
- {RARITY.map((r) => {
321
- const on = rarities.has(r.key);
322
- return (
323
- <button key={r.key} type="button" aria-pressed={on} onClick={() => changeRarity(r.key)}
324
- title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}. ${RARITY_NOTE}`} style={{
376
+ {pathOpen && (
377
+ <PathPicker facets={facets} text={pathText} onText={setPathText} isMobile={isMobile}
378
+ onPick={pickPath} onClose={() => setPathOpen(false)} />
379
+ )}
380
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: isMobile ? 'nowrap' : 'wrap', minWidth: 0,
381
+ // A phone scrolls this strip sideways, so the page itself never does.
382
+ overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin' }}>
383
+ <span style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5 }}>Their tier</span>
384
+ {['all', ...TIERS].map((t) => {
385
+ const on = tier === t;
386
+ const c = t === 'all' ? '#fff' : tierColors[t] || '#888';
387
+ const n = t === 'all' ? summary.people : summary.byTier[t];
388
+ return (
389
+ <button key={t} type="button" aria-pressed={on} onClick={() => changeTier(t)} style={{
325
390
  height: isMobile ? 40 : 30, padding: '0 10px', borderRadius: 15, cursor: 'pointer', flexShrink: 0,
326
391
  fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
327
- border: `1px solid ${on ? r.color : 'rgba(255,255,255,0.1)'}`,
328
- background: on ? `${r.color}22` : 'rgba(255,255,255,0.03)',
329
- color: on ? r.color : '#999',
392
+ border: `1px solid ${on ? c : 'rgba(255,255,255,0.1)'}`,
393
+ background: on ? `${c === '#fff' ? '#ffffff' : c}22` : 'rgba(255,255,255,0.03)',
394
+ color: on ? c : '#999',
395
+ }}>
396
+ {t === 'all' ? 'All' : t}
397
+ <span style={{ marginLeft: 5, fontWeight: 500, color: on ? c : '#666', opacity: 0.85 }}>{fmt(n)}</span>
398
+ </button>
399
+ );
400
+ })}
401
+ {/* Path: one sector or one company. The picker opens under here and goes away once you pick. */}
402
+ <span style={{ display: 'inline-flex', flexShrink: 0, height: isMobile ? 40 : 30, borderRadius: 15, overflow: 'hidden',
403
+ border: `1px solid ${path ? PATH : 'rgba(0,255,136,0.35)'}`, background: path ? 'rgba(0,255,136,0.12)' : 'rgba(0,255,136,0.04)' }}>
404
+ <button type="button" aria-haspopup="dialog" aria-expanded={pathOpen} onClick={() => (pathOpen ? setPathOpen(false) : openPath())}
405
+ title="Narrow everyone to one sector or one company" style={{
406
+ padding: path ? '0 6px 0 12px' : '0 12px', border: 'none', background: 'none', cursor: 'pointer',
407
+ fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap', color: PATH, maxWidth: isMobile ? 200 : 300, overflow: 'hidden', textOverflow: 'ellipsis',
330
408
  }}>
331
- {r.label}
332
- <span style={{ marginLeft: 5, fontWeight: 500, color: on ? r.color : '#666', opacity: 0.85 }}>{fmt(byRarity[r.key])}</span>
409
+ {path ? <>Path<span style={{ fontWeight: 500, color: '#cfe' }}> · {path.label}</span></> : 'Path ▾'}
333
410
  </button>
334
- );
335
- })}
411
+ {path && (
412
+ <button type="button" aria-label={`Clear the path: ${path.label}`} onClick={() => pickPath(null)} style={{
413
+ padding: '0 10px 0 4px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 14, color: PATH,
414
+ }}>×</button>
415
+ )}
416
+ </span>
417
+ {/* Where the slider is, in words */}
418
+ <span aria-live="polite" style={{
419
+ marginLeft: 'auto', flexShrink: 0, height: isMobile ? 40 : 30, padding: '0 12px', borderRadius: 15, boxSizing: 'border-box',
420
+ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
421
+ border: `1px solid ${slideColor}66`, background: `${slideColor}14`, color: slideColor,
422
+ }}>
423
+ <span style={{ width: 7, height: 7, borderRadius: '50%', background: slideColor }} />
424
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>{at}</span>
425
+ {slide.name}
426
+ </span>
427
+ </div>
428
+
429
+ {/* The slider: the rarest ways in at one end, the easiest at the other, power alone in the middle */}
430
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginTop: 8, fontSize: 10.5, fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase' }}>
431
+ <span style={{ color: RARE }}>Rare{isMobile ? '' : ' · one way in'}</span>
432
+ <span style={{ color: '#FFD700' }}>Power</span>
433
+ <span style={{ color: EASY }}>Easy{isMobile ? '' : ' · many mutual connections'}</span>
434
+ </div>
435
+ <input
436
+ className="sepslide" type="range" min={0} max={100} step={1} value={at}
437
+ onChange={(e) => changeAt(e.target.value)}
438
+ onDoubleClick={() => changeAt(SLIDER_MIDDLE)}
439
+ aria-label="Order: the rarest ways in, to the easiest"
440
+ aria-valuetext={`${at}: ${slide.name}`}
441
+ title="Drag to reorder everyone. Double-click to go back to the middle."
442
+ />
443
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: -2 }}>
444
+ {[[0, 'Rarest'], [25, ''], [SLIDER_MIDDLE, 'By power'], [75, ''], [100, 'Easiest']].map(([v, name]) => (
445
+ <button key={v} type="button" onClick={() => changeAt(v)} aria-label={`Set the slider to ${v}${name ? `: ${name}` : ''}`} style={{
446
+ padding: '2px 4px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 10,
447
+ color: at === v ? '#fff' : '#667', fontWeight: at === v ? 700 : 500, fontVariantNumeric: 'tabular-nums',
448
+ }}>{v}{name && !isMobile ? ` · ${name}` : ''}</button>
449
+ ))}
450
+ </div>
451
+ <div style={{ marginTop: 4, fontSize: 11.5, lineHeight: 1.45, color: '#bbb' }}>
452
+ <span style={{ color: slideColor, fontWeight: 700 }}>{slide.name}.</span>{' '}{slide.detail}
453
+ {!isMobile && <span style={{ color: '#667' }}>{' '}It only reorders: scores, tiers and ranks stay as they are.</span>}
454
+ </div>
455
+
456
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8, minWidth: 0, flexWrap: isMobile ? 'nowrap' : 'wrap',
457
+ overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin' }}>
458
+ <span title={RARITY_NOTE} style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5, cursor: 'help' }}>Only show</span>
459
+ {RARITY.map((r) => {
460
+ const on = rarities.has(r.key);
461
+ return (
462
+ <button key={r.key} type="button" aria-pressed={on} onClick={() => changeRarity(r.key)}
463
+ title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}. ${RARITY_NOTE}`} style={{
464
+ height: isMobile ? 40 : 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', flexShrink: 0,
465
+ fontSize: 11.5, fontWeight: 700, whiteSpace: 'nowrap',
466
+ border: `1px solid ${on ? r.color : 'rgba(255,255,255,0.1)'}`,
467
+ background: on ? `${r.color}22` : 'rgba(255,255,255,0.03)',
468
+ color: on ? r.color : '#999',
469
+ }}>
470
+ {r.label}
471
+ <span style={{ marginLeft: 5, fontWeight: 500, color: on ? r.color : '#666', opacity: 0.85 }}>{fmt(byRarity[r.key])}</span>
472
+ </button>
473
+ );
474
+ })}
475
+ </div>
336
476
  </div>
337
477
  </div>
338
478
 
@@ -343,15 +483,20 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
343
483
  style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', position: 'relative', padding: `0 ${sidePad}px` }}
344
484
  >
345
485
  <div style={{ position: 'relative', maxWidth: 980, margin: '0 auto', height: total }}>
346
- {top.length > 0 && (
486
+ {mapBlockH > 0 && (
347
487
  <div style={{ height: mapBlockH, boxSizing: 'border-box', paddingTop: 10 }}>
348
488
  <div style={{ height: captionH - 10, overflow: 'hidden' }}>
349
489
  <div style={{ fontSize: 12, fontWeight: 700, color: '#ddd', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
350
- Top {fmt(top.length)} you haven’t asked, of {fmt(visible.length)}{filtered ? ' shown' : ''} · every way in drawn
490
+ {single
491
+ ? <>Aiming at {target.person.name} · {layout.unscanned
492
+ ? `${fmt(target.waysIn + layout.unscanned)} mutual connections: ${fmt(target.waysIn)} drawn, ${fmt(layout.unscanned)} in circles not scanned yet`
493
+ : target.waysIn === 1 ? 'the one connection of yours who leads to them' : `all ${fmt(target.waysIn)} connections of yours who lead to them`}</>
494
+ : <>Top {fmt(top.length)} you haven’t asked, of {fmt(visible.length)}{filtered ? ' shown' : ''}{at !== SLIDER_MIDDLE ? ` · ${slide.name.toLowerCase()}` : ''} · every way in drawn</>}
351
495
  </div>
352
496
  <div style={{ fontSize: 10, color: '#777', marginTop: 2, whiteSpace: isMobile ? 'normal' : 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: 1.35 }}>
353
- {askedShown > 0 && <span style={{ color: '#FFD700' }}>{fmt(askedShown)} asked or connected, so it moved on · </span>}
354
- <span style={{ color: ORANGE }}>solid orange</span> = top-scored bridge · dashed = other routes · dot size = ways in
497
+ {!single && askedShown > 0 && <span style={{ color: '#FFD700' }}>{fmt(askedShown)} asked or connected, so it moved on · </span>}
498
+ <span style={{ color: ORANGE }}>solid orange</span> = top-scored bridge · dashed = other routes
499
+ {single ? ' · pick anyone in the list to aim at them instead' : cards ? ' · a lit pill is someone’s best way in' : ' · dot size = ways in'}
355
500
  </div>
356
501
  </div>
357
502
  {cw > 0 && (
@@ -363,22 +508,63 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
363
508
  isMobile={isMobile}
364
509
  onPick={onPick}
365
510
  onPickBridge={onPickBridge}
511
+ mutualsBy={rarityBy}
512
+ doors={!isMobile && cw >= 640}
513
+ cards={cards}
366
514
  />
367
515
  )}
516
+ {/* Aiming at one person: the next few, to aim at instead */}
517
+ {cw > 0 && single && (
518
+ <>
519
+ {nextUp.length > 0 && (
520
+ <div style={{ height: NEXT_H, display: 'flex', alignItems: 'center', gap: 6, overflow: 'hidden' }}>
521
+ <span style={{ fontSize: 10, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, flexShrink: 0 }}>Next</span>
522
+ {nextUp.map((p) => (
523
+ <button key={p.key} type="button" onClick={() => onPick(p)} title={`Aim at ${p.person.name}`} style={{
524
+ height: 26, padding: '0 10px', borderRadius: 13, cursor: 'pointer', flexShrink: 1, minWidth: 0,
525
+ fontSize: 11, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
526
+ border: `1px solid ${(tierColors[p.tier] || '#888')}55`, background: 'rgba(255,255,255,0.03)', color: '#ccc',
527
+ }}>
528
+ {shortName(p.person.name)}
529
+ <span style={{ marginLeft: 5, color: tierColors[p.tier] || '#888', fontWeight: 700 }}>{p.score.toFixed(1)}</span>
530
+ {/* The count they're ordered by: every mutual, scanned or not */}
531
+ <span style={{ marginLeft: 5, color: '#667', fontWeight: 500 }}>
532
+ {(() => { const n = Math.max(rarityBy.get(p.key)?.count || 0, p.waysIn); return n === 1 ? '1 mutual' : `${fmt(n)} mutuals`; })()}
533
+ </span>
534
+ </button>
535
+ ))}
536
+ </div>
537
+ )}
538
+ </>
539
+ )}
368
540
  </div>
369
541
  )}
370
542
 
543
+ {visible.length > 0 && !isMobile && (
544
+ <div style={{
545
+ height: TITLE_ROW, display: 'flex', alignItems: 'center', gap: 10, padding: '0 12px 0 15px', boxSizing: 'border-box',
546
+ borderTop: '1px solid rgba(255,255,255,0.06)',
547
+ }}>
548
+ <span style={{ fontSize: 15, fontWeight: 800 }}>Ranked by reach</span>
549
+ <span style={{ fontSize: 11, fontWeight: 800, color: '#000', background: '#FFD700', borderRadius: 10, padding: '2px 8px' }}>
550
+ {fmt(visible.length)}{filtered ? ` of ${fmt(summary.people)}` : ''}
551
+ </span>
552
+ <span style={{ marginLeft: 'auto', fontSize: 11, color: '#777' }}>
553
+ order follows the slider · <span style={{ color: slideColor }}>{slide.name.toLowerCase()}</span>
554
+ </span>
555
+ </div>
556
+ )}
371
557
  {visible.length > 0 && (
372
558
  <div style={{
373
559
  height: LABEL_ROW, display: 'grid', alignItems: 'center', gap: 10, padding: '0 12px 0 15px',
374
- gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : '64px 36px minmax(0,1.4fr) minmax(0,1fr) 96px 44px',
560
+ gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : COLUMNS,
375
561
  fontSize: 9, fontWeight: 700, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5,
376
562
  borderBottom: '1px solid rgba(255,255,255,0.08)', boxSizing: 'border-box',
377
563
  }}>
378
564
  <span title="Rank by score, then ways in. = means tied">#</span>
379
565
  <span />
380
- {isMobile ? <span>Person · way in</span> : <><span>Person</span><span>Way in</span><span>Power</span></>}
381
- <span style={{ textAlign: 'right' }}>{isMobile ? 'Score' : ''}</span>
566
+ {isMobile ? <span>Person · way in</span> : <><span>Person</span><span>Way in</span><span title={RARITY_NOTE}>How rare the way in</span><span style={{ textAlign: 'right' }}>Power</span></>}
567
+ {isMobile && <span style={{ textAlign: 'right' }}>Score</span>}
382
568
  </div>
383
569
  )}
384
570
 
@@ -422,6 +608,7 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
422
608
  // whatever the network's size. Hover is pure CSS from the scoped <style>, so a
423
609
  // still mouse can never re-render anything. aria-hidden because the same people
424
610
  // are the first rows of the list, and the list is the accessible path.
611
+ const MOVE = '.38s cubic-bezier(.2,.8,.2,1)';
425
612
  const MAP_CSS = `
426
613
  .sepmap .sp { cursor: pointer; transition: opacity .12s; }
427
614
  .sepmap .ppl:hover .sp { opacity: .28; }
@@ -430,11 +617,34 @@ const MAP_CSS = `
430
617
  .sepmap .br { cursor: pointer; }
431
618
  .sepmap .br:hover text { text-decoration: underline; }
432
619
  .sepmap .rt { pointer-events: none; }
620
+ /* Everything glides to its new place as the slider moves: dots and names by
621
+ their group's transform, lines by their path. Newcomers fade in. */
622
+ .sepmap .mv { transition: transform ${MOVE}; }
623
+ .sepmap .rt, .sepmap .spk { transition: d ${MOVE}, stroke-opacity .12s; }
624
+ .sepmap .in { animation: sepIn .3s ease-out both; }
625
+ @keyframes sepIn { from { opacity: 0; } to { opacity: 1; } }
626
+ @media (prefers-reduced-motion: reduce) {
627
+ .sepmap .mv, .sepmap .rt, .sepmap .spk { transition: none; }
628
+ .sepmap .in { animation: none; }
629
+ }
433
630
  `;
434
631
  const HALO = { paintOrder: 'stroke', stroke: '#0a0a1a', strokeWidth: 3, strokeLinejoin: 'round' };
632
+ const at = (x, y) => ({ transform: `translate(${x}px, ${y}px)` });
633
+
634
+ /**
635
+ * How many mutual connections lead to someone, said the way the data has it:
636
+ * the ones drawn are the connections of yours whose circles you've scanned;
637
+ * LinkedIn's own count, when a scan saved one, can be higher.
638
+ */
639
+ function waysTag(p, mutuals) {
640
+ if (mutuals?.from === 'linkedin' && mutuals.count > p.waysIn) {
641
+ return { text: `${fmt(mutuals.count)} mutuals · ${fmt(p.waysIn)} mapped`, rare: false, more: mutuals.count };
642
+ }
643
+ return p.waysIn === 1 ? { text: 'only way in', rare: true, more: 0 } : { text: `${fmt(p.waysIn)} ways in`, rare: false, more: 0 };
644
+ }
435
645
 
436
- const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, youLabel, isMobile, onPick, onPickBridge }) {
437
- const { width, height, you, people, bridges, links, spokes } = layout;
646
+ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, youLabel, isMobile, onPick, onPickBridge, mutualsBy, doors = false, cards = false }) {
647
+ const { width, height, you, people, bridges, links, spokes, ghosts = [], unscanned = 0, ghostLabel = null, drawn = 1 } = layout;
438
648
  const linksBy = useMemo(() => {
439
649
  const m = new Map();
440
650
  for (const l of links) { if (!m.has(l.key)) m.set(l.key, []); m.get(l.key).push(l); }
@@ -453,42 +663,122 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
453
663
  </linearGradient>
454
664
  </defs>
455
665
 
666
+ <defs>
667
+ <radialGradient id="sepAimHalo"><stop offset="0" stopColor="#FFD700" stopOpacity="0.2" /><stop offset="1" stopColor="#FFD700" stopOpacity="0" /></radialGradient>
668
+ </defs>
456
669
  {spokes.map((s) => (
457
- <line key={`s-${s.id ?? 'unresolved'}`} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="#fff" strokeOpacity={0.12} strokeWidth={1} />
670
+ <path key={`s-${s.id ?? 'unresolved'}`} className="spk in" d={`M${s.x1},${s.y1} L${cards ? s.x2 - PILL_W : s.x2},${s.y2}`} fill="none" stroke="#fff" strokeOpacity={0.12} strokeWidth={1} />
458
671
  ))}
459
672
 
673
+ {/* Not as bright when most of the way in is still unscanned: the glow follows the share drawn. */}
674
+ {cards && people.length === 1 && people[0].big && (
675
+ <circle className="mv" style={{ ...at(people[0].x + Math.min(200, (width - people[0].x) / 2), people[0].y), opacity: 0.3 + 0.7 * drawn }} r={170} fill="url(#sepAimHalo)" />
676
+ )}
677
+
678
+ {/* Mutual connections in circles you haven't scanned: grey, each with a faint line in. */}
679
+ {people.length === 1 && people[0].big && ghosts.length > 0 && (
680
+ <g className="in" pointerEvents="none">
681
+ {ghosts.map((g, i) => {
682
+ const t = people[0];
683
+ const x2 = cards ? t.x : t.x - t.r - 2;
684
+ const mx = (g.x + x2) / 2;
685
+ return <path key={`gl-${i}`} d={`M${g.x},${g.y} C${mx},${g.y} ${mx},${t.y} ${x2},${t.y}`} fill="none" stroke="#8a8fa8" strokeOpacity={0.09} strokeWidth={0.8} />;
686
+ })}
687
+ {ghosts.map((g, i) => <circle key={`gd-${i}`} cx={g.x} cy={g.y} r={3.3} fill="#4a4f68" stroke="#6b7090" strokeWidth={0.6} strokeOpacity={0.6} />)}
688
+ {ghostLabel && (
689
+ <text x={ghostLabel.x} y={ghostLabel.y} fontSize={10.5} fontWeight={700} fill="#8a8fa8" style={HALO}>
690
+ {`+${fmt(unscanned)} more mutual${unscanned === 1 ? '' : 's'}, in circles not scanned yet`}
691
+ {unscanned > ghosts.length ? ` (${fmt(ghosts.length)} shown)` : ''}
692
+ </text>
693
+ )}
694
+ </g>
695
+ )}
460
696
  <g className="ppl">
461
697
  {people.map((pp) => {
462
698
  const { p } = pp;
463
699
  const c = tierColors[p.tier] || '#888';
464
700
  const sel = pp.key === selectedKey;
465
701
  const via = p.routes.map((r) => (r.bridge ? r.bridge.name : CANT_NAME)).join(', ');
702
+ const tag = waysTag(p, mutualsBy?.get(p.key));
703
+ const lx = pp.r + 14;
704
+ const room = Math.max(8, Math.floor((width - pp.x - lx) / 6));
705
+ const clip = (t) => (String(t || '').length > room ? `${String(t).slice(0, room - 1)}…` : String(t || ''));
466
706
  return (
467
707
  <g key={pp.key} className={sel ? 'sp sel' : 'sp'} onClick={() => onPick(p)}>
468
- <title>{`${p.person.name} · #${p.rank}${p.tied ? '=' : ''} · ${p.tier} · ${p.score.toFixed(1)} · via ${via}`}</title>
708
+ <title>{`${p.person.name} · #${p.rank}${p.tied ? '=' : ''} · ${p.tier} · ${p.score.toFixed(1)} · ${tag.text} · via ${via}`}</title>
469
709
  {(linksBy.get(pp.key) || []).map((l) => {
470
710
  const mx = (l.x1 + l.x2) / 2;
471
- const stroke = l.unresolved ? '#777' : l.primary ? ORANGE : tierColors[l.tier] || '#888';
711
+ // Aiming at one person: their other routes are gold; among ten, each bridge's tier colour.
712
+ const stroke = l.unresolved ? '#777' : l.primary ? ORANGE : pp.big ? '#FFD700' : tierColors[l.tier] || '#888';
472
713
  return (
473
714
  <path
474
715
  key={`${l.key}-${l.bridgeId ?? 'u'}`}
475
- className="rt"
476
- d={`M${l.x1 + 5},${l.y1} C${mx},${l.y1} ${mx},${l.y2} ${l.x2 - pp.r},${l.y2}`}
716
+ className="rt in"
717
+ d={cards
718
+ ? `M${l.x1},${l.y1} C${mx},${l.y1} ${mx},${l.y2} ${l.x2},${l.y2}`
719
+ : `M${l.x1 + 5},${l.y1} C${mx},${l.y1} ${mx},${l.y2} ${l.x2 - pp.r - (pp.big ? 2 : 0)},${l.y2}`}
477
720
  fill="none"
478
721
  stroke={stroke}
479
- strokeWidth={l.primary ? 1.5 : 1}
480
- strokeOpacity={l.primary ? 0.8 : 0.35}
481
- strokeDasharray={l.primary ? undefined : '3 3'}
722
+ strokeWidth={pp.big ? (l.primary ? 2.2 : 1.4) : l.primary ? 1.5 : 1}
723
+ strokeOpacity={pp.big ? (l.primary ? 0.95 : 0.6) : l.primary ? 0.8 : 0.35}
724
+ strokeDasharray={l.primary ? undefined : pp.big ? '5 4' : '3 3'}
482
725
  />
483
726
  );
484
727
  })}
485
- <rect x={pp.x - 12} y={pp.y - gap / 2} width={Math.max(0, width - pp.x + 12)} height={gap} fill="transparent" />
486
- {sel && <circle cx={pp.x} cy={pp.y} r={pp.r + 3.5} fill="none" stroke="#fff" strokeWidth={1.5} />}
487
- <circle cx={pp.x} cy={pp.y} r={pp.r} fill={c} />
488
- <text x={pp.x + 14} y={pp.y} dy="0.35em" fontSize={fs} fill="#ddd" style={HALO}>
489
- {pp.label}
490
- <tspan dx={6} fill={c} fontWeight={700}>{p.score.toFixed(1)}</tspan>
491
- </text>
728
+ {/* As cards, each line plugs into both boxes: a small dot at each end */}
729
+ {cards && (linksBy.get(pp.key) || []).map((l) => {
730
+ const stroke = l.unresolved ? '#777' : l.primary ? ORANGE : pp.big ? '#FFD700' : tierColors[l.tier] || '#888';
731
+ return (
732
+ <g key={`e-${l.bridgeId ?? 'u'}`} className="in" pointerEvents="none">
733
+ <circle className="mv" style={at(l.x1, l.y1)} r={2.6} fill={stroke} />
734
+ <circle className="mv" style={at(l.x2, l.y2)} r={2.6} fill={stroke} />
735
+ </g>
736
+ );
737
+ })}
738
+ <g className="mv in" style={at(pp.x, pp.y)}>
739
+ {(pp.rings || []).map((r, i) => (
740
+ <circle key={r} r={r} fill="none" stroke={c} strokeOpacity={0.28 - i * 0.08} strokeWidth={1} strokeDasharray={i ? '2 5' : undefined} />
741
+ ))}
742
+ {!cards && <rect x={-12} y={-gap / 2} width={Math.max(0, width - pp.x + 12)} height={gap} fill="transparent" />}
743
+ {cards && pp.big ? (
744
+ <BigCard p={p} c={c} tag={tag} width={Math.max(260, Math.min(400, width - pp.x - 4))} fs={fs} />
745
+ ) : cards ? (
746
+ <SmallCard p={p} c={c} tag={tag} width={Math.max(240, Math.min(340, width - pp.x - 4))} sel={sel} />
747
+ ) : pp.big ? (
748
+ <>
749
+ <circle r={pp.r} fill="#0a0a1a" stroke={c} strokeWidth={3} strokeOpacity={0.35 + 0.65 * drawn} />
750
+ <text dy="0.35em" textAnchor="middle" fontSize={11} fontWeight={800} fill={c}>{initialsFor(p.person.name)}</text>
751
+ <text x={lx} y={-20} fontSize={fs + 3} fontWeight={800} fill="#fff" style={HALO}>{clip(p.person.name)}</text>
752
+ <text x={lx} y={-4} fontSize={fs} fill="#aaa" style={HALO}>{clip(subline(p.person))}</text>
753
+ <text x={lx} y={13} fontSize={fs} fontWeight={700} fill={c} style={HALO}>
754
+ #{p.rank}{p.tied ? '=' : ''} · {p.tier}-tier · {p.score.toFixed(1)}
755
+ </text>
756
+ <text x={lx} y={29} fontSize={fs} fill="#ccc" style={HALO}>
757
+ {clip(tag.more
758
+ ? `${fmt(tag.more)} mutual connections · ${fmt(p.waysIn)} drawn here`
759
+ : p.waysIn === 1 ? '1 mutual connection: the only way in' : `${fmt(p.waysIn)} mutual connections, all drawn`)}
760
+ </text>
761
+ {tag.more > 0 && (
762
+ <text x={lx} y={44} fontSize={fs - 1} fill="#778" style={HALO}>
763
+ {clip('The count is LinkedIn’s; the rest are in circles not scanned yet')}
764
+ </text>
765
+ )}
766
+ </>
767
+ ) : (
768
+ <>
769
+ {sel && <circle r={pp.r + 3.5} fill="none" stroke="#fff" strokeWidth={1.5} />}
770
+ <circle r={pp.r} fill={c} />
771
+ <text x={14} dy="0.35em" fontSize={fs} fill="#ddd" style={HALO}>
772
+ {pp.label}
773
+ <tspan dx={6} fill={c} fontWeight={700}>{p.score.toFixed(1)}</tspan>
774
+ {/* How many mutual connections lead to them: one is the rare kind */}
775
+ {doors && (
776
+ <tspan dx={8} fontSize={fs - 2} fontWeight={700} fill={tag.rare ? RARE : '#778'}>{tag.text}</tspan>
777
+ )}
778
+ </text>
779
+ </>
780
+ )}
781
+ </g>
492
782
  </g>
493
783
  );
494
784
  })}
@@ -504,25 +794,29 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
504
794
  >
505
795
  <title>{b.unresolved
506
796
  ? 'Routes whose connection record doesn’t match anyone in your 1st-degree list, so who introduces you can’t be named'
507
- : `${b.bridge.name} · ${b.bridge.tier}-tier · a way in to ${b.count} of these · click to open`}</title>
508
- <circle
509
- cx={b.x} cy={b.y} r={b.r}
510
- fill={b.unresolved ? 'none' : c}
511
- stroke={b.unresolved ? '#777' : '#0a0a1a'}
512
- strokeWidth={b.unresolved ? 1 : 1.5}
513
- strokeDasharray={b.unresolved ? '2 2' : undefined}
514
- />
515
- <text x={b.x - 9} y={b.y} dy="0.35em" textAnchor="end" fontSize={fs - 1} fill={c}
516
- fontStyle={b.unresolved ? 'italic' : undefined} fontWeight={b.unresolved ? 400 : 600} style={HALO}>
517
- {b.label}
518
- </text>
797
+ : `${b.bridge.name} · ${b.bridge.tier}-tier${b.count > 1 ? ` · a way in to ${b.count} of these` : ''} · click to open`}</title>
798
+ <g className="mv in" style={at(b.x, b.y)}>
799
+ {cards ? <Pill b={b} c={c} big={people.length === 1 && people[0].big} /> : (<>
800
+ <circle
801
+ r={b.r}
802
+ fill={b.unresolved ? 'none' : c}
803
+ stroke={b.unresolved ? '#777' : '#0a0a1a'}
804
+ strokeWidth={b.unresolved ? 1 : 1.5}
805
+ strokeDasharray={b.unresolved ? '2 2' : undefined}
806
+ />
807
+ <text x={-9} dy="0.35em" textAnchor="end" fontSize={fs - 1} fill={c}
808
+ fontStyle={b.unresolved ? 'italic' : undefined} fontWeight={b.unresolved ? 400 : b.primary ? 800 : 600} style={HALO}>
809
+ {b.label}
810
+ </text>
811
+ </>)}
812
+ </g>
519
813
  </g>
520
814
  );
521
815
  })}
522
816
 
523
- <g>
524
- <circle cx={you.x} cy={you.y} r={you.r} fill="url(#sepYouGrad)" />
525
- <text x={you.x} y={you.y} dy="0.35em" textAnchor="middle" fontSize={youLabel.length > 2 ? 9 : 10} fontWeight={800} fill="#000">
817
+ <g className="mv" style={at(you.x, you.y)}>
818
+ <circle r={you.r} fill="url(#sepYouGrad)" />
819
+ <text dy="0.35em" textAnchor="middle" fontSize={youLabel.length > 2 ? 9 : 10} fontWeight={800} fill="#000">
526
820
  {youLabel}
527
821
  </text>
528
822
  </g>
@@ -530,11 +824,102 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
530
824
  );
531
825
  });
532
826
 
827
+ // ── Cards (Blake, 2026-10-02: mock-ups 1 and 1b) ───────────────────────────
828
+ // Connections as pills in the middle, people as cards on the right; at the
829
+ // easy end, the one person as a big card. Plain SVG, placed by the same
830
+ // gliding groups as the dots, so the slider still moves everything smoothly.
831
+ const PILL_W = 236;
832
+ const clipText = (t, n) => (String(t || '').length > n ? `${String(t).slice(0, Math.max(1, n - 1))}…` : String(t || ''));
833
+ const chipW = (text, size = 9.5) => Math.round(String(text).length * size * 0.6 + 14);
834
+
835
+ /** A connection: their initials, name, tier and what they are to the people on the map. */
836
+ function Pill({ b, c, big }) {
837
+ if (b.unresolved) {
838
+ return (
839
+ <g>
840
+ <rect x={-PILL_W} y={-14} width={PILL_W} height={28} rx={14} fill="#11132a" stroke="#555" strokeDasharray="3 3" />
841
+ <text x={-PILL_W + 14} dy="0.35em" fontSize={11.5} fontStyle="italic" fill="#888">{clipText(b.label, 30)}</text>
842
+ </g>
843
+ );
844
+ }
845
+ const tier = b.bridge.tier;
846
+ const tag = big
847
+ ? (b.primary ? `${tier} · best way in` : tier)
848
+ : b.onlyFor ? `${tier} · only door${b.onlyFor > 1 ? ` ×${b.onlyFor}` : ''}`
849
+ : b.bestFor ? `${tier} · door for ${b.bestFor}` : tier;
850
+ const hot = big ? b.primary : b.bestFor > 0;
851
+ const tw = chipW(tag);
852
+ return (
853
+ <g>
854
+ <rect x={-PILL_W} y={-14} width={PILL_W} height={28} rx={14} fill="#14172e"
855
+ stroke={hot ? 'rgba(255,215,0,0.55)' : 'rgba(255,255,255,0.09)'} strokeWidth={hot ? 1.3 : 1} />
856
+ <circle cx={-PILL_W + 15} r={10} fill={c} />
857
+ <text x={-PILL_W + 15} dy="0.35em" textAnchor="middle" fontSize={8} fontWeight={800} fill="#000">{initialsFor(b.bridge.name)}</text>
858
+ <text x={-PILL_W + 31} dy="0.35em" fontSize={12} fontWeight={600} fill="#e8e8f0">
859
+ {clipText(b.bridge.name, Math.max(6, Math.floor((PILL_W - 31 - tw - 10) / 6.4)))}
860
+ </text>
861
+ <rect x={-tw - 7} y={-9} width={tw} height={18} rx={9} fill={`${c}22`} />
862
+ <text x={-7 - tw / 2} dy="0.35em" textAnchor="middle" fontSize={9.5} fontWeight={800} fill={c}>{tag}</text>
863
+ </g>
864
+ );
865
+ }
866
+
867
+ /** Someone on the map: rank, name, role, score, and how many ways in. */
868
+ function SmallCard({ p, c, tag, width, sel }) {
869
+ const tw = chipW(tag.text);
870
+ return (
871
+ <g>
872
+ <rect x={0} y={-20} width={width} height={40} rx={10} fill="#151830" stroke={sel ? '#fff' : 'rgba(255,255,255,0.09)'} strokeWidth={sel ? 1.5 : 1} />
873
+ <text x={12} dy="0.35em" fontSize={11} fontWeight={800} fill="#FFD700">#{p.rank}{p.tied ? '=' : ''}</text>
874
+ <text x={60} y={-3} fontSize={13} fontWeight={700} fill="#fff">{clipText(p.person.name, Math.floor((width - 60 - 70) / 7))}</text>
875
+ <text x={60} y={12} fontSize={10.5} fill="#8a8fa8">{clipText(subline(p.person), Math.floor((width - 60 - tw - 20) / 5.6))}</text>
876
+ <text x={width - 12} y={-2} textAnchor="end" fontSize={14} fontWeight={800} fill={c}>{p.score.toFixed(1)}</text>
877
+ <rect x={width - 12 - tw} y={4} width={tw} height={14} rx={7}
878
+ fill={tag.rare ? 'rgba(0,229,255,0.1)' : 'rgba(255,255,255,0.06)'} stroke={tag.rare ? 'rgba(0,229,255,0.35)' : 'rgba(255,255,255,0.14)'} />
879
+ <text x={width - 12 - tw / 2} y={11} dy="0.35em" textAnchor="middle" fontSize={9.5} fontWeight={800} fill={tag.rare ? RARE : '#cfd3e6'}>{tag.text}</text>
880
+ </g>
881
+ );
882
+ }
883
+
884
+ /** The one person you're aiming at: a large card with both numbers that matter. */
885
+ function BigCard({ p, c, tag, width, fs }) {
886
+ const named = p.routes.filter((r) => r.bridge).length;
887
+ const first = tag.more ? `${fmt(tag.more)} mutual connections` : p.waysIn === 1 ? '1 mutual connection' : `${fmt(p.waysIn)} mutual connections`;
888
+ const second = tag.more ? `${fmt(p.waysIn)} drawn here` : p.waysIn === 1 ? 'the only way in' : 'all drawn here';
889
+ const w1 = chipW(first, 10.5);
890
+ const w2 = chipW(second, 10.5);
891
+ const tx = 96;
892
+ const room = Math.floor((width - tx - 14) / 6.6);
893
+ return (
894
+ <g>
895
+ <rect x={0} y={-76} width={width} height={152} rx={16} fill="#151830" stroke="rgba(255,215,0,0.45)" />
896
+ <circle cx={48} r={32} fill="#0a0a1a" stroke={c} strokeWidth={3} />
897
+ <text x={48} dy="0.35em" textAnchor="middle" fontSize={18} fontWeight={800} fill={c}>{initialsFor(p.person.name)}</text>
898
+ <text x={tx} y={-40} fontSize={11} fontWeight={800} fill="#FFD700">#{p.rank}{p.tied ? '=' : ''} · {p.tier}-tier · {p.score.toFixed(1)}</text>
899
+ <text x={tx} y={-17} fontSize={fs + 8} fontWeight={800} fill="#fff">{clipText(p.person.name, Math.floor(room * 0.72))}</text>
900
+ <text x={tx} y={1} fontSize={12} fill="#8a8fa8">{clipText(subline(p.person), room)}</text>
901
+ <rect x={tx} y={12} width={w1} height={20} rx={10} fill="rgba(255,112,67,0.12)" stroke="rgba(255,112,67,0.4)" />
902
+ <text x={tx + w1 / 2} y={22} dy="0.35em" textAnchor="middle" fontSize={10.5} fontWeight={800} fill={EASY}>{first}</text>
903
+ <rect x={tx + w1 + 6} y={12} width={w2} height={20} rx={10} fill="rgba(255,255,255,0.06)" stroke="rgba(255,255,255,0.14)" />
904
+ <text x={tx + w1 + 6 + w2 / 2} y={22} dy="0.35em" textAnchor="middle" fontSize={10.5} fontWeight={800} fill="#cfd3e6">{second}</text>
905
+ <text x={tx} y={50} fontSize={10.5} fill="#6b7090">
906
+ {tag.more ? `The ${fmt(tag.more)} is LinkedIn’s count. The other ${fmt(tag.more - p.waysIn)}` : named === 1 ? 'Only one of your connections knows them.' : `${fmt(named)} of your connections know them.`}
907
+ </text>
908
+ {tag.more > 0 && (
909
+ <text x={tx} y={64} fontSize={10.5} fill="#6b7090">
910
+ {clipText('are in circles not scanned yet: the grey dots.', Math.floor((width - tx - 14) / 5.4))}
911
+ </text>
912
+ )}
913
+ </g>
914
+ );
915
+ }
916
+
533
917
  // ── Rows ───────────────────────────────────────────────────────────────────
534
918
 
535
919
  const ellipsis = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
536
920
 
537
921
  function Via({ p, route, tierColors, compact }) {
922
+ if (!compact) return <WayIn p={p} route={route} tierColors={tierColors} />;
538
923
  if (!route?.bridge) {
539
924
  return <span style={{ ...ellipsis, color: '#777', fontStyle: 'italic', fontSize: compact ? 11 : 12 }}>via {CANT_NAME}</span>;
540
925
  }
@@ -555,6 +940,44 @@ function Via({ p, route, tierColors, compact }) {
555
940
  );
556
941
  }
557
942
 
943
+ /** The ledger's way in: the connection's face and name, and under it "only way in" or how many more. */
944
+ function WayIn({ p, route, tierColors }) {
945
+ const b = route?.bridge;
946
+ const extra = p.waysIn - 1;
947
+ return (
948
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }} title={`Every way in:\n${everyRoute(p)}`}>
949
+ {b ? <Avatar person={b} size={26} tierColors={tierColors} /> : <span style={{ width: 26, height: 26, borderRadius: '50%', border: '1px dashed #666', flexShrink: 0 }} />}
950
+ <span style={{ minWidth: 0, display: 'flex', flexDirection: 'column' }}>
951
+ <span style={{ ...ellipsis, fontSize: 13, fontWeight: 700, color: b ? tierColors[b.tier] || '#ddd' : '#777', fontStyle: b ? 'normal' : 'italic' }}>
952
+ {b ? b.name : CANT_NAME}
953
+ </span>
954
+ <span style={{ ...ellipsis, fontSize: 10.5, marginTop: 1, color: extra > 0 ? '#99a' : RARE, fontWeight: extra > 0 ? 500 : 700 }}>
955
+ {extra > 0 ? `+${extra} more way${extra > 1 ? 's' : ''} in` : 'only way in'}
956
+ </span>
957
+ </span>
958
+ </span>
959
+ );
960
+ }
961
+
962
+ /** How rare the way in is, as a bar: full for one door, nearly empty for the warmest. */
963
+ function RarityBar({ p, rarity }) {
964
+ const info = rarity ? rarityInfo(rarity.key) : null;
965
+ if (!info) return <span />;
966
+ const fill = Math.max(0.06, 1 - easeOf(rarity.count));
967
+ const mapped = rarity.from === 'linkedin' && rarity.count > p.waysIn ? `, ${fmt(p.waysIn)} mapped` : '';
968
+ return (
969
+ <span style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}
970
+ title={`${rarity.count} mutual connection${rarity.count === 1 ? '' : 's'}${rarity.from === 'scans' ? ' (from your scans, so it can only go up)' : ' (LinkedIn’s count)'}. ${RARITY_NOTE}`}>
971
+ <span style={{ height: 6, borderRadius: 3, background: 'rgba(255,255,255,0.07)', overflow: 'hidden' }}>
972
+ <span style={{ display: 'block', height: '100%', width: `${fill * 100}%`, borderRadius: 3, background: info.color }} />
973
+ </span>
974
+ <span style={{ ...ellipsis, fontSize: 10.5, fontWeight: 700, color: info.color }}>
975
+ {info.label} · {fmt(rarity.count)} mutual{rarity.count === 1 ? '' : 's'}{mapped}
976
+ </span>
977
+ </span>
978
+ );
979
+ }
980
+
558
981
  /** "Request sent" or "Connected", and how rare the way in is. */
559
982
  function Tags({ status, rarity }) {
560
983
  const info = rarity ? rarityInfo(rarity.key) : null;
@@ -579,6 +1002,84 @@ function Tags({ status, rarity }) {
579
1002
  );
580
1003
  }
581
1004
 
1005
+ // ── Path: pick one sector or one company ───────────────────────────────────
1006
+ // Opens under the Path button, over the slider, and closes on a pick, on Esc
1007
+ // or on a click anywhere else. Sectors are inferred (lib/companies.js), so it says so.
1008
+ function PathPicker({ facets, text, onText, onPick, onClose, isMobile }) {
1009
+ const needle = text.trim().toLowerCase();
1010
+ const sectors = (facets?.sectors || []).filter((i) => !needle || i.label.toLowerCase().includes(needle));
1011
+ const companies = (facets?.companies || []).filter((c) => !needle || c.name.toLowerCase().includes(needle)).slice(0, needle ? 60 : 24);
1012
+ const onKey = (e) => {
1013
+ if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
1014
+ else if (e.key === 'Enter') {
1015
+ if (sectors.length === 1 && !companies.length) onPick({ kind: 'sector', key: sectors[0].key, label: sectors[0].label, color: sectors[0].color });
1016
+ else if (companies.length === 1 && !sectors.length) onPick({ kind: 'company', key: companies[0].name, label: companies[0].name });
1017
+ }
1018
+ };
1019
+ const sub = { fontSize: 9, fontWeight: 700, color: '#667', letterSpacing: 1, textTransform: 'uppercase', margin: '10px 0 6px' };
1020
+ return (
1021
+ <>
1022
+ <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
1023
+ <div role="dialog" aria-label="Pick a sector or a company" style={{
1024
+ position: 'absolute', zIndex: 41, top: isMobile ? 52 : 44, left: isMobile ? 6 : 14, width: isMobile ? 'calc(100% - 12px)' : 'min(560px, calc(100% - 28px))',
1025
+ maxHeight: 340, overflowY: 'auto', boxSizing: 'border-box', padding: 12, borderRadius: 12,
1026
+ background: '#0c101e', border: `1px solid ${PATH}55`, boxShadow: '0 12px 40px rgba(0,0,0,0.6)',
1027
+ }}>
1028
+ <input
1029
+ autoFocus type="search" value={text} onChange={(e) => onText(e.target.value)} onKeyDown={onKey}
1030
+ placeholder="Type a sector or a company" aria-label="Type a sector or a company"
1031
+ style={{
1032
+ width: '100%', height: isMobile ? 44 : 34, boxSizing: 'border-box', padding: '0 12px', borderRadius: 8, outline: 'none',
1033
+ color: '#fff', fontSize: isMobile ? 16 : 13, border: '1px solid rgba(255,255,255,0.14)', background: 'rgba(255,255,255,0.06)',
1034
+ }}
1035
+ />
1036
+ {sectors.length > 0 && <div style={sub}>Sectors · inferred from company names and headlines</div>}
1037
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
1038
+ {sectors.map((i) => (
1039
+ <button key={i.key} type="button" onClick={() => onPick({ kind: 'sector', key: i.key, label: i.label, color: i.color })} style={{
1040
+ height: 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', fontSize: 11.5, fontWeight: 700, whiteSpace: 'nowrap',
1041
+ border: `1px solid ${i.color}66`, background: `${i.color}14`, color: '#ddd', display: 'inline-flex', alignItems: 'center', gap: 6,
1042
+ }}>
1043
+ <span style={{ width: 7, height: 7, borderRadius: '50%', background: i.color }} />
1044
+ {i.label}
1045
+ <span style={{ fontWeight: 500, color: '#889' }}>{fmt(i.count)}</span>
1046
+ </button>
1047
+ ))}
1048
+ </div>
1049
+ {companies.length > 0 && <div style={sub}>Companies{needle ? '' : ' · the biggest; type to find any'}</div>}
1050
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
1051
+ {companies.map((c) => (
1052
+ <button key={c.name} type="button" onClick={() => onPick({ kind: 'company', key: c.name, label: c.name })} style={{
1053
+ height: 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', fontSize: 11.5, fontWeight: 600, whiteSpace: 'nowrap',
1054
+ border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.04)', color: '#ccc',
1055
+ }}>
1056
+ {c.name}
1057
+ <span style={{ marginLeft: 6, fontWeight: 500, color: '#889' }}>{fmt(c.count)}</span>
1058
+ </button>
1059
+ ))}
1060
+ </div>
1061
+ {!sectors.length && !companies.length && (
1062
+ <div style={{ fontSize: 12, color: '#888', padding: '12px 2px 4px' }}>
1063
+ {facets ? `Nothing matches ‘${text.trim()}’.` : 'Reading where everyone works…'}
1064
+ </div>
1065
+ )}
1066
+ </div>
1067
+ </>
1068
+ );
1069
+ }
1070
+
1071
+ const PATH = '#00ff88';
1072
+ // The slider's two ends, in the colours rarity already uses for them.
1073
+ const RARE = '#00E5FF';
1074
+ const EASY = '#FF7043';
1075
+ const SLIDER_CSS = `
1076
+ .sepslide { -webkit-appearance: none; appearance: none; display: block; width: 100%; height: 24px; margin: 4px 0 0; background: transparent; cursor: pointer; }
1077
+ .sepslide::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, ${RARE} 0%, #FFD700 50%, ${EASY} 100%); }
1078
+ .sepslide::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; margin-top: -7px; border-radius: 50%; background: #0a0a1a; border: 3px solid #fff; box-shadow: 0 0 10px rgba(255,255,255,0.45); }
1079
+ .sepslide::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, ${RARE} 0%, #FFD700 50%, ${EASY} 100%); }
1080
+ .sepslide::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: #0a0a1a; border: 3px solid #fff; box-shadow: 0 0 10px rgba(255,255,255,0.45); }
1081
+ .sepslide:focus-visible { outline: 2px solid rgba(255,255,255,0.7); outline-offset: 3px; border-radius: 6px; }
1082
+ `;
582
1083
  const RARITY_NOTE = 'Rarity is how many mutual connections lead to them: few is a rare way in, many is warm (likely to accept). It never changes a score or a tier.';
583
1084
 
584
1085
  const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors, onPick, q, status, rarity }) {
@@ -607,7 +1108,7 @@ const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors,
607
1108
  style={{
608
1109
  position: 'absolute', top, left: 0, right: 0, height, boxSizing: 'border-box',
609
1110
  display: 'grid', alignItems: 'center', gap: 10, padding: '0 12px',
610
- gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : '64px 36px minmax(0,1.4fr) minmax(0,1fr) 96px 44px',
1111
+ gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : COLUMNS,
611
1112
  borderLeft: `3px solid ${selected ? ORANGE : 'transparent'}`,
612
1113
  borderBottom: '1px solid rgba(255,255,255,0.04)',
613
1114
  background: base, cursor: 'pointer',
@@ -633,22 +1134,29 @@ const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors,
633
1134
  <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
634
1135
  <span style={{ ...ellipsis, fontSize: 13, fontWeight: 600, color: member ? '#888' : '#fff' }}>{person.name}</span>
635
1136
  <span style={{ flexShrink: 0, fontSize: 9, fontWeight: 800, color: c, border: `1px solid ${c}55`, borderRadius: 4, padding: '0 4px' }}>{p.tier}</span>
636
- <Tags status={status} rarity={rarity} />
1137
+ {/* The rarity has its own column here */}
1138
+ <Tags status={status} rarity={null} />
637
1139
  {member && <span style={{ flexShrink: 0, fontSize: 10, color: '#666' }}>out of network</span>}
638
1140
  </div>
639
1141
  <div style={{ ...ellipsis, fontSize: 10, color: '#777', marginTop: 2 }}>{subline(person)}</div>
640
1142
  </div>
641
1143
  <Via p={p} route={route} tierColors={tierColors} />
642
- <div style={{ height: 6, background: 'rgba(255,255,255,0.06)', borderRadius: 3 }}>
643
- <div style={{ height: '100%', borderRadius: 3, background: c, width: `${Math.max(0, Math.min(1, p.score / 10)) * 100}%` }} />
1144
+ <RarityBar p={p} rarity={rarity} />
1145
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1146
+ <div style={{ flex: 1, height: 6, background: 'rgba(255,255,255,0.06)', borderRadius: 3 }}>
1147
+ <div style={{ height: '100%', borderRadius: 3, background: c, width: `${Math.max(0, Math.min(1, p.score / 10)) * 100}%` }} />
1148
+ </div>
1149
+ <span style={{ fontSize: 15, fontWeight: 800, color: c, width: 30, textAlign: 'right' }}>{p.score.toFixed(1)}</span>
644
1150
  </div>
645
1151
  </>
646
1152
  )}
647
1153
 
648
- <div style={{ textAlign: 'right' }}>
649
- <div style={{ fontSize: 13, fontWeight: 800, color: c }}>{p.score.toFixed(1)}</div>
650
- {isMobile && <div style={{ fontSize: 9, color: '#666', fontWeight: 700 }}>{p.tier}</div>}
651
- </div>
1154
+ {isMobile && (
1155
+ <div style={{ textAlign: 'right' }}>
1156
+ <div style={{ fontSize: 13, fontWeight: 800, color: c }}>{p.score.toFixed(1)}</div>
1157
+ <div style={{ fontSize: 9, color: '#666', fontWeight: 700 }}>{p.tier}</div>
1158
+ </div>
1159
+ )}
652
1160
  </div>
653
1161
  );
654
1162
  });