six-degrees 0.3.0 → 0.4.0-beta.1

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 (299) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +34 -6
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.rsc +10 -10
  17. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
  18. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  19. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +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.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  37. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +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.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/import.html +1 -1
  52. package/.next/standalone/.next/server/app/import.rsc +12 -12
  53. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +12 -12
  54. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  55. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  56. package/.next/standalone/.next/server/app/index.html +1 -1
  57. package/.next/standalone/.next/server/app/index.rsc +12 -12
  58. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  59. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +12 -12
  60. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/launch.html +2 -2
  64. package/.next/standalone/.next/server/app/launch.rsc +12 -12
  65. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +12 -12
  66. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  67. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  68. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/paths.html +1 -1
  73. package/.next/standalone/.next/server/app/paths.rsc +12 -12
  74. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  77. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/profile.html +1 -1
  80. package/.next/standalone/.next/server/app/profile.rsc +12 -12
  81. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +12 -12
  82. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  83. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  84. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/queue.html +1 -1
  87. package/.next/standalone/.next/server/app/queue.rsc +12 -12
  88. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +12 -12
  89. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  90. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  91. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  92. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  93. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  94. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  95. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  96. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  97. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  98. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  99. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  100. package/.next/standalone/.next/server/app/scores.html +1 -0
  101. package/.next/standalone/.next/server/app/scores.meta +26 -0
  102. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  103. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  104. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  105. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  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 +12 -12
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +12 -12
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +11 -11
  113. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/setup.html +1 -1
  116. package/.next/standalone/.next/server/app/setup.rsc +12 -12
  117. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +12 -12
  118. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  119. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  120. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  121. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rc27za._.js +8 -0
  122. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  124. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  127. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +3 -2
  130. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  131. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  134. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/_0cu6-an._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  144. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  145. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
  146. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  147. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  148. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  149. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  150. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  151. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  153. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  154. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  155. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  156. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  157. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  158. package/.next/standalone/.next/server/pages/404.html +1 -1
  159. package/.next/standalone/.next/server/pages/500.html +1 -1
  160. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  161. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  162. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  163. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +16 -0
  164. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +1 -0
  165. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +1 -0
  166. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +1 -0
  167. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
  168. package/.next/{static/chunks/1chp3nx45meg5.js → standalone/.next/static/chunks/1roznk7hkjes1.js} +1 -1
  169. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +33 -0
  170. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +2 -0
  171. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +1 -0
  172. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
  173. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +2 -0
  174. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
  175. package/.next/standalone/CHANGELOG.md +134 -0
  176. package/.next/standalone/README.md +30 -15
  177. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  178. package/.next/standalone/app/api/company-scores/route.js +1 -1
  179. package/.next/standalone/app/api/ingest/route.js +17 -1
  180. package/.next/standalone/app/api/outreach/route.js +107 -44
  181. package/.next/standalone/app/api/scraper/route.js +69 -8
  182. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  183. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  184. package/.next/standalone/app/components/CompanyScores.js +16 -10
  185. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  186. package/.next/standalone/app/components/OrbitGraph.js +30 -0
  187. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  188. package/.next/standalone/app/components/SeparationView.js +96 -14
  189. package/.next/standalone/app/components/Sidebar.js +258 -239
  190. package/.next/standalone/app/components/TheirCircle.js +293 -0
  191. package/.next/standalone/app/components/settings/SectorSection.js +5 -5
  192. package/.next/standalone/app/components/settings/TierSection.js +104 -0
  193. package/.next/standalone/app/components/useRequests.js +12 -0
  194. package/.next/standalone/app/components/useScanner.js +13 -0
  195. package/.next/standalone/app/components/views.js +1 -3
  196. package/.next/standalone/app/launch/page.js +4 -1
  197. package/.next/standalone/app/page.js +55 -51
  198. package/.next/standalone/app/paths/page.js +17 -12
  199. package/.next/standalone/app/profile/page.js +3 -3
  200. package/.next/standalone/app/queue/page.js +38 -50
  201. package/.next/standalone/app/scores/page.js +72 -0
  202. package/.next/standalone/app/settings/page.js +15 -3
  203. package/.next/standalone/db/schema.js +1 -0
  204. package/.next/standalone/lib/circle.js +169 -0
  205. package/.next/standalone/lib/companies.js +1 -1
  206. package/.next/standalone/lib/csv.js +17 -4
  207. package/.next/standalone/lib/db-client.js +1 -0
  208. package/.next/standalone/lib/ingest.js +12 -0
  209. package/.next/standalone/lib/legacy-offer.js +7 -6
  210. package/.next/standalone/lib/legacy-scores.js +2 -2
  211. package/.next/standalone/lib/rarity.js +80 -0
  212. package/.next/standalone/lib/requests-client.js +113 -0
  213. package/.next/standalone/lib/rpc.js +26 -15
  214. package/.next/standalone/lib/score-guess.js +60 -0
  215. package/.next/standalone/lib/scoring.js +1002 -61
  216. package/.next/standalone/lib/scraper-client.js +208 -17
  217. package/.next/standalone/lib/sector-directory.js +1 -1
  218. package/.next/standalone/lib/sector-focus.js +4 -3
  219. package/.next/standalone/lib/settings-effects.js +19 -1
  220. package/.next/standalone/lib/settings.js +4 -1
  221. package/.next/standalone/package-lock.json +2 -2
  222. package/.next/standalone/package.json +2 -2
  223. package/.next/standalone/public/demo-data.json +1 -1
  224. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  225. package/.next/standalone/scripts/scrape.py +62 -0
  226. package/.next/static/chunks/0_yurn4q91vq8.js +16 -0
  227. package/.next/static/chunks/0d_mh-06srh2k.js +1 -0
  228. package/.next/static/chunks/0x3oadfikvb1c.js +1 -0
  229. package/.next/static/chunks/1d5hl1io798n2.js +1 -0
  230. package/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
  231. package/.next/{standalone/.next/static/chunks/1chp3nx45meg5.js → static/chunks/1roznk7hkjes1.js} +1 -1
  232. package/.next/static/chunks/2_bf2n4gstad9.js +33 -0
  233. package/.next/static/chunks/36gbwek52x_w-.js +2 -0
  234. package/.next/static/chunks/3a55suosthntc.js +1 -0
  235. package/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
  236. package/.next/static/chunks/3nbnio971zlji.js +2 -0
  237. package/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
  238. package/CHANGELOG.md +134 -0
  239. package/README.md +30 -15
  240. package/db/schema.js +1 -0
  241. package/lib/circle.js +169 -0
  242. package/lib/companies.js +1 -1
  243. package/lib/csv.js +17 -4
  244. package/lib/db-client.js +1 -0
  245. package/lib/ingest.js +12 -0
  246. package/lib/legacy-offer.js +7 -6
  247. package/lib/legacy-scores.js +2 -2
  248. package/lib/rarity.js +80 -0
  249. package/lib/requests-client.js +113 -0
  250. package/lib/rpc.js +26 -15
  251. package/lib/score-guess.js +60 -0
  252. package/lib/scoring.js +1002 -61
  253. package/lib/scraper-client.js +208 -17
  254. package/lib/sector-directory.js +1 -1
  255. package/lib/sector-focus.js +4 -3
  256. package/lib/settings-effects.js +19 -1
  257. package/lib/settings.js +4 -1
  258. package/package.json +2 -2
  259. package/public/demo-data.json +1 -1
  260. package/scripts/scrape.py +62 -0
  261. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  262. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  263. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  264. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  265. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  266. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  267. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  268. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  269. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  270. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  271. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  272. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  273. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  274. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  275. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  276. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  277. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  278. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  279. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  280. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  281. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  282. package/.next/standalone/app/components/BridgeRing.js +0 -753
  283. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  284. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  285. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  286. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  287. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  288. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  289. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  290. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  291. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  292. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  293. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  294. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
  295. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
  296. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
  297. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
  298. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
  299. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
