six-degrees 0.2.0 → 0.2.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 (180) 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 +11 -11
  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 +11 -11
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  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.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/import.html +1 -1
  38. package/.next/standalone/.next/server/app/import.rsc +13 -13
  39. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  42. package/.next/standalone/.next/server/app/index.html +1 -1
  43. package/.next/standalone/.next/server/app/index.rsc +13 -13
  44. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  45. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  46. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/launch.html +2 -2
  50. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  51. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  52. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  53. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  54. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/paths.html +1 -1
  59. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  60. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  61. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  62. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  63. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/profile.html +1 -1
  66. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  67. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  68. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  69. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  70. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/queue.html +1 -1
  73. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  74. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  75. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  77. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/setup.html +1 -1
  80. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  81. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  83. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/_1_fynia._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  100. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  101. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  102. package/.next/standalone/.next/server/pages/404.html +1 -1
  103. package/.next/standalone/.next/server/pages/500.html +1 -1
  104. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  106. package/.next/standalone/.next/static/chunks/0jk64vo17avsb.js +1 -0
  107. package/.next/standalone/.next/static/chunks/0s65dsrthylo6.js +2 -0
  108. package/.next/standalone/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  109. package/.next/standalone/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  110. package/.next/{static/chunks/3ts-_4i6bfkh2.js → standalone/.next/static/chunks/2g54uafnwuakc.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  112. package/.next/{static/chunks/3pfyuezyqd9ob.js → standalone/.next/static/chunks/2spwqs6b6dx13.js} +1 -1
  113. package/.next/{static/chunks/1odow92u6wxz0.js → standalone/.next/static/chunks/38dlepuubiox0.js} +1 -1
  114. package/.next/standalone/AGENTS.md +10 -0
  115. package/.next/standalone/CHANGELOG.md +41 -0
  116. package/.next/standalone/CONTRIBUTING.md +9 -0
  117. package/.next/standalone/README.md +49 -29
  118. package/.next/standalone/SECURITY.md +12 -9
  119. package/.next/standalone/app/api/scraper/route.js +4 -3
  120. package/.next/standalone/app/api/update/route.js +10 -2
  121. package/.next/standalone/app/components/ForceGraph.js +1 -1
  122. package/.next/standalone/app/components/Sidebar.js +8 -8
  123. package/.next/standalone/app/components/UpdatePanel.js +39 -8
  124. package/.next/standalone/app/import/page.js +3 -3
  125. package/.next/standalone/app/layout.js +2 -2
  126. package/.next/standalone/app/page.js +2 -2
  127. package/.next/standalone/app/paths/page.js +1 -1
  128. package/.next/standalone/app/profile/page.js +0 -178
  129. package/.next/standalone/app/setup/page.js +4 -4
  130. package/.next/standalone/bin/six-degrees.mjs +7 -2
  131. package/.next/standalone/install.sh +1 -1
  132. package/.next/standalone/lib/csv.js +53 -27
  133. package/.next/standalone/lib/release.js +5 -2
  134. package/.next/standalone/lib/scraper-client.js +3 -3
  135. package/.next/standalone/package-lock.json +103 -9
  136. package/.next/standalone/package.json +12 -10
  137. package/.next/standalone/public/demo-data.json +1 -1
  138. package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
  139. package/.next/standalone/scripts/readme_buttons.py +89 -0
  140. package/.next/standalone/scripts/screenshots.py +2 -0
  141. package/.next/standalone/site/app.js +84 -0
  142. package/.next/standalone/site/index.html +221 -0
  143. package/.next/standalone/site/style.css +155 -0
  144. package/.next/standalone/site/video/install-mac-poster.jpg +0 -0
  145. package/.next/standalone/site/video/install-mac.mp4 +0 -0
  146. package/.next/standalone/site/video/linux-poster.jpg +0 -0
  147. package/.next/standalone/site/video/linux.mp4 +0 -0
  148. package/.next/standalone/site/video/scan-poster.jpg +0 -0
  149. package/.next/standalone/site/video/scan.mp4 +0 -0
  150. package/.next/static/chunks/0jk64vo17avsb.js +1 -0
  151. package/.next/static/chunks/0s65dsrthylo6.js +2 -0
  152. package/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  153. package/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  154. package/.next/{standalone/.next/static/chunks/3ts-_4i6bfkh2.js → static/chunks/2g54uafnwuakc.js} +1 -1
  155. package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  156. package/.next/{standalone/.next/static/chunks/3pfyuezyqd9ob.js → static/chunks/2spwqs6b6dx13.js} +1 -1
  157. package/.next/{standalone/.next/static/chunks/1odow92u6wxz0.js → static/chunks/38dlepuubiox0.js} +1 -1
  158. package/CHANGELOG.md +41 -0
  159. package/README.md +49 -29
  160. package/SECURITY.md +12 -9
  161. package/bin/six-degrees.mjs +7 -2
  162. package/lib/csv.js +53 -27
  163. package/lib/release.js +5 -2
  164. package/lib/scraper-client.js +3 -3
  165. package/package.json +12 -10
  166. package/public/demo-data.json +1 -1
  167. package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  168. package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  169. package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  170. package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  171. package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  172. package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  173. package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  174. package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  175. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  177. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
  178. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  179. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  180. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
@@ -179,6 +179,21 @@ function main() {
179
179
  const circle = Array.from({ length: size }, () => person(2, b.id));
180
180
  degree2.push(...circle);
181
181
  }
182
+ // In a real network some people are known by more than one of your
183
+ // connections: that is what "reachable 2+ ways" counts, and what makes
184
+ // sorting Degrees by ways in worth doing. About one in seven gets a second
185
+ // route, and some of those a third. Same person, same profile, another bridge.
186
+ const shared = [];
187
+ for (const p of degree2) {
188
+ if (rand() >= 0.15) continue;
189
+ const others = bridges.filter((b) => b.id !== p.source_connection_id);
190
+ const routes = rand() < 0.3 ? 2 : 1;
191
+ for (let k = 0; k < routes; k++) {
192
+ const b = others.splice(Math.floor(rand() * others.length), 1)[0];
193
+ shared.push({ ...p, id: `${p.id}-via${k + 1}`, source_connection_id: b.id, influence_signals: {} });
194
+ }
195
+ }
196
+ degree2.push(...shared);
182
197
  // Everyone again, together: circles now exist, so bridges get their boost.
183
198
  scoreRows([...degree1, ...degree2]);
184
199
  degree1.sort((a, b) => b.power_score - a.power_score);
@@ -205,7 +220,7 @@ function main() {
205
220
  const kb = Math.round(JSON.stringify(payload).length / 1024);
206
221
  console.log(`wrote ${out} (${kb} KB, seed ${seed})`);
207
222
  console.log(` 1st degree : ${degree1.length}`);
208
- console.log(` 2nd degree : ${degree2.length} across ${BRIDGE_COUNT} bridges`);
223
+ console.log(` 2nd degree : ${degree2.length - shared.length} people across ${BRIDGE_COUNT} bridges, ${new Set(shared.map((r) => r.profile_url)).size} of them reachable 2+ ways`);
209
224
  console.log(` catalysts : ${bridges.filter((b) => b.is_catalyst).length}`);
210
225
  console.log(` tier mix : ${['S','A','B','C','D'].map((t) => `${t}:${tiers[t] || 0}`).join(' ')}`);
211
226
  }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """Draw the README's download buttons.
3
+
4
+ A GitHub README can't style links, so the buttons are pictures: drawn here from
5
+ HTML in Chrome, with the app icon inside, and saved at twice their shown size so
6
+ they stay sharp. Each has its own background, so it reads on GitHub's light and
7
+ dark themes alike. No server needed:
8
+
9
+ python3 scripts/readme_buttons.py
10
+
11
+ Requires playwright (pip3 install playwright) and Google Chrome. Redraw whenever
12
+ the icon (desktop/icon/icon.svg) or the wording changes.
13
+ """
14
+ import pathlib
15
+ import re
16
+ import sys
17
+
18
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
19
+ OUT = ROOT / "docs" / "img"
20
+ ICON = (ROOT / "desktop" / "icon" / "icon.svg").read_text()
21
+ # The icon's artwork sits in its 1024 canvas with a margin (the macOS grid).
22
+ # Cropping to the rounded square lets it fill its spot in the button. A new
23
+ # icon with another canvas needs a new crop, so stop rather than draw it wrong.
24
+ CROP = "100 100 824 824"
25
+ root = re.search(r"<svg\b[^>]*>", ICON)
26
+ if not root or 'viewBox="0 0 1024 1024"' not in root.group(0):
27
+ sys.exit('desktop/icon/icon.svg is no longer a 1024 canvas (viewBox="0 0 1024 1024"): '
28
+ "update CROP in scripts/readme_buttons.py, then look at docs/img/download-*.png")
29
+ tag = re.sub(r'\s(?:viewBox|width|height)="[^"]*"', "", root.group(0))
30
+ ICON = ICON[:root.start()] + tag.replace("<svg", f'<svg viewBox="{CROP}"', 1) + ICON[root.end():]
31
+
32
+ ARROW = """<svg class="arrow" viewBox="0 0 24 24" width="30" height="30" aria-hidden="true">
33
+ <circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.6" opacity=".55"/>
34
+ <path d="M12 6.5v9M7.8 11.6 12 15.8l4.2-4.2" fill="none" stroke="currentColor" stroke-width="2"
35
+ stroke-linecap="round" stroke-linejoin="round"/></svg>"""
36
+
37
+ # (file, top line, big line, bottom line, style)
38
+ BUTTONS = [
39
+ ("download-apple-silicon.png", "Click to download for Mac", "Apple Silicon", "M1 or newer", "green"),
40
+ ("download-intel.png", "Click to download for Mac", "Intel", "Macs with an Intel chip", "slate"),
41
+ ]
42
+
43
+ CSS = """
44
+ * { box-sizing: border-box; margin: 0; }
45
+ body { background: transparent; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
46
+ -webkit-font-smoothing: antialiased; }
47
+ .pad { display: inline-block; padding: 6px 8px 12px; }
48
+ .btn { display: flex; align-items: center; gap: 16px; width: 330px; padding: 13px 18px 13px 14px;
49
+ border-radius: 16px; color: #fff; }
50
+ /* Dark enough that white text passes WCAG AA (4.5:1) everywhere on it. */
51
+ .green { background: linear-gradient(180deg, #1f883d 0%, #1a7f37 55%, #116329 100%);
52
+ box-shadow: 0 1px 0 rgba(255,255,255,.22) inset, 0 0 0 1px rgba(0,0,0,.12), 0 6px 16px rgba(17,99,41,.35); }
53
+ .slate { background: linear-gradient(180deg, #3a414a 0%, #2b3137 55%, #24292f 100%);
54
+ box-shadow: 0 1px 0 rgba(255,255,255,.16) inset, 0 0 0 1px rgba(255,255,255,.14), 0 6px 16px rgba(0,0,0,.28); }
55
+ .icon { flex: none; width: 56px; height: 56px; border-radius: 13px; overflow: hidden;
56
+ box-shadow: 0 0 0 1px rgba(255,255,255,.18), 0 2px 6px rgba(0,0,0,.35); }
57
+ .icon svg { display: block; width: 56px; height: 56px; }
58
+ .text { flex: 1; min-width: 0; line-height: 1.15; }
59
+ .top { font-size: 12.5px; font-weight: 600; letter-spacing: .02em; }
60
+ .big { font-size: 25px; font-weight: 800; letter-spacing: -.01em; margin: 2px 0 3px; }
61
+ .sub { font-size: 13.5px; font-weight: 500; }
62
+ .arrow { flex: none; color: #fff; }
63
+ """
64
+
65
+
66
+ def button_html(top, big, sub, style):
67
+ return f"""<!doctype html><html><head><style>{CSS}</style></head><body>
68
+ <div class="pad" id="shot"><div class="btn {style}">
69
+ <div class="icon">{ICON}</div>
70
+ <div class="text"><div class="top">{top}</div><div class="big">{big}</div><div class="sub">{sub}</div></div>
71
+ {ARROW}
72
+ </div></div></body></html>"""
73
+
74
+
75
+ def main():
76
+ from playwright.sync_api import sync_playwright
77
+
78
+ with sync_playwright() as p:
79
+ browser = p.chromium.launch(channel="chrome")
80
+ page = browser.new_page(viewport={"width": 600, "height": 200}, device_scale_factor=2)
81
+ for name, top, big, sub, style in BUTTONS:
82
+ page.set_content(button_html(top, big, sub, style))
83
+ page.locator("#shot").screenshot(path=str(OUT / name), omit_background=True)
84
+ print(f" wrote docs/img/{name}")
85
+ browser.close()
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -102,6 +102,8 @@ def main():
102
102
  if script:
103
103
  page.evaluate(script)
104
104
  page.wait_for_timeout(1500)
105
+ # The dev server's own badge (Next.js's "N" in a corner) isn't part of the app.
106
+ page.add_style_tag(content="nextjs-portal { display: none !important; }")
105
107
  page.screenshot(path=str(OUT / name))
106
108
  print(f" wrote docs/img/{name}")
107
109
 
@@ -0,0 +1,84 @@
1
+ // The download page's small helpers. The page works fully without them.
2
+ // 1. Not on a Mac: say so plainly, with the Linux route.
3
+ // 2. A likely chip: if the graphics card name gives it away (Chrome shows
4
+ // "Apple M…", or "Intel"), point at that download. Safari hides it; then
5
+ // nothing is guessed and both buttons stay equal.
6
+ // 3. The latest version and its size, asked of GitHub once. If that fails,
7
+ // the page keeps its fallback line.
8
+ // 4. A Copy button for the Terminal line.
9
+
10
+ (() => {
11
+ const $ = (id) => document.getElementById(id);
12
+ const ua = navigator.userAgent || '';
13
+ // iPadOS Safari says "Macintosh", so an iPad is a "Mac" with a touch screen.
14
+ const isPhone = /iPhone|iPad|Android/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
15
+ const isMac = /Macintosh|Mac OS X/.test(ua) && !isPhone;
16
+ let chip = null;
17
+
18
+ if (!isMac) {
19
+ if (isPhone) {
20
+ $('not-mac').textContent = "You're on a phone or tablet. Six Degrees is a Mac app: open this page on your Mac to download it.";
21
+ }
22
+ $('not-mac').hidden = false;
23
+ } else {
24
+ chip = guessChip();
25
+ if (chip) {
26
+ const pick = chip === 'silicon' ? $('dl-silicon') : $('dl-intel');
27
+ pick.classList.add('suggested');
28
+ $('chip-guess').textContent = chip === 'silicon'
29
+ ? 'This looks like an Apple Silicon Mac: take the Apple Silicon download.'
30
+ : 'This looks like an Intel Mac: take the Intel download.';
31
+ $('chip-guess').hidden = false;
32
+ }
33
+ }
34
+
35
+ function guessChip() {
36
+ try {
37
+ const gl = document.createElement('canvas').getContext('webgl');
38
+ const info = gl && gl.getExtension('WEBGL_debug_renderer_info');
39
+ const name = info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : '';
40
+ if (/Apple M\d/.test(name)) return 'silicon';
41
+ if (/Intel|AMD|Radeon/.test(name)) return 'intel';
42
+ } catch { /* no WebGL: no guess */ }
43
+ return null;
44
+ }
45
+
46
+ fetch('https://api.github.com/repos/blakeb056/six-degrees/releases/latest', { headers: { Accept: 'application/vnd.github+json' } })
47
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
48
+ .then((release) => {
49
+ const version = String(release.tag_name || '').replace(/^v/, '');
50
+ if (!version) return;
51
+ const mb = (name) => {
52
+ const a = (release.assets || []).find((x) => x.name === name);
53
+ return a ? Math.round(a.size / 1e6) : null;
54
+ };
55
+ const silicon = mb('Six-Degrees-Mac-Apple-Silicon.dmg');
56
+ const intel = mb('Six-Degrees-Mac-Intel.dmg');
57
+ // The size of the download this Mac needs, when we know which; else both.
58
+ let size = '';
59
+ if (chip === 'silicon' && silicon) size = ` · ${silicon} MB`;
60
+ else if (chip === 'intel' && intel) size = ` · ${intel} MB`;
61
+ else if (silicon && intel) size = ` · ${silicon} MB (Apple Silicon), ${intel} MB (Intel)`;
62
+ $('version-line').textContent = `Version ${version} · macOS 13.5 or later${size} · free and open source`;
63
+ })
64
+ .catch(() => { /* keep the fallback line */ });
65
+
66
+ const copy = $('copy');
67
+ copy.addEventListener('click', async () => {
68
+ const text = $('cmd').textContent.trim();
69
+ try {
70
+ await navigator.clipboard.writeText(text);
71
+ copy.textContent = 'Copied';
72
+ $('copy-status').textContent = 'Install command copied.';
73
+ } catch {
74
+ const range = document.createRange();
75
+ range.selectNodeContents($('cmd'));
76
+ const sel = window.getSelection();
77
+ sel.removeAllRanges();
78
+ sel.addRange(range);
79
+ copy.textContent = 'Press ⌘C';
80
+ $('copy-status').textContent = 'Install command selected. Press Command C to copy it.';
81
+ }
82
+ setTimeout(() => { copy.textContent = 'Copy'; }, 1800);
83
+ });
84
+ })();
@@ -0,0 +1,221 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Six Degrees — see your LinkedIn network as a galaxy</title>
7
+ <meta name="description" content="A free Mac app that maps your LinkedIn network: who you know, who they know, and the shortest path to someone you haven't met. Runs on your own computer; nothing is uploaded.">
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; connect-src https://api.github.com; base-uri 'none'; form-action 'none'">
9
+ <meta name="referrer" content="no-referrer">
10
+ <meta property="og:title" content="Six Degrees">
11
+ <meta property="og:description" content="A free Mac app that maps your LinkedIn network as a galaxy. Runs on your own computer.">
12
+ <meta property="og:image" content="https://blakeb056.github.io/six-degrees/img/app-window.png">
13
+ <meta name="theme-color" content="#0b0a1c">
14
+ <link rel="icon" href="img/icon.svg" type="image/svg+xml">
15
+ <link rel="stylesheet" href="style.css?v=2026-09-25">
16
+ <script src="app.js?v=2026-09-25" defer></script>
17
+ </head>
18
+ <body>
19
+ <header class="top">
20
+ <a class="brand" href="./"><img src="img/icon.svg" alt="" width="32" height="32"> Six Degrees</a>
21
+ <nav>
22
+ <a href="#install">Install</a>
23
+ <a href="#what">What it does</a>
24
+ <a href="#privacy">Privacy</a>
25
+ <a href="https://github.com/blakeb056/six-degrees">GitHub</a>
26
+ </nav>
27
+ </header>
28
+
29
+ <main>
30
+ <section class="hero">
31
+ <img class="app-icon" src="img/icon.svg" alt="" width="96" height="96">
32
+ <h1>See your LinkedIn network <br>as a galaxy.</h1>
33
+ <p class="lede">Who you know, who <em>they</em> know, and the shortest path to someone you haven't met.
34
+ A free Mac app that runs on your own computer. Nothing is uploaded.</p>
35
+
36
+ <p class="not-mac" id="not-mac" role="status" hidden>
37
+ Six Degrees is a Mac app. On Linux, run <code>npx six-degrees</code> (Node 22.13 or later;
38
+ <a href="#linux">details</a>).
39
+ Windows isn't supported yet.
40
+ </p>
41
+
42
+ <div class="downloads" id="download">
43
+ <a class="dl primary" id="dl-silicon" href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg">
44
+ <span class="dl-title">Download for Mac</span>
45
+ <span class="dl-sub">Apple Silicon (M1 or newer)</span>
46
+ </a>
47
+ <a class="dl" id="dl-intel" href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg">
48
+ <span class="dl-title">Download for Mac</span>
49
+ <span class="dl-sub">Intel</span>
50
+ </a>
51
+ </div>
52
+ <p class="fine" id="version-line">macOS 13.5 or later · about 185–190 MB · free and open source</p>
53
+ <p class="guess" id="chip-guess" hidden></p>
54
+ <p class="fine watch"><a href="#install">New here? Watch how to install it (about a minute)</a></p>
55
+
56
+ <img class="shot" src="img/app-window.png" srcset="img/app-window-1200.png 1200w, img/app-window.png 3136w" sizes="(max-width: 1120px) calc(100vw - 32px), 1072px" alt="Six Degrees on a Mac: your network drawn as circles around you, the most powerful people closest" width="3136" height="2118">
57
+ <p class="caption">Every person shown on this page is invented.</p>
58
+ </section>
59
+
60
+ <section class="install" id="install">
61
+ <h2>Install it in a minute</h2>
62
+ <figure class="video">
63
+ <video controls muted playsinline preload="metadata" poster="video/install-mac-poster.jpg" width="1920" height="1080" aria-label="Video: installing Six Degrees on a Mac">
64
+ <source src="video/install-mac.mp4" type="video/mp4">
65
+ </video>
66
+ <figcaption><strong>Installing on a Mac, step by step</strong> (about a minute): the download, the one-time macOS
67
+ warning and Open Anyway, or the one-line Terminal install, then a first look.</figcaption>
68
+ </figure>
69
+ <div class="which">
70
+ <strong>Which download?</strong> Apple menu → <strong>About This Mac</strong>.
71
+ "Chip: Apple M…" means <strong>Apple Silicon</strong>. "Processor: …Intel…" means <strong>Intel</strong>.
72
+ </div>
73
+
74
+ <ol class="steps" role="list">
75
+ <li>
76
+ <h3>Drag it into Applications</h3>
77
+ <p>Open the file you downloaded (<strong>Six-Degrees-Mac-….dmg</strong>, in your Downloads folder).
78
+ In the window that opens, drag <strong>Six Degrees</strong> onto the <strong>Applications</strong> folder.</p>
79
+ </li>
80
+ <li>
81
+ <h3>Open it once, and close the warning</h3>
82
+ <p>Open <strong>Six Degrees</strong> from Applications. macOS says it can't check the app for malware,
83
+ because it isn't signed with a paid Apple certificate yet. Close that message with <strong>Done</strong>
84
+ (on macOS 13 or 14: <strong>OK</strong> or <strong>Cancel</strong>). Anything except <em>Move to Trash</em>.</p>
85
+ </li>
86
+ <li>
87
+ <h3>Allow it in System Settings</h3>
88
+ <p>Open <strong>System Settings → Privacy &amp; Security</strong>, scroll down to
89
+ <em>"Six Degrees" was blocked</em>, and click <strong>Open Anyway</strong>. Confirm with
90
+ <strong>Open Anyway</strong> again (on macOS 13 or 14: <strong>Open</strong>), and your password if asked. That's once per download; after that it opens like any app.</p>
91
+ </li>
92
+ </ol>
93
+
94
+ <div class="terminal">
95
+ <h3>Or skip those steps with Terminal</h3>
96
+ <p>Press <kbd>⌘</kbd> <kbd>Space</kbd>, type <em>Terminal</em> and press Return. Paste this line and press Return:</p>
97
+ <div class="cmd">
98
+ <code id="cmd">curl -fsSL https://raw.githubusercontent.com/blakeb056/six-degrees/main/install.sh | bash</code>
99
+ <button type="button" id="copy" aria-label="Copy the install command">Copy</button>
100
+ </div>
101
+ <span id="copy-status" class="sr-only" role="status"></span>
102
+ <p class="fine">It downloads the right version for your Mac, checks the file is exactly the one published,
103
+ puts Six Degrees in Applications and opens it. Running it again updates the app.
104
+ <a href="https://github.com/blakeb056/six-degrees/blob/main/install.sh">Read the script</a>.</p>
105
+ </div>
106
+ </section>
107
+
108
+ <section class="linux" id="linux">
109
+ <h2>On Linux</h2>
110
+ <figure class="video">
111
+ <video controls muted playsinline preload="metadata" poster="video/linux-poster.jpg" width="1920" height="1080" aria-label="Video: running Six Degrees on Linux">
112
+ <source src="video/linux.mp4" type="video/mp4">
113
+ </video>
114
+ <figcaption><strong>Running it on Linux</strong> (half a minute): one command, then the same app in your browser.</figcaption>
115
+ </figure>
116
+ <div class="terminal">
117
+ <h3>One command</h3>
118
+ <p>Install <strong>Node 22.13 or later</strong> (from nodejs.org or nvm; Ubuntu's own <code>nodejs</code> package is
119
+ too old), then run:</p>
120
+ <div class="cmd"><code>npx six-degrees</code></div>
121
+ <p class="fine">It opens <code>http://127.0.0.1:6363</code> in your browser and keeps your data in
122
+ <code>~/.six-degrees</code>. Stop it with Ctrl-C; update it with <code>npx six-degrees@latest</code>. Scanning
123
+ also needs Google Chrome (not Chromium) and Python 3.9 or later with venv (on Ubuntu:
124
+ <code>sudo apt install python3-venv</code>). Windows isn't supported yet.</p>
125
+ </div>
126
+ </section>
127
+
128
+ <section class="what" id="what">
129
+ <h2>What it shows you</h2>
130
+ <div class="features">
131
+ <article>
132
+ <h3>Network Circle</h3>
133
+ <p>Everyone you know on rings by how much they can open up for you, the most powerful closest to you.</p>
134
+ </article>
135
+ <article>
136
+ <h3>Degrees</h3>
137
+ <p>The people two steps away, ranked, each with every way in: which of your connections can introduce you.
138
+ Needs a scan, or try it with the sample.</p>
139
+ </article>
140
+ <article>
141
+ <h3>Paths</h3>
142
+ <p>Your network by company and industry: who you know at each company, who you can reach, and the warmest way in.</p>
143
+ </article>
144
+ <article>
145
+ <h3>Outlink</h3>
146
+ <p>Getting introduced, as a game: work through each connection's circle five people at a time. Needs a scan.</p>
147
+ </article>
148
+ </div>
149
+
150
+ <figure>
151
+ <img src="img/degrees.png" srcset="img/degrees-1200.png 1200w, img/degrees.png 2880w" sizes="(max-width: 1120px) calc(100vw - 32px), 1072px" alt="Screenshot of the Degrees view" width="2880" height="1800" loading="lazy">
152
+ <figcaption><strong>Degrees.</strong> Everyone two steps away, ranked, with the connection who can introduce you.</figcaption>
153
+ </figure>
154
+ <figure>
155
+ <img src="img/paths.png" srcset="img/paths-1200.png 1200w, img/paths.png 2880w" sizes="(max-width: 1120px) calc(100vw - 32px), 1072px" alt="Screenshot of the Paths view" width="2880" height="1800" loading="lazy">
156
+ <figcaption><strong>Paths.</strong> Companies grouped by industry, and for any one of them, who's there and how to reach them.</figcaption>
157
+ </figure>
158
+ </section>
159
+
160
+ <section class="ways">
161
+ <h2>Three ways to bring your network in</h2>
162
+ <div class="cards">
163
+ <article>
164
+ <h3>Explore a sample</h3>
165
+ <p>150 invented connections and the 598 people they know. Every view except Outlink, which needs your own scan.</p>
166
+ </article>
167
+ <article>
168
+ <h3>Import LinkedIn's export</h3>
169
+ <p>LinkedIn's own <code>Connections.csv</code>, read on your Mac. It shows the people you know, not who they
170
+ know (so no Degrees or Outlink), and there's no risk to your LinkedIn account.</p>
171
+ </article>
172
+ <article>
173
+ <h3>Scan your LinkedIn</h3>
174
+ <p>The only way to see who <em>your connections</em> know. The app opens Google Chrome with a profile of its
175
+ own, you sign into LinkedIn there yourself, and it reads slowly with daily limits. Needs Google Chrome (in
176
+ Applications) and Python 3.9 or later.</p>
177
+ </article>
178
+ </div>
179
+ <p class="warning"><strong>Scanning uses your own LinkedIn account, and LinkedIn may restrict accounts that
180
+ automate it.</strong> Automating LinkedIn may break its User Agreement. It happened twice while building this
181
+ app. The scanner now reads slowly, keeps to a budget and stops at the first push-back, but the risk is yours.
182
+ The sample and the CSV import carry no LinkedIn risk at all.</p>
183
+ <figure class="video" id="scanning">
184
+ <video controls muted playsinline preload="metadata" poster="video/scan-poster.jpg" width="1920" height="1080" aria-label="Video: how scanning works">
185
+ <source src="video/scan.mp4" type="video/mp4">
186
+ </video>
187
+ <figcaption><strong>How scanning works</strong> (under a minute): install what it needs, sign into LinkedIn in the Chrome
188
+ window it opens, scan the people you know, then map who they know a few at a time.</figcaption>
189
+ </figure>
190
+ </section>
191
+
192
+ <section class="privacy" id="privacy">
193
+ <h2>Your network stays on your Mac</h2>
194
+ <ul role="list">
195
+ <li><strong>No account, no server.</strong> Everything stays on your computer: your network in a hidden
196
+ <code>.six-degrees</code> folder, and the app window's own storage (including an imported CSV) in
197
+ <code>~/Library/Application Support/Six Degrees</code>.</li>
198
+ <li><strong>Nothing is uploaded.</strong> No telemetry, no analytics, no crash reports.</li>
199
+ <li><strong>It only goes online when you act:</strong> LinkedIn while you scan (and for profile photos
200
+ until they're saved on your Mac), PyPI once to set up the scanner, and GitHub when you check for updates.</li>
201
+ <li><strong>Open source.</strong> Every line is on <a href="https://github.com/blakeb056/six-degrees">GitHub</a>,
202
+ under the MIT licence.</li>
203
+ </ul>
204
+ <p class="fine">This page has no trackers either. To show the latest version number, it asks GitHub once.</p>
205
+ </section>
206
+
207
+ <section class="again">
208
+ <a class="dl primary" href="#download">
209
+ <span class="dl-title">Get Six Degrees</span>
210
+ <span class="dl-sub">Free for Mac</span>
211
+ </a>
212
+ </section>
213
+ </main>
214
+
215
+ <footer>
216
+ <p>Six Degrees · MIT licence · <a href="https://github.com/blakeb056/six-degrees">Source on GitHub</a> ·
217
+ <a href="https://github.com/blakeb056/six-degrees/releases">All releases</a></p>
218
+ <p class="fine">It estimates network position, not what anyone is worth as a person.</p>
219
+ </footer>
220
+ </body>
221
+ </html>
@@ -0,0 +1,155 @@
1
+ /* The Six Degrees download page. System fonts, no external requests. */
2
+
3
+ :root {
4
+ --bg: #0b0a1c;
5
+ --bg-2: #12102a;
6
+ --panel: rgba(255, 255, 255, 0.04);
7
+ --line: rgba(255, 255, 255, 0.1);
8
+ --text: #eef0f6;
9
+ --muted: #a9aec4;
10
+ --faint: #9297ad;
11
+ --green: #1a7f37;
12
+ --green-2: #238636;
13
+ --gold: #ffd700;
14
+ --purple: #9b59b6;
15
+ --blue: #3498db;
16
+ --orange: #ff8a3d;
17
+ --radius: 14px;
18
+ }
19
+
20
+ * { box-sizing: border-box; }
21
+ html { scroll-behavior: smooth; }
22
+ body {
23
+ margin: 0;
24
+ background: radial-gradient(ellipse 120% 60% at 50% 0%, #2a2350 0%, var(--bg-2) 45%, var(--bg) 80%) no-repeat, var(--bg);
25
+ color: var(--text);
26
+ font: 17px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
27
+ -webkit-font-smoothing: antialiased;
28
+ }
29
+ a { color: #8fc7ff; }
30
+ a:hover { color: #b9dcff; }
31
+ :focus-visible { outline: 3px solid var(--gold); outline-offset: 3px; border-radius: 6px; }
32
+ img, video { max-width: 100%; height: auto; }
33
+ code, kbd { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.92em; }
34
+ kbd { padding: 1px 7px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 6px; background: var(--panel); }
35
+
36
+ .top {
37
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
38
+ max-width: 1120px; margin: 0 auto; padding: 18px 24px;
39
+ }
40
+ .brand { display: inline-flex; align-items: center; gap: 10px; color: var(--text); text-decoration: none; font-weight: 700; font-size: 18px; }
41
+ .top nav { display: flex; gap: 22px; flex-wrap: wrap; }
42
+ .top nav a { color: var(--muted); text-decoration: none; font-size: 15px; }
43
+ .top nav a:hover { color: var(--text); }
44
+
45
+ main { max-width: 1120px; margin: 0 auto; padding: 0 24px; }
46
+ section { padding: 56px 0; }
47
+ h1, h2, h3 { line-height: 1.2; letter-spacing: -0.01em; }
48
+ h2 { font-size: clamp(26px, 4vw, 34px); margin: 0 0 20px; text-align: center; }
49
+ h3 { font-size: 19px; margin: 0 0 6px; }
50
+ .fine { color: var(--faint); font-size: 14px; }
51
+
52
+ /* hero */
53
+ .hero { text-align: center; padding-top: 32px; }
54
+ .app-icon { display: block; margin: 0 auto 18px; filter: drop-shadow(0 10px 30px rgba(155, 89, 182, 0.35)); }
55
+ .hero h1 { font-size: clamp(34px, 6vw, 58px); margin: 0 0 16px; font-weight: 800; }
56
+ .lede { font-size: clamp(17px, 2.2vw, 20px); color: var(--muted); max-width: 640px; margin: 0 auto 28px; }
57
+
58
+ .downloads { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
59
+ .dl {
60
+ display: inline-flex; flex-direction: column; align-items: center; min-width: 250px;
61
+ padding: 14px 26px; border-radius: 12px; text-decoration: none; color: var(--text);
62
+ background: rgba(255, 255, 255, 0.07); border: 1px solid var(--line);
63
+ transition: transform 0.12s ease, background 0.12s ease;
64
+ }
65
+ .dl:hover { transform: translateY(-1px); background: rgba(255, 255, 255, 0.11); color: var(--text); }
66
+ .dl.primary { background: linear-gradient(180deg, var(--green-2), var(--green)); border-color: transparent; color: #fff; box-shadow: 0 10px 30px rgba(46, 164, 79, 0.3); }
67
+ .dl.primary:hover { background: linear-gradient(180deg, #1f883d, #196c2e); }
68
+ .dl.primary .dl-sub { opacity: 1; }
69
+ .dl.suggested { outline: 2px solid var(--gold); outline-offset: 3px; }
70
+ .dl.suggested:focus-visible { outline: 3px solid #fff; outline-offset: 5px; }
71
+ .sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
72
+ .dl-title { font-weight: 700; font-size: 18px; }
73
+ .dl-sub { font-size: 14px; opacity: 0.85; }
74
+ .hero .fine { margin: 14px 0 0; }
75
+ .guess { color: var(--gold); font-size: 14px; margin: 6px 0 0; }
76
+ .not-mac { max-width: 560px; margin: 0 auto 18px; padding: 12px 16px; border-radius: 10px; background: rgba(255, 138, 61, 0.1); border: 1px solid rgba(255, 138, 61, 0.35); color: var(--text); font-size: 15px; }
77
+ .shot { display: block; margin: 44px auto 6px; border-radius: 12px; }
78
+ .caption { color: var(--faint); font-size: 13px; margin: 0; }
79
+
80
+ /* install */
81
+ .which { max-width: 760px; margin: 0 auto 24px; padding: 14px 18px; border-radius: 10px; background: var(--panel); border: 1px solid var(--line); text-align: center; color: var(--muted); }
82
+ .which strong { color: var(--text); }
83
+ .steps { list-style: none; counter-reset: step; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 0; margin: 0 0 24px; }
84
+ .steps li { counter-increment: step; position: relative; padding: 22px 20px 18px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); }
85
+ .steps li::before {
86
+ content: counter(step); position: absolute; top: -14px; left: 18px; width: 30px; height: 30px; border-radius: 50%;
87
+ display: grid; place-items: center; font-weight: 800; font-size: 15px; color: #0b0a1c; background: var(--gold);
88
+ }
89
+ .steps p { margin: 0; color: var(--muted); font-size: 15.5px; }
90
+ .steps strong { color: var(--text); }
91
+ .terminal { max-width: 860px; margin: 0 auto; padding: 22px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); }
92
+ .terminal p { color: var(--muted); margin: 6px 0; }
93
+ .cmd { display: flex; gap: 10px; align-items: stretch; margin: 12px 0; }
94
+ .cmd code { flex: 1; min-width: 0; white-space: normal; overflow-wrap: anywhere; padding: 12px 14px; border-radius: 10px; background: #05040f; border: 1px solid var(--line); color: #d8f5dc; }
95
+ .cmd button {
96
+ flex: none; padding: 0 18px; border-radius: 10px; border: 1px solid var(--line); cursor: pointer;
97
+ background: rgba(255, 255, 255, 0.08); color: var(--text); font: inherit; font-weight: 600; font-size: 15px;
98
+ }
99
+ .cmd button:hover { background: rgba(255, 255, 255, 0.14); }
100
+
101
+ /* the tutorial videos (silent: the captions are in the picture). index.html asks for
102
+ this file by version (style.css?v=…): a browser holding the old copy drew them at
103
+ their full 1920px, off-centre. Bump the version whenever the page's layout changes. */
104
+ .watch { margin: 8px 0 0; }
105
+ .video { max-width: 860px; margin: 0 auto 32px; }
106
+ .video video { display: block; width: 100%; height: auto; border-radius: 12px; border: 1px solid var(--line); background: #05040f; }
107
+ .ways .video { margin: 32px auto 0; }
108
+
109
+ /* what it does */
110
+ .features { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 32px; }
111
+ .features article, .cards article { padding: 18px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); }
112
+ .features article:nth-child(1) h3 { color: var(--gold); }
113
+ .features article:nth-child(2) h3 { color: var(--orange); }
114
+ .features article:nth-child(3) h3 { color: #3ee08f; }
115
+ .features article:nth-child(4) h3 { color: #ff9f5a; }
116
+ .features p, .cards p { margin: 0; color: var(--muted); font-size: 15.5px; }
117
+ figure { margin: 0 0 28px; }
118
+ figure img { display: block; border-radius: 12px; border: 1px solid var(--line); }
119
+ figcaption { color: var(--muted); font-size: 15px; margin-top: 10px; text-align: center; }
120
+ figcaption strong { color: var(--text); }
121
+
122
+ /* ways in */
123
+ .cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
124
+ .warning { max-width: 860px; margin: 22px auto 0; padding: 14px 18px; border-radius: 10px; background: rgba(255, 215, 0, 0.07); border: 1px solid rgba(255, 215, 0, 0.3); color: var(--muted); font-size: 15px; }
125
+ .warning strong { color: var(--text); }
126
+
127
+ /* privacy */
128
+ .privacy ul { max-width: 760px; margin: 0 auto; padding: 0; list-style: none; display: grid; gap: 10px; }
129
+ .privacy li { padding: 14px 18px; border-radius: 10px; background: var(--panel); border: 1px solid var(--line); color: var(--muted); }
130
+ .privacy li strong { color: var(--text); }
131
+ .privacy .fine { text-align: center; }
132
+
133
+ .again { text-align: center; padding-top: 8px; }
134
+
135
+ footer { max-width: 1120px; margin: 0 auto; padding: 40px 24px 56px; text-align: center; color: var(--muted); font-size: 14px; border-top: 1px solid var(--line); }
136
+
137
+ @media (max-width: 860px) {
138
+ .steps, .features, .cards { grid-template-columns: 1fr; }
139
+ .steps { gap: 26px; }
140
+ section { padding: 40px 0; }
141
+ }
142
+ @media (max-width: 520px) {
143
+ body { font-size: 16px; }
144
+ .hero h1 br { display: none; }
145
+ .top nav { gap: 16px; }
146
+ .top nav a:not(:last-child) { display: none; }
147
+ .dl { min-width: 0; width: 100%; }
148
+ .cmd { flex-direction: column; }
149
+ .cmd button { padding: 10px; }
150
+ .top, main { padding-left: 16px; padding-right: 16px; }
151
+ }
152
+ @media (prefers-reduced-motion: reduce) {
153
+ html { scroll-behavior: auto; }
154
+ .dl { transition: none; }
155
+ }