@@ -0,0 +1,293 @@
1
+ 'use client';
2
+
3
+ // Their circle, at the top of a profile card (Blake, 2026-09-28).
4
+ //
5
+ // The person in the middle; their scanned circle round them (D2); the people
6
+ // in it you connected with ringed in green, with their own circles fanning out
7
+ // behind them (D3), and so on out to D6. A request still out is a dotted dot.
8
+ // Each dot is coloured by its tier. Rarity (lib/rarity.js) is a filter beside
9
+ // the tier's rather than a mark on every dot (while it's counted from your
10
+ // scans, most people read as rare, and a ring on nearly everything says
11
+ // nothing): the two work together, so "S" and "Only way in" lights exactly the
12
+ // rare finds in this circle and dims the rest. Tap the map for the large one,
13
+ // where a dot opens that person's card.
14
+
15
+ import { useEffect, useMemo, useState } from 'react';
16
+ import { createPortal } from 'react-dom';
17
+ import { circleIndex, circleRings, circleLayout, requestedByDefault } from '../../lib/circle';
18
+ import { keyFor, routeIndex } from '../../lib/separation';
19
+ import { RARITY, rarityOf, passes, countByRarity, toggle, rarityInfo } from '../../lib/rarity';
20
+ import { scoreGuess } from '../../lib/score-guess';
21
+
22
+ const CLASSIC = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
23
+ const TIERS = ['S', 'A', 'B', 'C', 'D'];
24
+ const GREEN = '#00ff88';
25
+
26
+ export default function TheirCircle({ person, connections = [], degree2 = [], tierColors = CLASSIC, onSelect, isRequested = requestedByDefault }) {
27
+ const index = useMemo(() => circleIndex(connections, degree2), [connections, degree2]);
28
+ const ways = useMemo(() => routeIndex(degree2), [degree2]);
29
+ const { rings, totals } = useMemo(() => circleRings(person, index, { isRequested }), [person, index, isRequested]);
30
+ const [tiers, setTiers] = useState(() => new Set());
31
+ const [rarities, setRarities] = useState(() => new Set());
32
+ const [big, setBig] = useState(false);
33
+
34
+ // Each dot's tier and rarity. Your own connections have no rarity: you're
35
+ // already in.
36
+ const people = useMemo(() => rings.flatMap((ring) => ring.people.map((p) => {
37
+ const yours = p.state === 'connected' || p.row.degree === 1;
38
+ const r = yours ? null : rarityOf(p.row, ways.get(keyFor(p.row))?.size || 1);
39
+ return { key: p.key, tier: p.row.tier, rarity: r?.key ?? null, mutuals: r };
40
+ })), [rings, ways]);
41
+ const facts = useMemo(() => new Map(people.map((p) => [p.key, p])), [people]);
42
+ const filter = useMemo(() => ({ tiers, rarities }), [tiers, rarities]);
43
+ const shown = useMemo(() => people.filter((p) => passes(p, filter)).length, [people, filter]);
44
+
45
+ // How complete the ways-in count is: circles scanned out of your connections.
46
+ const scanned = useMemo(() => connections.filter((c) => index.circles.has(c.id)).length, [connections, index]);
47
+ const fromLinkedIn = people.some((p) => p.mutuals?.from === 'linkedin');
48
+ // A score the headline gave nothing to read for: say so, and point at the
49
+ // scan, which is what can firm it up (lib/score-guess.js).
50
+ const guess = useMemo(() => scoreGuess(person), [person]);
51
+
52
+ if (!person) return null;
53
+ if (!rings.length) {
54
+ const first = person.name?.split(' ')[0] || 'them';
55
+ return (
56
+ <div style={guess ? { ...box, borderColor: 'rgba(255,215,0,0.28)' } : box}>
57
+ <Label>Their circle</Label>
58
+ {guess ? (
59
+ <>
60
+ <div style={{ fontSize: 11.5, color: '#d8ccb0', lineHeight: 1.5, marginBottom: 9 }}>
61
+ <b style={{ color: '#FFD700' }}>Their score is a guess.</b> {guess.reason} Scanning {first}&apos;s circle shows
62
+ who you can reach through them, and a strong circle adds up to +2 to their score.
63
+ </div>
64
+ <button type="button" onClick={toScanBox} style={{
65
+ width: '100%', padding: '8px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 700,
66
+ border: '1px solid rgba(255,215,0,0.4)', background: 'rgba(255,215,0,0.08)', color: '#FFD700',
67
+ }}>
68
+ Scan their circle ↓
69
+ </button>
70
+ </>
71
+ ) : (
72
+ <div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>
73
+ Not scanned yet. Scan {first}&apos;s circle below to see who they know, and
74
+ everyone you reach through them after that.
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ const byTier = Object.fromEntries(TIERS.map((t) => [t, people.filter((p) => p.tier === t).length]));
82
+ const byRarity = countByRarity(people);
83
+ const filtering = tiers.size > 0 || rarities.size > 0;
84
+
85
+ const chips = (
86
+ <Filters
87
+ tierColors={tierColors} byTier={byTier} byRarity={byRarity}
88
+ tiers={tiers} rarities={rarities}
89
+ onTier={(t) => setTiers((s) => toggle(s, t))} onRarity={(r) => setRarities((s) => toggle(s, r))}
90
+ onClear={() => { setTiers(new Set()); setRarities(new Set()); }}
91
+ />
92
+ );
93
+ const note = (
94
+ <div style={{ fontSize: 9.5, color: '#777', lineHeight: 1.45, marginTop: 6 }}>
95
+ Rarity is how many mutual connections lead to someone. It never changes a score.{' '}
96
+ {fromLinkedIn
97
+ ? 'LinkedIn’s own count where a scan saved it; otherwise the ways in from your scans.'
98
+ : `Counted from the ${scanned} ${scanned === 1 ? 'circle' : 'circles'} you’ve scanned, so it can only go up as you scan more.`}
99
+ </div>
100
+ );
101
+
102
+ return (
103
+ <div style={box}>
104
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
105
+ <Label>Their circle</Label>
106
+ <button type="button" onClick={() => setBig(true)} style={linkButton} aria-label="Open the large map">
107
+ Enlarge ⤢
108
+ </button>
109
+ </div>
110
+ <button type="button" onClick={() => setBig(true)} aria-label={`${person.name}'s circle: open the large map`} style={{
111
+ display: 'block', width: '100%', padding: 0, border: 'none', background: 'none', cursor: 'zoom-in',
112
+ }}>
113
+ <RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={240} />
114
+ </button>
115
+ <RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
116
+ {chips}
117
+ {note}
118
+ {big && (
119
+ <Enlarged onClose={() => setBig(false)} title={`${person.name}’s circle`}>
120
+ <RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={640} labels
121
+ onPick={(row) => { setBig(false); onSelect?.(row); }} />
122
+ <RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
123
+ {chips}
124
+ <Legend />
125
+ {note}
126
+ </Enlarged>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function RingMap({ person, rings, facts, filter, tierColors, size, labels = false, onPick }) {
133
+ const layout = useMemo(() => circleLayout(rings, size), [rings, size]);
134
+ const c = layout.center;
135
+ const k = size / 240;
136
+ return (
137
+ <svg viewBox={`0 0 ${size} ${size}`} width="100%" style={{ display: 'block', maxWidth: size, margin: '0 auto' }}
138
+ role="img" aria-label={`${person.name}'s circle, ${rings.length} ${rings.length === 1 ? 'ring' : 'rings'}`}>
139
+ {layout.rings.map((ring) => (
140
+ <g key={ring.degree}>
141
+ <circle cx={c.x} cy={c.y} r={ring.radius} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={0.6 * k} />
142
+ <text x={c.x} y={c.y - ring.radius - ring.dotR - 3 * k} textAnchor="middle" fontSize={6.5 * k} fill="#666" fontWeight={700}>
143
+ D{ring.degree}
144
+ </text>
145
+ </g>
146
+ ))}
147
+ {layout.spokes.map((s, i) => (
148
+ <line key={i} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="rgba(0,255,136,0.35)" strokeWidth={0.7 * k} />
149
+ ))}
150
+ {layout.rings.map((ring) => ring.items.map((it) => {
151
+ const f = facts.get(it.key);
152
+ const on = passes(f, filter);
153
+ const color = tierColors[it.row.tier] || '#888';
154
+ const r = it.state === 'connected' ? ring.dotR * 1.7 : ring.dotR;
155
+ const tip = [
156
+ `${it.row.name} · ${it.row.tier}-tier${it.row.power_score != null ? ` · ${Number(it.row.power_score).toFixed(1)}` : ''}`,
157
+ it.state === 'connected' ? 'You added them' : it.state === 'requested' ? 'Request sent' : null,
158
+ f?.mutuals ? `${rarityInfo(f.rarity)?.label}: ${f.mutuals.count} mutual${f.mutuals.count === 1 ? '' : 's'}${f.mutuals.from === 'scans' ? ' (from your scans)' : ''}` : null,
159
+ ].filter(Boolean).join('\n');
160
+ return (
161
+ <g key={it.key} opacity={on ? 1 : 0.12} style={onPick ? { cursor: 'pointer' } : undefined}
162
+ onClick={onPick ? (e) => { e.stopPropagation(); onPick(it.row); } : undefined}>
163
+ <title>{tip}</title>
164
+ {it.state === 'requested' ? (
165
+ <circle cx={it.x} cy={it.y} r={r * 1.25} fill="none" stroke={color} strokeWidth={Math.max(0.5, r * 0.45)}
166
+ strokeDasharray={`${Math.max(0.6, r * 0.55)} ${Math.max(0.5, r * 0.45)}`} />
167
+ ) : (
168
+ <circle cx={it.x} cy={it.y} r={r} fill={color}
169
+ stroke={it.state === 'connected' ? GREEN : 'none'} strokeWidth={Math.max(0.6, r * 0.3)} />
170
+ )}
171
+ {labels && it.state === 'connected' && (
172
+ <text x={it.x} y={it.y - r - 3} textAnchor="middle" fontSize={9} fill={GREEN} fontWeight={700}>
173
+ {String(it.row.name || '').split(' ')[0]}
174
+ </text>
175
+ )}
176
+ </g>
177
+ );
178
+ }))}
179
+ <circle cx={c.x} cy={c.y} r={7 * k} fill={tierColors[person.tier] || '#888'} stroke="#fff" strokeWidth={0.8 * k} />
180
+ <text x={c.x} y={c.y + 2.4 * k} textAnchor="middle" fontSize={6.5 * k} fontWeight={800} fill={person.tier === 'S' ? '#000' : '#fff'}>
181
+ {String(person.name || '?').charAt(0)}
182
+ </text>
183
+ </svg>
184
+ );
185
+ }
186
+
187
+ function RingCounts({ rings, totals, shown }) {
188
+ return (
189
+ <div style={{ fontSize: 10.5, color: '#aaa', marginTop: 6, lineHeight: 1.5 }}>
190
+ {rings.map((r) => `D${r.degree} ${r.people.length.toLocaleString('en-US')}`).join(' · ')}
191
+ {totals.connected > 0 && <span style={{ color: GREEN }}> · you added {totals.connected}</span>}
192
+ {totals.requested > 0 && <span style={{ color: '#FFD700' }}> · {totals.requested} {totals.requested === 1 ? 'request' : 'requests'} out</span>}
193
+ {shown != null && <span style={{ color: '#fff' }}> · {shown.toLocaleString('en-US')} shown</span>}
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function Filters({ tierColors, byTier, byRarity, tiers, rarities, onTier, onRarity, onClear }) {
199
+ const chip = (on, color) => ({
200
+ height: 22, padding: '0 7px', borderRadius: 11, cursor: 'pointer', fontSize: 10, fontWeight: 700,
201
+ whiteSpace: 'nowrap', border: `1px solid ${on ? color : 'rgba(255,255,255,0.1)'}`,
202
+ background: on ? `${color}22` : 'rgba(255,255,255,0.03)', color: on ? color : '#999',
203
+ });
204
+ return (
205
+ <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 5 }}>
206
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
207
+ <span style={rowLabel}>Tier</span>
208
+ {TIERS.map((t) => (
209
+ <button key={t} type="button" aria-pressed={tiers.has(t)} onClick={() => onTier(t)} style={chip(tiers.has(t), tierColors[t] || '#888')}>
210
+ {t} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byTier[t]}</span>
211
+ </button>
212
+ ))}
213
+ </div>
214
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
215
+ <span style={rowLabel}>Rarity</span>
216
+ {RARITY.map((r) => (
217
+ <button key={r.key} type="button" aria-pressed={rarities.has(r.key)} onClick={() => onRarity(r.key)}
218
+ title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}`} style={chip(rarities.has(r.key), r.color)}>
219
+ {r.label} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byRarity[r.key]}</span>
220
+ </button>
221
+ ))}
222
+ {(tiers.size > 0 || rarities.size > 0) && (
223
+ <button type="button" onClick={onClear} style={linkButton}>Clear</button>
224
+ )}
225
+ </div>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ function Legend() {
231
+ const item = (swatch, text) => (
232
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginRight: 12, whiteSpace: 'nowrap' }}>{swatch}{text}</span>
233
+ );
234
+ return (
235
+ <div style={{ fontSize: 10.5, color: '#999', marginTop: 8, lineHeight: 1.8 }}>
236
+ {item(<svg width="10" height="10"><circle cx="5" cy="5" r="4" fill="#FFD700" /></svg>, 'colour = tier')}
237
+ {item(<svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9B59B6" stroke={GREEN} strokeWidth="1.5" /></svg>, 'you added them')}
238
+ {item(<svg width="12" height="12"><circle cx="6" cy="6" r="4.2" fill="none" stroke="#3498DB" strokeWidth="1.4" strokeDasharray="2 1.6" /></svg>, 'request sent')}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ // Rendered into <body>, not the card: the side panel's backdrop blur makes it
244
+ // the containing block for anything position: fixed inside it, so an overlay
245
+ // there only ever covered the panel. Fixed, so it can't change the page's
246
+ // size (TRAPS §29).
247
+ function Enlarged({ title, onClose, children }) {
248
+ useEffect(() => {
249
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
250
+ window.addEventListener('keydown', onKey);
251
+ return () => window.removeEventListener('keydown', onKey);
252
+ }, [onClose]);
253
+ return createPortal(
254
+ <div role="dialog" aria-modal="true" aria-label={title} onClick={onClose} style={{
255
+ position: 'fixed', inset: 0, zIndex: 300, background: 'rgba(0,0,0,0.78)',
256
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
257
+ }}>
258
+ <div onClick={(e) => e.stopPropagation()} style={{
259
+ width: 'min(720px, 100%)', maxHeight: '100%', overflowY: 'auto', boxSizing: 'border-box',
260
+ background: '#0f0c05', border: '1px solid rgba(255,215,0,0.18)', borderRadius: 14, padding: 16,
261
+ }}>
262
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
263
+ <div style={{ fontSize: 14, fontWeight: 800, color: '#fff' }}>{title}</div>
264
+ <button type="button" onClick={onClose} style={{ ...linkButton, fontSize: 12 }}>Close ✕</button>
265
+ </div>
266
+ <div style={{ fontSize: 10.5, color: '#888', marginBottom: 8 }}>Click a dot to open that person.</div>
267
+ {children}
268
+ </div>
269
+ </div>,
270
+ document.body,
271
+ );
272
+ }
273
+
274
+ // The card's scan box (Sidebar.js CreateClusterCard, id "scan-box"): bring it
275
+ // into view, flash it, and put the keyboard on its button.
276
+ function toScanBox() {
277
+ const scanBox = document.getElementById('scan-box');
278
+ if (!scanBox) return;
279
+ scanBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
280
+ scanBox.animate?.([{ boxShadow: '0 0 0 3px rgba(255,215,0,0.75)' }, { boxShadow: '0 0 0 0 rgba(255,215,0,0)' }], { duration: 1600 });
281
+ scanBox.querySelector('button:not([disabled])')?.focus({ preventScroll: true });
282
+ }
283
+
284
+ function Label({ children }) {
285
+ return <div style={{ fontSize: 10, fontWeight: 700, color: '#FF6B35', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 6 }}>{children}</div>;
286
+ }
287
+
288
+ const box = {
289
+ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)',
290
+ borderRadius: 10, padding: 10, marginBottom: 16,
291
+ };
292
+ const rowLabel = { fontSize: 9, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, width: 38, flexShrink: 0 };
293
+ const linkButton = { background: 'none', border: 'none', color: '#888', fontSize: 10.5, fontWeight: 600, cursor: 'pointer', padding: 0 };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- // Settings → Your sector. Up to three sectors you work in: one of the twelve
3
+ // Scores → Your sector (app/scores; it was Settings → Your sector). Up to three sectors you work in: one of the twelve
4
4
  // broad industries, or a narrower sector from the app's directory (Dental,
5
5
  // Real Estate, Software & SaaS…). Companies in them count for more when your
6
6
  // scanned network is scored, like setting their scores by hand on Paths →
@@ -13,7 +13,6 @@
13
13
  // people you've scanned (/api/settings/sector-suggestions).
14
14
 
15
15
  import { useEffect, useMemo, useRef, useState } from 'react';
16
- import Link from 'next/link';
17
16
  import { Section, Body, Status, Btn, LINE } from '../ui';
18
17
  import { INDUSTRIES } from '../../../lib/companies';
19
18
  import { MAX_SECTORS, sameFocus, focusFingerprint } from '../../../lib/sector-focus';
@@ -64,7 +63,7 @@ function tierLine(up, down, past) {
64
63
  return up ? `${plural(up, 'person', 'people')} ${verb(up)} up a tier.` : `${plural(down, 'person', 'people')} ${verb(down)} down a tier.`;
65
64
  }
66
65
 
67
- export default function SectorSection() {
66
+ export default function SectorSection({ onSaved } = {}) {
68
67
  const [saved, setSaved] = useState(null);
69
68
  const [draft, setDraft] = useState(null);
70
69
  const [preview, setPreview] = useState(null);
@@ -103,7 +102,7 @@ export default function SectorSection() {
103
102
  return () => { off = true; clearTimeout(timer.current); };
104
103
  }, []);
105
104
 
106
- // Links to /settings#sector (the profile's card, Paths → Scores) arrive
105
+ // Links to /scores#sector (the profile's card, the old /settings#sector) arrive
107
106
  // before this section has loaded, so the browser's own jump to it finds
108
107
  // nothing. Jump once it's here, then keep it in place while what loads above
109
108
  // it (Updates) pushes it down: the browser only holds steady what's already
@@ -184,6 +183,7 @@ export default function SectorSection() {
184
183
  if (d.settings) { setSaved(d.settings.sectorFocus); setDraft(d.settings.sectorFocus); setPreview(null); }
185
184
  if (!r.ok) { setResult({ tone: 'bad', text: d.error || 'Could not save.' }); return; }
186
185
  setResult({ tone: 'ok', text: savedLine(d.settings.sectorFocus, d.effects?.sectorFocus) });
186
+ onSaved?.();
187
187
  } catch {
188
188
  setResult({ tone: 'bad', text: 'Could not reach the app. Reload this page to see what is saved.' });
189
189
  } finally {
@@ -321,7 +321,7 @@ export default function SectorSection() {
321
321
 
322
322
  <ul style={{ margin: '16px 0 0', paddingLeft: 18, fontSize: 12.5, color: '#778', lineHeight: 1.7 }}>
323
323
  <li>Tiers rank how reachable someone is through your network, not the people themselves.</li>
324
- <li>A company score you set on <Link href="/paths?tab=scores" style={{ color: '#3498DB' }}>Paths → Scores</Link> always wins. Your sector never changes it.</li>
324
+ <li>A company score you set on <a href="#companies" style={{ color: '#3498DB' }}>Company scores</a>, below, always wins. Your sector never changes it.</li>
325
325
  <li>If you scan highest tier first, people in your sectors come up sooner. Scanning newest first, the default, doesn&rsquo;t go by tier.</li>
326
326
  <li>
327
327
  An industry includes its sectors. It counts a company in any of its sectors (Healthcare &amp; Biotech counts a
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ // Scores → Tiers (app/scores; it was Settings → Tiers). How the S to D rings are drawn: on your network's curve
4
+ // (your top 3% are S, and so on down), or on the fixed scale, the same lines
5
+ // for everyone. The curve only lifts, so a network full of companies the
6
+ // curated list knows keeps its tiers. The model is lib/scoring.js (CURVE,
7
+ // curvedTier); saving rescores everyone, as a new sector focus does, and says
8
+ // how many people changed tier (lib/settings-effects.js).
9
+
10
+ import { useEffect, useState } from 'react';
11
+ import { Section, Body, Status, LINE } from '../ui';
12
+ import { CURVE, CURVE_FLOOR } from '../../../lib/scoring';
13
+ import { csvNetworkSource } from '../../../lib/csv';
14
+
15
+ const pct = (x) => `${Math.round(x * 100)}%`;
16
+ const [S, A, B] = CURVE.map(([, share]) => share);
17
+ const plural = (n, one, many) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
18
+
19
+ const OPTIONS = [
20
+ {
21
+ key: 'curve',
22
+ label: "On your network's curve",
23
+ note: `Your top ${pct(S)} are S, the next ${pct(A - S)} A and the next ${pct(B - A)} B, so any network has a top, even one full of companies the app doesn't know. It only lifts: nobody drops a tier because their network is strong, and nobody under ${CURVE_FLOOR} is lifted into S or A.`,
24
+ },
25
+ {
26
+ key: 'fixed',
27
+ label: 'On the fixed scale',
28
+ note: 'The same lines for everyone: S from 7.5, A from 5.5, B from 4, C from 2.5. Easy to compare from one network to another, but a network outside well-known companies has almost nobody in S or A.',
29
+ },
30
+ ];
31
+
32
+ export default function TierSection({ onSaved } = {}) {
33
+ const [saved, setSaved] = useState(null);
34
+ const [saving, setSaving] = useState(false);
35
+ const [result, setResult] = useState(null);
36
+ const [loadError, setLoadError] = useState(null);
37
+ // A CSV import or the sample network held in this window: neither is changed
38
+ // by this (a CSV is always graded on its own curve; the sample keeps its
39
+ // tiers). This section only renders in the browser (OnboardingGate), so
40
+ // reading sessionStorage here can't disagree with a server render.
41
+ const [onScreen] = useState(() => (typeof window === 'undefined' ? null : csvNetworkSource()));
42
+
43
+ useEffect(() => {
44
+ let off = false;
45
+ fetch('/api/settings')
46
+ .then((r) => r.json().then((d) => (r.ok ? d : Promise.reject(new Error(d.error || 'Could not load how tiers are graded.')))))
47
+ .then((d) => { if (!off) setSaved(d.settings?.tierScale || 'curve'); })
48
+ .catch((e) => { if (!off) setLoadError(e.message); });
49
+ return () => { off = true; };
50
+ }, []);
51
+
52
+ async function choose(key) {
53
+ if (saving || key === saved) return;
54
+ setSaving(true);
55
+ setResult(null);
56
+ try {
57
+ const r = await fetch('/api/settings', {
58
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { tierScale: key } }),
59
+ });
60
+ const d = await r.json();
61
+ if (d.settings) setSaved(d.settings.tierScale);
62
+ if (!r.ok) { setResult({ tone: 'bad', text: d.error || 'Could not save.' }); return; }
63
+ const e = d.effects?.tierScale;
64
+ setResult({ tone: 'ok', text: e ? `Saved. ${plural(e.moved, 'person', 'people')} changed tier.` : 'Saved.' });
65
+ onSaved?.();
66
+ } catch {
67
+ setResult({ tone: 'bad', text: 'Could not reach the app. Reload this page to see what is saved.' });
68
+ } finally {
69
+ setSaving(false);
70
+ }
71
+ }
72
+
73
+ return (
74
+ <Section id="tiers" title="Tiers" intro="How the S to D rings are drawn. A tier says where someone sits in your network, not what they're worth.">
75
+ {loadError && <Status tone="bad">{loadError}</Status>}
76
+ {saved && (
77
+ <div role="radiogroup" aria-label="How tiers are graded" style={{ display: 'grid', gap: 8 }}>
78
+ {OPTIONS.map((o) => {
79
+ const on = saved === o.key;
80
+ return (
81
+ <button key={o.key} type="button" role="radio" aria-checked={on} disabled={saving} onClick={() => choose(o.key)} style={{
82
+ textAlign: 'left', padding: '12px 14px', borderRadius: 8, cursor: saving ? 'wait' : on ? 'default' : 'pointer', color: '#fff',
83
+ background: on ? 'rgba(155,89,182,0.14)' : 'rgba(255,255,255,0.03)',
84
+ border: on ? '1px solid rgba(155,89,182,0.6)' : LINE,
85
+ }}>
86
+ <div style={{ fontSize: 14, fontWeight: 650 }}>{on ? '● ' : '○ '}{o.label}{o.key === 'curve' ? ' (recommended)' : ''}</div>
87
+ <Body style={{ marginTop: 4 }}>{o.note}</Body>
88
+ </button>
89
+ );
90
+ })}
91
+ </div>
92
+ )}
93
+ {onScreen && (
94
+ <Body style={{ color: '#FFD700', marginTop: 10 }}>
95
+ {onScreen === 'sample'
96
+ ? 'The sample network open in this window keeps its own tiers (they’re the same either way).'
97
+ : 'The CSV import open in this window is always graded on its own curve.'}{' '}
98
+ This applies to networks you&rsquo;ve scanned.
99
+ </Body>
100
+ )}
101
+ {result && <Status tone={result.tone}>{result.text}</Status>}
102
+ </Section>
103
+ );
104
+ }
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+ import { watchRequests, requestsNow, REQUESTS_UNKNOWN } from '../../lib/requests-client';
5
+
6
+ /**
7
+ * Who you've sent a request to, shared by every view (lib/requests-client.js).
8
+ * Pass the answer to hasRequest(row, state) and requestCount(state).
9
+ */
10
+ export default function useRequests() {
11
+ return useSyncExternalStore(watchRequests, requestsNow, () => REQUESTS_UNKNOWN);
12
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+ import { watchScanner, scannerNow, SCANNER_UNKNOWN } from '../../lib/scraper-client';
5
+
6
+ /**
7
+ * What the scanner is running, shared by every Scan button on the page: one
8
+ * answer, asked once (lib/scraper-client.js). Any button that starts a scan
9
+ * greys out while `running`, and says why with busyReason().
10
+ */
11
+ export default function useScanner() {
12
+ return useSyncExternalStore(watchScanner, scannerNow, () => SCANNER_UNKNOWN);
13
+ }
@@ -13,7 +13,6 @@
13
13
  import ForceGraph from './ForceGraph';
14
14
  import OrbitGraph from './OrbitGraph';
15
15
  import ChainView from './ChainView';
16
- import BridgeRing from './BridgeRing';
17
16
  import GridView from './GridView';
18
17
  import ListView from './ListView';
19
18
  import RingsView from './RingsView';
@@ -31,7 +30,6 @@ export const VIEWS = {
31
30
  orbit: { component: OrbitGraph, modes: ['network', 'degrees'], label: 'Orbit', icon: '🪐', desc: 'Tier orbits + circle dots; in Degrees, every mapped circle fanned out', allDegree2: true },
32
31
  separation: { component: SeparationView, modes: ['degrees'], label: 'Separation', icon: '🏆', desc: 'Every 2nd degree, ranked · every way in' },
33
32
  chain: { component: ChainView, modes: ['degrees'], label: 'Bridge Chains', icon: '🔗', desc: 'Degree paths' },
34
- revolver: { component: BridgeRing, modes: ['degrees'], label: 'Revolver', icon: '🎯', desc: 'Rotary dial · spin to switch' },
35
33
  rings: { component: RingsView, modes: ['network', 'degrees'], label: 'Pyramid', icon: '🔺', desc: 'Tier hierarchy' },
36
34
  list: { component: ListView, modes: ['network', 'degrees'], label: 'List', icon: '☰', desc: 'Ranked power list' },
37
35
  grid: { component: GridView, modes: ['network', 'degrees'], label: 'Grid', icon: '▦', desc: 'Cards' },
@@ -42,7 +40,7 @@ export const VIEWS = {
42
40
  * it is also where resolveView lands when the chosen view isn't a Degrees one
43
41
  * (Galaxy, say, carried over from Network Circle).
44
42
  */
45
- const ORDER = ['galaxy', 'orbit', 'separation', 'chain', 'revolver', 'rings', 'list'];
43
+ const ORDER = ['galaxy', 'orbit', 'separation', 'chain', 'rings', 'list'];
46
44
 
47
45
  export function viewsForMode(mode) {
48
46
  return ORDER
@@ -474,6 +474,9 @@ function ScoringSection() {
474
474
  </div>
475
475
  ))}
476
476
  </div>
477
+ <p style={{ textAlign: 'center', fontSize: 12, color: '#555', marginTop: 24 }}>
478
+ These are the fixed scale&rsquo;s lines. By default tiers are graded on your own network&rsquo;s curve: your top 3% are S, the next 12% A.
479
+ </p>
477
480
  </div>
478
481
  </section>
479
482
  );
@@ -615,7 +618,7 @@ function TechSection() {
615
618
  // The model in lib/scoring.js: a product, so a title counts for more at a bigger company.
616
619
  { sym: 'T', label: 'Title', desc: 'C-suite 10, VP 9, Director 7.5, Manager 6.5', note: 'Multiplied by the company', color: TIER.S },
617
620
  { sym: 'C', label: 'Company', desc: 'Well-known 7–10, else estimated from your network', note: 'Weight 0.5 to 1.0', color: TIER.A },
618
- { sym: 'R + B', label: 'Reach & bridge', desc: 'Investor, YC, an audience in the millions; a strong circle', note: 'Up to +1.5 and +1', color: TIER.B },
621
+ { sym: 'R + B', label: 'Reach & bridge', desc: 'Investor, YC, an audience in the millions; a strong circle', note: 'Up to +1.5 and +2', color: TIER.B },
619
622
  ].map((item, i) => (
620
623
  <div key={i} style={{
621
624
  padding: 16, borderRadius: 12, background: `${item.color}08`,