From 956e8e8ea49dffd91bf62e27c141569ef083121d Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 00:36:06 +0000 Subject: [PATCH 01/10] site: four switchable brand directions (phase 1) Co-Authored-By: Dailen Gunter --- scripts/brand/directions.py | 115 +++++++ site/README.md | 37 +++ site/assets/fonts/Inter-LICENSE.txt | 92 ++++++ site/assets/fonts/inter-latin.woff2 | Bin 0 -> 48256 bytes site/assets/icons/Lucide-LICENSE.txt | 15 + site/assets/logo/bound-lockup.svg | 5 + site/assets/logo/bound-mark.svg | 4 + site/assets/logo/singlet-lockup.svg | 7 + site/assets/logo/singlet-mark.svg | 6 + site/assets/logo/spacelike-lockup.svg | 6 + site/assets/logo/spacelike-mark.svg | 5 + site/assets/logo/swap-lockup.svg | 8 + site/assets/logo/swap-mark.svg | 7 + site/directions.js | 441 ++++++++++++++++++++++++++ site/index.html | 148 +++++++++ site/styles.css | 371 ++++++++++++++++++++++ 16 files changed, 1267 insertions(+) create mode 100644 scripts/brand/directions.py create mode 100644 site/README.md create mode 100644 site/assets/fonts/Inter-LICENSE.txt create mode 100644 site/assets/fonts/inter-latin.woff2 create mode 100644 site/assets/icons/Lucide-LICENSE.txt create mode 100644 site/assets/logo/bound-lockup.svg create mode 100644 site/assets/logo/bound-mark.svg create mode 100644 site/assets/logo/singlet-lockup.svg create mode 100644 site/assets/logo/singlet-mark.svg create mode 100644 site/assets/logo/spacelike-lockup.svg create mode 100644 site/assets/logo/spacelike-mark.svg create mode 100644 site/assets/logo/swap-lockup.svg create mode 100644 site/assets/logo/swap-mark.svg create mode 100644 site/directions.js create mode 100644 site/index.html create mode 100644 site/styles.css diff --git a/scripts/brand/directions.py b/scripts/brand/directions.py new file mode 100644 index 0000000..c2edbe7 --- /dev/null +++ b/scripts/brand/directions.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Generates the Phase 1 logo concepts for the Entangle site, one per brand direction. + + python3 scripts/brand/directions.py + +Writes into site/assets/logo/ (committed): + -mark.svg the symbol alone on a 48 x 32 grid + -lockup.svg symbol + "entangle" wordmark (Inter 600, from the site's own font) + +Standard library only. Every shape is computed here so a chosen direction can be refined by +changing a constant and re-running, never by hand-editing paths. Colors are the direction's +palette tokens in site/styles.css; keep the two in step. +""" + +import math +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "site/assets/logo" + +# Palette tokens, mirrored from site/styles.css ([data-direction="…"]). +PALETTES = { + "singlet": {"ink": "#1A1A1F", "a": "#C8431F", "b": "#176B6B"}, + "spacelike": {"ink": "#ECEBE6", "a": "#F0B23A", "b": "#F0B23A"}, + "swap": {"ink": "#EFEBE0", "a": "#D7935B", "b": "#D7935B"}, + "bound": {"ink": "#0F1B1E", "a": "#0F7A5A", "b": "#9AA7AC"}, +} + + +def f(v): + return ("%.2f" % v).rstrip("0").rstrip(".") + + +def half_disc(cx, cy, r, angle_deg, side, fill): + """One half of a disc, cut by a diameter at angle_deg; side = +1 or -1 picks the half.""" + a = math.radians(angle_deg) + dx, dy = math.cos(a) * r, math.sin(a) * r + x1, y1 = cx + dx, cy + dy + x2, y2 = cx - dx, cy - dy + sweep = 1 if side > 0 else 0 + return '' % ( + f(x1), f(y1), f(r), f(r), sweep, f(x2), f(y2), fill) + + +def singlet(p): + """Two particles in a spin singlet: measured along the same axis they are always opposite. + Each disc is cut by the same measurement axis; the filled halves are on opposite sides.""" + r, ang = 10, -32 + out = [] + for cx, side, col in ((13, +1, p["a"]), (35, -1, p["b"])): + out.append('' % (cx, r, p["ink"])) + out.append(half_disc(cx, 16, r - 0.75, ang, side, col)) + return "\n".join(out) + + +def spacelike(p): + """Two events outside each other's light cone. The cone is the X of a spacetime diagram + (time up); the two dots sit on one time slice, where no signal can have passed between them.""" + out = ['' % p["ink"]] + for cx in (5, 43): + out.append('' % (cx, p["a"])) + return "\n".join(out) + + +def swap(p): + """Entanglement swapping: two short-range pairs (A-R, R-C) become one long-range pair (A-C) + after a measurement at R, which is left holding nothing. The repeater in one glyph.""" + y = 22 + out = [] + out.append('' % (y, y, y, p["ink"])) + out.append('' % (y, y, p["a"])) + out.append('' % (y, p["a"])) + out.append('' % (y, p["a"])) + out.append('' % (y, p["ink"])) + return "\n".join(out) + + +def bound(p): + """Bell's theorem as a plot: the quantum correlation (a cosine) rising above the straight + line that is the most any local hidden-variable model can reach.""" + out = [] + out.append('' % p["b"]) + # -cos sampled; a smooth cosine from 5 to 43 peaking at 24. + pts = [] + for i in range(0, 41): + t = i / 40 + x = 5 + 38 * t + y = 27 - 23 * (0.5 - 0.5 * math.cos(2 * math.pi * t)) + pts.append("%s%s %s" % ("M" if i == 0 else "L", f(x), f(y))) + out.append('' % (" ".join(pts), p["a"])) + return "\n".join(out) + + +MARKS = {"singlet": singlet, "spacelike": spacelike, "swap": swap, "bound": bound} + + +def svg(w, h, body, extra=""): + return ('\n%s\n\n' + % (w, h, w, h, extra, body)) + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + for name, fn in MARKS.items(): + p = PALETTES[name] + mark = fn(p) + (OUT / ("%s-mark.svg" % name)).write_text(svg(48, 32, mark, ' role="img" aria-label="Entangle"')) + word = ('entangle' % p["ink"]) + (OUT / ("%s-lockup.svg" % name)).write_text(svg(152, 32, mark + "\n" + word, ' role="img" aria-label="Entangle"')) + print("wrote", name) + + +if __name__ == "__main__": + main() diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..848ef0d --- /dev/null +++ b/site/README.md @@ -0,0 +1,37 @@ +# Entangle website + +Static site for https://daileng.github.io/entangle/ (GitHub Pages, `/entangle/` base path; all +asset references are relative so a custom domain needs no changes). No build step, no framework, +no third-party requests: Inter is self-hosted from `assets/fonts/`, Lucide glyphs are inlined in +`index.html`. + +Type scale, spacing, radii and icon rules follow the tachyon design system +(DailenG/tachyon, `docs/design/system/`). The palette and the mark are Entangle's own. + +## Phase 1: directions + +`index.html` currently shows four switchable brand directions. Switching changes the palette +(`html[data-direction]` in `styles.css`), the header mark, the hero copy and the hero animation +(`directions.js`). Desktop: the labelled "Direction" pill in the bottom-right corner. Phones: the +pill under the hero visual, or tap the visual. Keys `1`–`4` switch, `Q` toggles quarter speed. +Query parameters: `?d=&speed=quarter`, plus `&t=&picker=open` for captures. + +| # | id | Physics | Palette (role: hex) | +|---|----|---------|---------------------| +| 1 | `singlet` | Spin singlet: the same axis measured on both particles always gives opposite results | bg `#F5F1E8`, ink `#1A1A1F`, body `#44454D`, muted `#6B6C75`, A `#C8431F`, B `#176B6B` | +| 2 | `spacelike` | Minkowski diagram: two simultaneous outcomes whose light cones have not met | bg `#0F1115`, ink `#ECEBE6`, body `#B4B6BC`, muted `#868A93`, trace `#F0B23A` | +| 3 | `swap` | Entanglement swapping: a Bell measurement at the repeater entangles A and C, which never met | bg `#1F2B27`, chalk `#EFEBE0`, body `#C7C3B7`, muted `#9A988D`, copper `#D7935B`, slate `#8FB8C9` | +| 4 | `bound` | Bell's inequality: measured correlation E(θ) rising above the local-realist straight line | bg `#F2F5F6`, ink `#0F1B1E`, body `#3C4A4F`, muted `#627179`, trace `#0F7A5A`, bound `#9AA7AC` | + +Marks are generated by `scripts/brand/directions.py` into `assets/logo/`; the header draws the +same geometry inline so it recolors with the direction. + +Motion respects `prefers-reduced-motion` (a still frame is drawn instead) and pauses when the +canvas is off-screen or the tab is hidden. + +## Local preview + +```sh +mkdir -p /tmp/srv && ln -sfn "$PWD/site" /tmp/srv/entangle && (cd /tmp/srv && python3 -m http.server 8765) +# open http://localhost:8765/entangle/ +``` diff --git a/site/assets/fonts/Inter-LICENSE.txt b/site/assets/fonts/Inter-LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/site/assets/fonts/Inter-LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/fonts/inter-latin.woff2 b/site/assets/fonts/inter-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d15208de03cd1ad7c5199f0a0ce915fe841e4722 GIT binary patch literal 48256 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∾Na#*TBTU(!jF?9zmR-!C*+RdvGrd0&{`| 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 + + +entangle + diff --git a/site/assets/logo/bound-mark.svg b/site/assets/logo/bound-mark.svg new file mode 100644 index 0000000..17e58d6 --- /dev/null +++ b/site/assets/logo/bound-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/site/assets/logo/singlet-lockup.svg b/site/assets/logo/singlet-lockup.svg new file mode 100644 index 0000000..d9030a7 --- /dev/null +++ b/site/assets/logo/singlet-lockup.svg @@ -0,0 +1,7 @@ + + + + + +entangle + diff --git a/site/assets/logo/singlet-mark.svg b/site/assets/logo/singlet-mark.svg new file mode 100644 index 0000000..15d453e --- /dev/null +++ b/site/assets/logo/singlet-mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/site/assets/logo/spacelike-lockup.svg b/site/assets/logo/spacelike-lockup.svg new file mode 100644 index 0000000..5edcdd6 --- /dev/null +++ b/site/assets/logo/spacelike-lockup.svg @@ -0,0 +1,6 @@ + + + + +entangle + diff --git a/site/assets/logo/spacelike-mark.svg b/site/assets/logo/spacelike-mark.svg new file mode 100644 index 0000000..c168a8a --- /dev/null +++ b/site/assets/logo/spacelike-mark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/site/assets/logo/swap-lockup.svg b/site/assets/logo/swap-lockup.svg new file mode 100644 index 0000000..a782c93 --- /dev/null +++ b/site/assets/logo/swap-lockup.svg @@ -0,0 +1,8 @@ + + + + + + +entangle + diff --git a/site/assets/logo/swap-mark.svg b/site/assets/logo/swap-mark.svg new file mode 100644 index 0000000..5b466a0 --- /dev/null +++ b/site/assets/logo/swap-mark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/site/directions.js b/site/directions.js new file mode 100644 index 0000000..caebbb0 --- /dev/null +++ b/site/directions.js @@ -0,0 +1,441 @@ +/* Entangle website, Phase 1: four switchable brand directions. + Switching changes html[data-direction] (palette), the header mark, the hero copy and the hero + animation. Picker: fixed card on desktop; pill under the hero visual, or a tap on the visual, + opens a sheet on narrow screens. Speed: normal or quarter. Reduced motion: a still frame. + Query: ?d=&speed=quarter, plus ?t=&picker=open for previews. Plain script, no dependencies. */ +(function () { + "use strict"; + + var DIRECTIONS = [ + { + id: "singlet", + name: "Singlet", + desc: "Paper and ink. Two spins measured on the same axis: always opposite.", + eyebrow: "Local-first MCP · Agent to agent", + title: "Two agents, one state.", + lead: "Entangle lets AI agents on your network find each other, pair under a human's eye, and share files and JSON over end-to-end encrypted links. Nothing in between can read them.", + mark: markSinglet, + anim: AnimSinglet + }, + { + id: "spacelike", + name: "Spacelike", + desc: "Dark bench, amber trace. Two events no signal could connect.", + eyebrow: "Encrypted agent-to-agent sync", + title: "Same state. Any distance.", + lead: "Agents on your LAN discover each other over mDNS; agents on different networks meet through a repeater you host. Either way the link is Noise-encrypted end to end.", + mark: markSpacelike, + anim: AnimSpacelike + }, + { + id: "swap", + name: "Swap", + desc: "Chalkboard and copper. Entanglement swapping, the repeater's trick.", + eyebrow: "MCP server for agent collaboration", + title: "Entangle your agents.", + lead: "Discovery on the LAN, a three-emoji check by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", + mark: markSwap, + anim: AnimSwap + }, + { + id: "bound", + name: "Bound", + desc: "Lab notebook. Bell's inequality, violated live, one pair at a time.", + eyebrow: "Agent-to-agent, measured", + title: "Share state, not secrets.", + lead: "An open-source Rust MCP server: agents find each other over mDNS, a human approves each pairing with a three-emoji check, and every sync is Noise-encrypted and SHA-256 verified.", + mark: markBound, + anim: AnimBound + } + ]; + + var root = document.documentElement; + var canvas = document.getElementById("hero-canvas"); + var caption = document.getElementById("hero-caption"); + var figure = canvas && canvas.parentNode; + var markSlot = document.querySelector(".brand-mark"); + var copyEls = { eyebrow: q("eyebrow"), title: q("title"), lead: q("lead") }; + function q(k) { return document.querySelector('[data-copy="' + k + '"]'); } + + var reduced = window.matchMedia("(prefers-reduced-motion: reduce)"); + var params = new URLSearchParams(location.search); + var store = safeStorage(); + var speed = (params.get("speed") || store.get("entangle.speed") || "normal") === "quarter" ? 0.25 : 1; + var current = null, anim = null, raf = 0, last = 0, simTime = 0, visible = true, inView = true; + + // ---------- marks (header); same geometry as scripts/brand/directions.py ---------- + function halfDisc(cx, cy, r, deg, side, fill) { + var a = deg * Math.PI / 180, dx = Math.cos(a) * r, dy = Math.sin(a) * r; + return ''; + } + function markSinglet(c) { + return '' + halfDisc(13, 16, 9.25, -32, 1, c.a) + + '' + halfDisc(35, 16, 9.25, -32, -1, c.b); + } + function markSpacelike(c) { + return '' + + ''; + } + function markSwap(c) { + return '' + + '' + + '' + + ''; + } + function markBound(c) { + var d = ""; + for (var i = 0; i <= 40; i++) { var t = i / 40; d += (i ? "L" : "M") + (5 + 38 * t).toFixed(2) + " " + (27 - 23 * (0.5 - 0.5 * Math.cos(2 * Math.PI * t))).toFixed(2) + " "; } + return '' + + ''; + } + + // ---------- helpers ---------- + function safeStorage() { + try { var s = window.localStorage; s.setItem("entangle.t", "1"); s.removeItem("entangle.t"); return { get: function (k) { return s.getItem(k); }, set: function (k, v) { s.setItem(k, v); } }; } + catch (e) { var m = {}; return { get: function (k) { return m[k] || null; }, set: function (k, v) { m[k] = v; } }; } + } + function tokens() { + var cs = getComputedStyle(root); + function v(n) { return cs.getPropertyValue(n).trim(); } + return { bg: v("--bg"), surface: v("--surface"), sunken: v("--sunken"), line: v("--line"), lineStrong: v("--line-strong"), ink: v("--ink"), body: v("--body"), muted: v("--muted"), a: v("--a"), b: v("--b") }; + } + function hash(k) { var x = Math.sin(k * 12.9898 + 78.233) * 43758.5453; return x - Math.floor(x); } + function ease(t) { t = Math.min(1, Math.max(0, t)); return 1 - Math.pow(1 - t, 3); } + function clamp01(t) { return Math.min(1, Math.max(0, t)); } + function lerp(a, b, t) { return a + (b - a) * t; } + function withAlpha(hex, a) { + var h = hex.replace("#", ""); if (h.length === 3) h = h.replace(/./g, function (ch) { return ch + ch; }); + var n = parseInt(h, 16); return "rgba(" + (n >> 16 & 255) + "," + (n >> 8 & 255) + "," + (n & 255) + "," + a + ")"; + } + function mono(px) { return (px || 12) + 'px ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'; } + function sans(px, w) { return (w || 500) + " " + px + 'px Inter, ui-sans-serif, system-ui, sans-serif'; } + function pad(n, w) { var s = String(n); while (s.length < w) s = "0" + s; return s; } + + // ---------- 1. Singlet: repeated spin measurements on a Bell pair ---------- + function AnimSinglet(ctx, c) { + this.T = 3.2; + this.draw = function (w, h, t, still) { + var k = Math.floor(t / this.T), u = (t - k * this.T) / this.T; + var angPrev = hash(k - 1) * 180 - 90, ang = hash(k) * 180 - 90; + var s = hash(k + 0.5) > 0.5 ? 1 : -1; + if (still) { u = 0.6; } + var rot = lerp(angPrev, ang, ease(u / 0.16)); + var fill = u < 0.16 ? 0 : u < 0.22 ? ease((u - 0.16) / 0.06) : u < 0.9 ? 1 : 1 - ease((u - 0.9) / 0.1); + var R = Math.min(w, h) * 0.2, cy = h * 0.44, ax = w * 0.3, bx = w * 0.7; + ctx.clearRect(0, 0, w, h); + // common measurement axis, drawn as a hairline across both + ctx.save(); ctx.strokeStyle = c.lineStrong; ctx.lineWidth = 1; ctx.setLineDash([3, 5]); + var a = rot * Math.PI / 180, L = Math.min(w, h) * 0.46; + [ax, bx].forEach(function (cx) { + ctx.beginPath(); ctx.moveTo(cx - Math.cos(a) * L / 2, cy - Math.sin(a) * L / 2); ctx.lineTo(cx + Math.cos(a) * L / 2, cy + Math.sin(a) * L / 2); ctx.stroke(); + }); + ctx.restore(); + [[ax, 1, c.a], [bx, -1, c.b]].forEach(function (d) { + var cx = d[0], side = d[1] * s, col = d[2]; + ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.lineWidth = 1.5; ctx.strokeStyle = c.ink; ctx.stroke(); + if (fill > 0) { + ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.clip(); + ctx.translate(cx, cy); ctx.rotate(a); + ctx.globalAlpha = fill; ctx.fillStyle = col; + ctx.fillRect(-R - 1, side > 0 ? -R - 1 : 0, 2 * R + 2, R + 1); + ctx.restore(); + } + }); + ctx.fillStyle = c.muted; ctx.font = sans(12, 600); ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; + ctx.fillText("A", ax, cy + R + 24); ctx.fillText("B", bx, cy + R + 24); + ctx.font = mono(11.5); ctx.fillText("axis " + pad(Math.round(rot + 90), 3) + "\u00b0", w / 2, cy - R - 10); + // ledger of recent outcomes + var n = Math.min(k + 1, 12), y0 = h * 0.87, gap = Math.min(36, (w - 48) / 12); + var x0 = w / 2 - (n - 1) * gap / 2; + ctx.font = mono(15); ctx.textAlign = "center"; + for (var i = 0; i < n; i++) { + var kk = k - n + 1 + i, so = hash(kk + 0.5) > 0.5 ? 1 : -1, alpha = kk === k ? fill : 1; + if (kk < 0) continue; + ctx.globalAlpha = alpha; ctx.fillStyle = c.a; ctx.fillText(so > 0 ? "\u2191" : "\u2193", x0 + i * gap - 6, y0); + ctx.fillStyle = c.b; ctx.fillText(so > 0 ? "\u2193" : "\u2191", x0 + i * gap + 6, y0); + } + ctx.globalAlpha = 1; + return "trial " + pad(k + 1, 4) + " \u00b7 axis " + pad(Math.round(ang + 90), 3) + "\u00b0 \u00b7 A " + (s > 0 ? "\u2191" : "\u2193") + " B " + (s > 0 ? "\u2193" : "\u2191") + " \u00b7 agreements 0 / " + (k + 1); + }; + } + + // ---------- 2. Spacelike: a Minkowski diagram; two simultaneous outcomes, light cones still on their way ---------- + function AnimSpacelike(ctx, c) { + this.P = 8; + this.draw = function (w, h, t, still) { + var narrow = w < 700; + var X = narrow ? w * 0.44 : Math.min(w * 0.39, 560), xc = w / 2; + var base = h * 0.8, spd = X / 5.5; // light crosses the midpoint 5.5 s after the pair fires + if (still) t = 4.5; + ctx.clearRect(0, 0, w, h); + // grid: equal spacing in x and t so light moves at 45 degrees + var unit = X / 4; + ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.beginPath(); + for (var y = base; y > -1; y -= unit) { ctx.moveTo(0, y); ctx.lineTo(w, y); } + for (var y2 = base + unit; y2 < h; y2 += unit) { ctx.moveTo(0, y2); ctx.lineTo(w, y2); } + for (var x = xc % unit; x < w; x += unit) { ctx.moveTo(x, 0); ctx.lineTo(x, h); } + ctx.stroke(); + // worldlines of the two machines + ctx.strokeStyle = c.lineStrong; ctx.lineWidth = 1.5; ctx.beginPath(); + ctx.moveTo(xc - X, 0); ctx.lineTo(xc - X, h); ctx.moveTo(xc + X, 0); ctx.lineTo(xc + X, h); ctx.stroke(); + ctx.fillStyle = c.muted; ctx.font = mono(12); ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; + ctx.fillText("A", xc - X, 22); ctx.fillText("B", xc + X, 22); + if (!narrow) { ctx.textAlign = "right"; ctx.fillText("x \u2192", w - 20, h - 18); ctx.fillText("t \u2191", w - 20, h - 36); } + var kNow = Math.floor(t / this.P), out = ""; + for (var k = kNow; k >= kNow - 1; k--) { + if (k < 0) continue; + var age = t - k * this.P, reach = age * spd; + var alpha = k === kNow ? Math.min(1, age / 0.4) : Math.max(0, 1 - age / this.P - 0) * 0; + if (k !== kNow) { alpha = Math.max(0, 1 - (age - this.P) / 1.2); } + if (alpha <= 0) continue; + var s = hash(k + 0.5) > 0.5 ? 1 : -1; + ctx.save(); ctx.globalAlpha = alpha; + // light cones opening upward from each event + ctx.strokeStyle = withAlpha(c.a, 0.6); ctx.lineWidth = 1; ctx.beginPath(); + ctx.moveTo(xc - X, base); ctx.lineTo(xc - X + reach, base - reach); ctx.moveTo(xc - X, base); ctx.lineTo(xc - X - reach, base - reach); + ctx.moveTo(xc + X, base); ctx.lineTo(xc + X - reach, base - reach); ctx.moveTo(xc + X, base); ctx.lineTo(xc + X + reach, base - reach); + ctx.stroke(); + // the same instant in this frame: a dotted line between the two events + ctx.setLineDash([2, 6]); ctx.strokeStyle = c.lineStrong; ctx.beginPath(); ctx.moveTo(xc - X + 10, base); ctx.lineTo(xc + X - 10, base); ctx.stroke(); ctx.setLineDash([]); + // outcomes, always opposite + ctx.fillStyle = c.a; + [[xc - X, s], [xc + X, -s]].forEach(function (e) { + ctx.beginPath(); ctx.arc(e[0], base, 4.5, 0, Math.PI * 2); ctx.fill(); + ctx.font = mono(14); ctx.textAlign = "center"; ctx.fillText(e[1] > 0 ? "\u2191" : "\u2193", e[0] + (e[0] < xc ? -18 : 18), base + 5); + }); + // milestones: the two cones meet halfway; later, light from A arrives at B's worldline + if (reach >= X) { ctx.beginPath(); ctx.arc(xc, base - X, 3, 0, Math.PI * 2); ctx.fillStyle = c.a; ctx.fill(); } + if (reach >= 2 * X && base - 2 * X > 30) { ctx.beginPath(); ctx.arc(xc + X, base - 2 * X, 3.5, 0, Math.PI * 2); ctx.fillStyle = c.ink; ctx.fill(); } + ctx.restore(); + if (k === kNow) { + var pct = Math.min(100, Math.round(100 * reach / (2 * X))); + out = "pair " + pad(k + 1, 4) + " \u00b7 A " + (s > 0 ? "\u2191" : "\u2193") + " B " + (s > 0 ? "\u2193" : "\u2191") + " \u00b7 light from A is " + pct + "% of the way to B"; + } + } + return out; + }; + } + + // ---------- 3. Swap: entanglement swapping, the repeater's one trick ---------- + function AnimSwap(ctx, c) { + this.T = 9; + function arc(ctx, x1, x2, y, lift, from, to) { + // quadratic arc sampled so it can be drawn partially + var cx = (x1 + x2) / 2, cy = y - lift, n = 48; ctx.beginPath(); + for (var i = 0; i <= n; i++) { + var u = from + (to - from) * i / n, mt = 1 - u; + var x = mt * mt * x1 + 2 * mt * u * cx + u * u * x2, yy = mt * mt * y + 2 * mt * u * cy + u * u * y; + if (i === 0) ctx.moveTo(x, yy); else ctx.lineTo(x, yy); + } + ctx.stroke(); + } + this.draw = function (w, h, t, still) { + var k = Math.floor(t / this.T), u = t - k * this.T; + if (still) u = 6; + var y = h * 0.6, ax = w * 0.12, rx = w / 2, cx = w * 0.88, r = Math.max(5, Math.min(w * 0.014, 9)); + var liftS = h * 0.42, liftL = h * 0.9; + var short = u < 1.6 ? ease(u / 1.6) : 1, shortA = u < 4.2 ? 1 : u < 5.2 ? 1 - ease((u - 4.2) / 1) : 0; + var measure = u >= 3 && u < 4.2 ? (u - 3) / 1.2 : -1; + var longDraw = u < 4.2 ? 0 : u < 5.4 ? ease((u - 4.2) / 1.2) : 1; + var fadeAll = u > 8.2 ? 1 - (u - 8.2) / 0.8 : 1; + ctx.clearRect(0, 0, w, h); + ctx.globalAlpha = fadeAll; + ctx.lineCap = "round"; + // baseline, chalk + ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.setLineDash([1, 7]); ctx.beginPath(); ctx.moveTo(ax, y); ctx.lineTo(cx, y); ctx.stroke(); ctx.setLineDash([]); + // the two short pairs + if (shortA > 0 && short > 0) { + ctx.globalAlpha = fadeAll * shortA * 0.7; ctx.strokeStyle = c.ink; ctx.lineWidth = 1.5; + arc(ctx, ax, rx, y, liftS, 0, short); arc(ctx, cx, rx, y, liftS, 0, short); + ctx.globalAlpha = fadeAll; + } + // classical bits from the repeater to both ends + if (measure >= 0) { + var p = ease(measure), bx1 = lerp(rx, ax, p), bx2 = lerp(rx, cx, p); + ctx.fillStyle = c.ink; ctx.beginPath(); ctx.arc(bx1, y, 3, 0, 7); ctx.fill(); ctx.beginPath(); ctx.arc(bx2, y, 3, 0, 7); ctx.fill(); + } + // the long pair + if (longDraw > 0) { ctx.strokeStyle = c.a; ctx.lineWidth = 2.25; arc(ctx, ax, cx, y, liftL, 0.5 - longDraw / 2, 0.5 + longDraw / 2); } + // nodes + ctx.fillStyle = c.a; ctx.beginPath(); ctx.arc(ax, y, r, 0, 7); ctx.fill(); ctx.beginPath(); ctx.arc(cx, y, r, 0, 7); ctx.fill(); + var rr = measure >= 0 ? r * (1 - 0.35 * Math.sin(measure * Math.PI)) : r; + ctx.strokeStyle = c.ink; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(rx, y, rr, 0, 7); ctx.stroke(); + if (u < 3) { ctx.fillStyle = withAlpha(c.ink, 0.35); ctx.beginPath(); ctx.arc(rx, y, rr * 0.55, 0, 7); ctx.fill(); } + // labels + ctx.fillStyle = c.muted; ctx.font = mono(12); ctx.textAlign = "center"; + ctx.fillText("A", ax, y + r + 22); ctx.fillText("repeater", rx, y + r + 22); ctx.fillText("C", cx, y + r + 22); + ctx.font = sans(13, 500); + var label = u < 3 ? "A\u2013R and R\u2013C are each entangled" : u < 4.2 ? "Bell-state measurement at R; two classical bits go to A and C" : u < 8.2 ? "A and C are now entangled; R holds nothing" : ""; + ctx.fillStyle = c.body; ctx.fillText(label, rx, h - 10); + ctx.globalAlpha = 1; + return "cycle " + pad(k + 1, 3) + " \u00b7 A\u2013R, R\u2013C \u2192 A\u2013C \u00b7 the repeater keeps two bits, never the state"; + }; + } + + // ---------- 4. Bound: Bell's inequality violated live ---------- + function AnimBound(ctx, c) { + var BINS = 36, RATE = 42, N_MAX = 1400, HOLD = 4; + this.draw = function (w, h, t, still) { + var cycle = N_MAX / RATE + HOLD, k = Math.floor(t / cycle), u = t - k * cycle; + if (still) u = N_MAX / RATE; + var n = Math.min(N_MAX, Math.floor(u * RATE)); + var fade = u > cycle - 0.6 ? (cycle - u) / 0.6 : 1; + var same = new Array(BINS).fill(0), tot = new Array(BINS).fill(0); + for (var i = 0; i < n; i++) { + var bin = Math.floor(hash(k * 7919 + i) * BINS), th = (bin + 0.5) / BINS * Math.PI; + tot[bin]++; if (hash(k * 7919 + i + 0.5) < Math.pow(Math.sin(th / 2), 2)) same[bin]++; + } + var narrow = w < 700, mL = narrow ? 44 : 56, mR = 24, mT = 36, mB = narrow ? 48 : 56; + var px = function (deg) { return mL + (w - mL - mR) * deg / 180; }, py = function (E) { return mT + (h - mT - mB) * (1 - E) / 2; }; + ctx.clearRect(0, 0, w, h); + ctx.globalAlpha = fade; + // axes and ticks + ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.beginPath(); + [-1, -0.5, 0, 0.5, 1].forEach(function (E) { ctx.moveTo(mL, py(E)); ctx.lineTo(w - mR, py(E)); }); ctx.stroke(); + ctx.strokeStyle = c.lineStrong; ctx.beginPath(); ctx.moveTo(mL, py(0)); ctx.lineTo(w - mR, py(0)); ctx.moveTo(mL, mT); ctx.lineTo(mL, h - mB); ctx.stroke(); + ctx.fillStyle = c.muted; ctx.font = mono(11.5); ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; + [0, 45, 90, 135, 180].forEach(function (d) { ctx.fillText(d + "\u00b0", px(d), h - mB + 18); }); + ctx.textAlign = "right"; [-1, 0, 1].forEach(function (E) { ctx.fillText((E > 0 ? "+" : "") + E, mL - 8, py(E) + 4); }); + ctx.textAlign = "left"; ctx.fillText("E(\u03b8)", mL - 32, mT - 14); ctx.textAlign = "right"; ctx.fillText("\u03b8 between analyzers", w - mR, h - mB + 36); + // the local-realist limit: perfect anticorrelation at 0 forces a straight line + ctx.setLineDash([5, 5]); ctx.strokeStyle = c.b; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(px(0), py(-1)); ctx.lineTo(px(180), py(1)); ctx.stroke(); ctx.setLineDash([]); + // quantum prediction + ctx.strokeStyle = withAlpha(c.a, 0.35); ctx.lineWidth = 1.5; ctx.beginPath(); + for (var d = 0; d <= 180; d += 2) { var yq = py(-Math.cos(d * Math.PI / 180)); if (d === 0) ctx.moveTo(px(d), yq); else ctx.lineTo(px(d), yq); } + ctx.stroke(); + // measured points with error bars + for (var b = 0; b < BINS; b++) { + if (!tot[b]) continue; + var E = (2 * same[b] - tot[b]) / tot[b], se = Math.min(1, 1 / Math.sqrt(tot[b])), xd = (b + 0.5) / BINS * 180; + ctx.strokeStyle = c.a; ctx.lineWidth = 1.25; ctx.beginPath(); ctx.moveTo(px(xd), py(Math.max(-1, E - se))); ctx.lineTo(px(xd), py(Math.min(1, E + se))); ctx.stroke(); + ctx.fillStyle = c.a; ctx.beginPath(); ctx.arc(px(xd), py(E), 2.75, 0, 7); ctx.fill(); + } + // the latest pair, as a tick at +1 or -1 + if (n > 0 && n < N_MAX) { + var lb = Math.floor(hash(k * 7919 + n - 1) * BINS), lth = (lb + 0.5) / BINS * Math.PI, ls = hash(k * 7919 + n - 1 + 0.5) < Math.pow(Math.sin(lth / 2), 2) ? 1 : -1; + ctx.strokeStyle = c.ink; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px((lb + 0.5) / BINS * 180), py(ls) - 5); ctx.lineTo(px((lb + 0.5) / BINS * 180), py(ls) + 5); ctx.stroke(); + } + // legend + ctx.font = sans(12.5, 500); ctx.textAlign = "left"; + var lx = px(8), ly = py(0.88); + ctx.strokeStyle = c.a; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 22, ly); ctx.stroke(); + ctx.fillStyle = c.body; ctx.fillText("measured \u00b7 quantum: \u2212cos \u03b8", lx + 30, ly + 4); + ctx.setLineDash([5, 5]); ctx.strokeStyle = c.b; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(lx, ly + 22); ctx.lineTo(lx + 22, ly + 22); ctx.stroke(); ctx.setLineDash([]); + ctx.fillText("any local hidden-variable model", lx + 30, ly + 26); + ctx.globalAlpha = 1; + var viol = 0; [8, 9, 26, 27].forEach(function (b) { if (tot[b] > 25) { var E = (2 * same[b] - tot[b]) / tot[b], lim = -1 + 2 * (b + 0.5) / BINS; if (Math.abs(E - lim) > 1 / Math.sqrt(tot[b]) && Math.abs(E) > Math.abs(lim)) viol++; } }); + return "n = " + n.toLocaleString("en-US") + " pairs \u00b7 " + BINS + " settings \u00b7 bound exceeded at " + viol + " of 4 checked angles"; + }; + } + + // ---------- engine ---------- + var ctx = canvas.getContext("2d"); + function size() { + var r = canvas.getBoundingClientRect(), dpr = Math.min(2, window.devicePixelRatio || 1); + var W = Math.max(1, Math.round(r.width * dpr)), H = Math.max(1, Math.round(r.height * dpr)); + if (canvas.width !== W || canvas.height !== H) { canvas.width = W; canvas.height = H; } + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return { w: r.width, h: r.height }; + } + function frame(now) { + raf = 0; + if (!anim) return; + var dt = last ? Math.min(0.1, (now - last) / 1000) : 0; last = now; + simTime += dt * speed; + var s = size(); + var text = anim.draw(s.w, s.h, simTime, false); + if (text !== undefined && caption.textContent !== text) caption.textContent = text; + if (!reduced.matches && visible && inView) raf = requestAnimationFrame(frame); + } + function drawStill() { + var s = size(); var text = anim.draw(s.w, s.h, 0, true); if (text !== undefined) caption.textContent = text; + } + function start() { + if (raf) cancelAnimationFrame(raf); raf = 0; last = 0; + if (reduced.matches) { drawStill(); return; } + if (visible && inView) raf = requestAnimationFrame(frame); + } + function apply(id, push) { + var d = DIRECTIONS.filter(function (x) { return x.id === id; })[0] || DIRECTIONS[0]; + current = d; + root.setAttribute("data-direction", d.id); + var c = tokens(); + markSlot.innerHTML = d.mark(c); + copyEls.eyebrow.textContent = d.eyebrow; copyEls.title.textContent = d.title; copyEls.lead.textContent = d.lead; + anim = new d.anim(ctx, c); + simTime = parseFloat(params.get("t")) || 0; + store.set("entangle.direction", d.id); + document.querySelectorAll(".dir-toggle strong, .dir-pill strong").forEach(function (el) { el.textContent = d.name; }); + var radio = document.getElementById("dir-" + d.id); if (radio) radio.checked = true; + if (push) { var p = new URLSearchParams(location.search); p.set("d", d.id); if (speed !== 1) p.set("speed", "quarter"); else p.delete("speed"); history.replaceState(null, "", location.pathname + "?" + p.toString() + location.hash); } + start(); + } + function setSpeed(v, push) { + speed = v; store.set("entangle.speed", v === 1 ? "normal" : "quarter"); + if (push) { var p = new URLSearchParams(location.search); if (v !== 1) p.set("speed", "quarter"); else p.delete("speed"); var qs = p.toString(); history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash); } + } + + // ---------- picker ---------- + function buildPicker() { + var picker = document.createElement("aside"); + picker.className = "dir-picker"; picker.id = "dir-picker"; picker.setAttribute("aria-label", "Brand direction picker"); + var html = '

Direction

' + + '

Phase 1 preview. Switches palette, mark, copy and hero.

'; + DIRECTIONS.forEach(function (d, i) { + html += '' + + ''; + }); + html += '
Speed' + + '' + + '
' + + '

Keys 1\u20134 switch direction, Q toggles speed. Reduced-motion users get a still frame.

'; + picker.innerHTML = html; + var backdrop = document.createElement("div"); backdrop.className = "dir-backdrop"; + var corner = document.createElement("div"); corner.className = "dir-corner"; + var pill = document.createElement("button"); + pill.type = "button"; pill.className = "dir-pill"; pill.setAttribute("aria-controls", "dir-picker"); pill.setAttribute("aria-expanded", "false"); + pill.innerHTML = 'Direction: 1\u20134'; + corner.appendChild(picker); corner.appendChild(pill); + document.body.appendChild(backdrop); document.body.appendChild(corner); + + var toggle = document.createElement("button"); + toggle.type = "button"; toggle.className = "dir-toggle"; toggle.setAttribute("aria-controls", "dir-picker"); toggle.setAttribute("aria-expanded", "false"); + toggle.innerHTML = 'Direction: '; + figure.parentNode.insertBefore(toggle, figure.nextSibling); + + function open() { picker.classList.add("is-open"); backdrop.classList.add("is-open"); toggle.setAttribute("aria-expanded", "true"); pill.setAttribute("aria-expanded", "true"); var r = picker.querySelector("input:checked"); if (r) r.focus(); } + function close() { picker.classList.remove("is-open"); backdrop.classList.remove("is-open"); toggle.setAttribute("aria-expanded", "false"); pill.setAttribute("aria-expanded", "false"); } + function isSheet() { return window.matchMedia("(max-width: 900px)").matches; } + function flip() { picker.classList.contains("is-open") ? close() : open(); } + toggle.addEventListener("click", flip); pill.addEventListener("click", flip); + if (params.get("picker") === "open") open(); + backdrop.addEventListener("click", close); + picker.querySelector(".dir-picker-close").addEventListener("click", close); + figure.setAttribute("role", "button"); figure.setAttribute("tabindex", "0"); figure.setAttribute("aria-label", "Open the direction picker"); + figure.addEventListener("click", function () { if (isSheet()) open(); }); + figure.addEventListener("keydown", function (e) { if ((e.key === "Enter" || e.key === " ") && isSheet()) { e.preventDefault(); open(); } }); + picker.addEventListener("change", function (e) { + if (e.target.name === "direction") { apply(e.target.value, true); if (isSheet()) close(); } + if (e.target.name === "speed") setSpeed(parseFloat(e.target.value), true); + }); + document.addEventListener("keydown", function (e) { + if (e.target.closest("input, textarea, [contenteditable]")) return; + if (e.key === "Escape") close(); + var n = parseInt(e.key, 10); if (n >= 1 && n <= DIRECTIONS.length) apply(DIRECTIONS[n - 1].id, true); + if (e.key === "q" || e.key === "Q") { setSpeed(speed === 1 ? 0.25 : 1, true); document.getElementById(speed === 1 ? "speed-normal" : "speed-quarter").checked = true; } + }); + document.getElementById(speed === 1 ? "speed-normal" : "speed-quarter").checked = true; + } + + // ---------- lifecycle ---------- + document.addEventListener("visibilitychange", function () { visible = !document.hidden; start(); }); + if ("IntersectionObserver" in window) { + new IntersectionObserver(function (es) { inView = es[0].isIntersecting; start(); }, { threshold: 0.05 }).observe(canvas); + } + window.addEventListener("resize", function () { if (reduced.matches) drawStill(); }); + reduced.addEventListener && reduced.addEventListener("change", start); + + buildPicker(); + var initial = params.get("d") || store.get("entangle.direction") || "singlet"; + apply(initial, false); +})(); diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..808f647 --- /dev/null +++ b/site/index.html @@ -0,0 +1,148 @@ + + + + + +Entangle: encrypted state sync between AI agents + + + + + + + + + + + + + +
+
+
+
+ Local-first MCP · Agent to agent +

Two agents, one state.

+

Entangle lets AI agents on your network find each other, pair under a human's eye, and share files and JSON over end-to-end encrypted links. Nothing in between can read them.

+ +
    +
  • v0.5.0
  • +
  • Rust, one binary
  • +
  • Windows, macOS, Linux
  • +
  • MIT or Apache-2.0
  • +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ How it works +

Discover, pair, sync.

+

Each machine runs one Entangle process as an MCP stdio server. Discovery is automatic, pairing is a human step, and every sync is encrypted end to end.

+
+
    +
  1. +

    Resonance

    +

    Particles advertise over mDNS on the local network and publish only a public pairing-group ID, never the key. Where multicast is blocked, static --peer entries do the same job.

    +
    entangle scan
    +
  2. +
  3. +

    Pairing, by a human

    +

    One person creates a pairing code, another enters it. On first contact both machines derive three emoji from a commit-then-reveal exchange; the humans compare and approve. Agents never see the code and cannot approve.

    +
    entangle pair init
    +entangle pair join <code>
    +entangle peer approve
    +
  4. +
  5. +

    State sync

    +

    An agent sends JSON or one file over a Noise XXpsk3 link on TCP 7337. The receiver checks the byte count and SHA-256 before anything reaches its inbox; the other agent observes it, then collapses it.

    +
    find_entangled_particles
    +sync_entangled_state
    +observe_entangled_states
    +
  6. +
+
+
+ +
+
+
+ Quantum Repeater +

Across networks, through a relay that cannot read.

+

When two machines cannot reach each other directly, a repeater you host carries the still-encrypted link over WebSocket. The Noise session stays end to end; the repeater forwards bytes it cannot open.

+
+
    +
  • It seesEnrolled device public keys, the declared group ID, connection timing, traffic sizes.
  • +
  • It cannot seeParticle names, pairing keys, the verification emoji, or any synced content.
  • +
  • EnrollmentA single-use invite code from the host, or a join request the host approves after comparing emoji. Devices pin the repeater's static key; revocation by fingerprint.
  • +
  • Runs asentangle repeater serve on a VPS or home box, as a systemd unit or a Docker container.
  • +
  • Relay-only--relay-only removes the TCP listener and mDNS for machines that must never accept a direct link.
  • +
+
+
+ +
+
+
+ Install +

One binary, no administrator rights.

+

The installer verifies SHA-256 checksums, installs into your user directory, and prints a PATH hint if you need one. Then register it with your MCP client.

+
+
+

macOS and Linux

curl -fsSL https://raw.githubusercontent.com/DailenG/entangle/main/install.sh | sh
+

Windows PowerShell

irm https://raw.githubusercontent.com/DailenG/entangle/main/install.ps1 | iex
+

Claude Code

claude mcp add entangle -- entangle serve --context project-x
+

Any stdio MCP client

{ "mcpServers": { "entangle": {
+    "command": "/ABSOLUTE/PATH/TO/entangle",
+    "args": ["serve", "--context", "project-x"] } } }
+
+
+
+
+ + + + + + diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..bf19652 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,371 @@ +/* Entangle website, Phase 1: four brand directions on one page. Typography, spacing, radii and + icon rules follow the tachyon design system (DailenG/tachyon, docs/design/system/); the + palettes and marks are Entangle's own and switch with html[data-direction]. No gradients, no + glows, no third-party requests. */ + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url("assets/fonts/inter-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + --container: 1180px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --dur-fast: 120ms; + --dur-base: 180ms; + --header-h: 68px; +} + +/* 1. Singlet: warm paper, ink, and the two anti-correlated outcomes as complementary hues. */ +[data-direction="singlet"] { + color-scheme: light; + --bg: #f5f1e8; + --surface: #fbf9f4; + --sunken: #ece7db; + --line: #e0d9ca; + --line-strong: #c9c0ad; + --ink: #1a1a1f; + --body: #44454d; + --muted: #6b6c75; + --a: #c8431f; + --b: #176b6b; + --link: #176b6b; + --btn-bg: #1a1a1f; + --btn-fg: #f5f1e8; + --header-bg: rgba(245, 241, 232, 0.9); + --selection: rgba(23, 107, 107, 0.2); + --eyebrow: #c8431f; +} + +/* 2. Spacelike: an instrument on a dark bench, warm white and one amber trace. */ +[data-direction="spacelike"] { + color-scheme: dark; + --bg: #0f1115; + --surface: #15181d; + --sunken: #1b1f26; + --line: #262b33; + --line-strong: #39404a; + --ink: #ecebe6; + --body: #b4b6bc; + --muted: #868a93; + --a: #f0b23a; + --b: #f0b23a; + --link: #f0b23a; + --btn-bg: #f0b23a; + --btn-fg: #0f1115; + --header-bg: rgba(15, 17, 21, 0.86); + --selection: rgba(240, 178, 58, 0.3); + --eyebrow: #f0b23a; +} + +/* 3. Swap: a lecture-hall chalkboard, chalk text, copper for the entangled pair. */ +[data-direction="swap"] { + color-scheme: dark; + --bg: #1f2b27; + --surface: #243330; + --sunken: #1a2522; + --line: #34443e; + --line-strong: #4a5c55; + --ink: #efebe0; + --body: #c7c3b7; + --muted: #9a988d; + --a: #d7935b; + --b: #8fb8c9; + --link: #d7935b; + --btn-bg: #efebe0; + --btn-fg: #1f2b27; + --header-bg: rgba(31, 43, 39, 0.88); + --selection: rgba(215, 147, 91, 0.3); + --eyebrow: #d7935b; +} + +/* 4. Bound: a lab notebook plot, cool white, graphite, one green trace. */ +[data-direction="bound"] { + color-scheme: light; + --bg: #f2f5f6; + --surface: #ffffff; + --sunken: #e8edef; + --line: #d9e0e3; + --line-strong: #b9c4c9; + --ink: #0f1b1e; + --body: #3c4a4f; + --muted: #627179; + --a: #0f7a5a; + --b: #9aa7ac; + --link: #0f7a5a; + --btn-bg: #0f7a5a; + --btn-fg: #ffffff; + --header-bg: rgba(242, 245, 246, 0.9); + --selection: rgba(15, 122, 90, 0.2); + --eyebrow: #0f7a5a; +} + +/* Base */ +*, *::before, *::after { box-sizing: border-box; } +html { scroll-behavior: smooth; scroll-padding-top: 84px; } +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + transition: background var(--dur-base), color var(--dur-base); +} +h1, h2, h3 { color: var(--ink); font-weight: 600; letter-spacing: -0.01em; text-wrap: balance; margin: 0; } +p { text-wrap: pretty; margin: 0; } +a { color: var(--link); text-decoration-thickness: 1.5px; text-underline-offset: 2px; } +a:hover { color: var(--ink); } +::selection { background: var(--selection); } +:focus-visible { outline: 2px solid var(--a); outline-offset: 2px; } +img { max-width: 100%; height: auto; } +code { + font-family: var(--font-mono); + font-size: 0.88em; + background: var(--sunken); + border: 1px solid var(--line); + border-radius: 4px; + padding: 0.05em 0.35em; +} +pre { + margin: 0; + padding: 14px 18px; + font-family: var(--font-mono); + font-size: 13.5px; + line-height: 1.6; + color: var(--ink); + background: var(--sunken); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + overflow-x: auto; +} +.sprite { display: none; } +.icon { + width: 1.15em; height: 1.15em; flex: none; + fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; +} +.wrap { max-width: var(--container); margin: 0 auto; padding: 0 24px; } +.skip-link { + position: absolute; left: 1rem; top: -4rem; padding: 0.6rem 1rem; z-index: 100; + background: var(--surface); color: var(--ink); border: 1px solid var(--line-strong); + border-radius: var(--radius-sm); font-weight: 600; +} +.skip-link:focus { top: 1rem; } +.mono { font-family: var(--font-mono); font-size: 13px; letter-spacing: 0; } + +/* Buttons (tachyon Button sizes; solid colors only) */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + height: 44px; padding: 0 20px; border-radius: var(--radius-md); + border: 1px solid transparent; font: 600 16px/1 var(--font-sans); letter-spacing: -0.005em; + text-decoration: none; white-space: nowrap; cursor: pointer; + transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast), color var(--dur-fast), + transform var(--dur-fast) var(--ease-out); +} +.btn-sm { height: 36px; padding: 0 14px; border-radius: 10px; font-size: 14px; gap: 6px; } +.btn-lg { height: 52px; padding: 0 26px; border-radius: 14px; font-size: 17px; gap: 10px; } +.btn-primary { background: var(--btn-bg); color: var(--btn-fg); } +.btn-primary:hover { color: var(--btn-fg); transform: translateY(-1px); } +.btn-primary:active { transform: none; } +.btn-secondary { background: transparent; color: var(--ink); border-color: var(--line-strong); } +.btn-secondary:hover { color: var(--ink); border-color: var(--ink); } + +/* Header: the one translucent surface */ +.site-header { + position: sticky; top: 0; z-index: 20; + background: var(--header-bg); + border-bottom: 1px solid var(--line); + -webkit-backdrop-filter: saturate(1.2) blur(8px); backdrop-filter: saturate(1.2) blur(8px); +} +.site-header .wrap { height: var(--header-h); display: flex; align-items: center; justify-content: space-between; gap: 16px; } +.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; } +.brand svg { width: 42px; height: 28px; display: block; } +.brand .wordmark { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; } +.main-nav { display: flex; align-items: center; gap: 26px; } +.main-nav a:not(.btn) { font-size: 15px; font-weight: 500; color: var(--body); text-decoration: none; } +.main-nav a:not(.btn):hover { color: var(--ink); } +@media (max-width: 760px) { .main-nav a:not(.btn) { display: none; } } + +/* Eyebrow: 12px 600 uppercase +16% (design system) */ +.eyebrow { + display: block; font-size: 12px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; + color: var(--eyebrow); margin-bottom: 16px; +} + +/* Hero: a generic copy block and a visual block; each direction lays them out differently. */ +.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); } +.hero-inner { position: relative; display: grid; gap: 48px; padding-top: 72px; padding-bottom: 64px; } +.hero-title { font-size: clamp(42px, 5.4vw, 68px); line-height: 1.04; letter-spacing: -0.025em; margin: 0 0 20px; } +.hero-lead { font-size: 20px; line-height: 1.5; color: var(--body); max-width: 56ch; margin: 0 0 32px; } +.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 28px; } +.hero-facts { display: flex; flex-wrap: wrap; gap: 6px 18px; padding: 0; margin: 0; list-style: none; color: var(--muted); font-size: 14px; } +.hero-facts li { display: inline-flex; align-items: center; gap: 8px; } +.hero-facts li + li::before { content: ""; width: 3px; height: 3px; border-radius: 50%; background: var(--line-strong); } +.hero-visual { position: relative; min-width: 0; } +.hero-figure { position: relative; margin: 0; } +.hero-canvas { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; border-radius: var(--radius-lg); touch-action: manipulation; } +.hero-caption { + margin: 12px 0 0; font-family: var(--font-mono); font-size: 12.5px; color: var(--muted); + font-variant-numeric: tabular-nums; min-height: 1.5em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.hero-figure[role="button"] { cursor: pointer; } + +/* Singlet: two columns, visual framed like a lab card */ +[data-direction="singlet"] .hero-inner { grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); align-items: center; } +[data-direction="singlet"] .hero-canvas { background: var(--surface); border: 1px solid var(--line); } + +/* Spacelike: the diagram fills the hero; copy sits centered on top of it */ +[data-direction="spacelike"] .hero { min-height: calc(100svh - var(--header-h)); display: grid; align-items: center; } +[data-direction="spacelike"] .hero-inner { position: static; grid-template-columns: 1fr; justify-items: center; text-align: center; padding-top: 48px; padding-bottom: 48px; } +[data-direction="spacelike"] .hero-copy { max-width: 760px; position: relative; z-index: 1; } +[data-direction="spacelike"] .hero-lead { margin-left: auto; margin-right: auto; } +[data-direction="spacelike"] .hero-actions, [data-direction="spacelike"] .hero-facts { justify-content: center; } +[data-direction="spacelike"] .hero-visual { position: absolute; inset: 0; z-index: 0; } +[data-direction="spacelike"] .hero-figure { position: absolute; inset: 0; } +[data-direction="spacelike"] .hero-canvas { position: absolute; inset: 0; width: 100%; height: 100%; aspect-ratio: auto; border-radius: 0; } +[data-direction="spacelike"] .hero-caption { position: absolute; left: 24px; bottom: 18px; margin: 0; z-index: 1; max-width: calc(100% - 48px); } + +/* Swap: centered copy, then a wide chalk band */ +[data-direction="swap"] .hero-inner { grid-template-columns: 1fr; justify-items: center; text-align: center; gap: 20px; padding-top: 56px; padding-bottom: 40px; } +[data-direction="swap"] .hero-copy { max-width: 720px; } +[data-direction="swap"] .hero-lead { margin-left: auto; margin-right: auto; } +[data-direction="swap"] .hero-actions, [data-direction="swap"] .hero-facts { justify-content: center; } +[data-direction="swap"] .hero-visual { width: 100%; } +[data-direction="swap"] .hero-canvas { aspect-ratio: 21 / 6; border-radius: 0; } +[data-direction="swap"] .hero-caption { text-align: center; } + +/* Bound: the plot takes the wider column */ +[data-direction="bound"] .hero-inner { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); align-items: center; } +[data-direction="bound"] .hero-canvas { aspect-ratio: 16 / 11; background: var(--surface); border: 1px solid var(--line); } + +@media (max-width: 900px) { + .hero-inner, [data-direction="singlet"] .hero-inner, [data-direction="bound"] .hero-inner, [data-direction="swap"] .hero-inner { grid-template-columns: 1fr; gap: 24px; padding-top: 36px; padding-bottom: 36px; } + .hero-title { font-size: clamp(36px, 10vw, 48px); margin-bottom: 14px; } + .hero-lead { font-size: 17px; margin-bottom: 20px; } + .hero-actions { margin-bottom: 16px; } + .eyebrow { margin-bottom: 12px; } + .hero-actions .btn-lg { height: 46px; padding: 0 20px; font-size: 16px; } + [data-direction="swap"] .hero-canvas { aspect-ratio: 4 / 3; } + [data-direction="spacelike"] .hero { min-height: calc(100svh - var(--header-h)); } + [data-direction="spacelike"] .hero-inner { padding-top: 56px; padding-bottom: 72px; } + [data-direction="spacelike"] .hero-caption { bottom: 12px; font-size: 11.5px; } +} + +/* Sections */ +section { padding: 88px 0; border-bottom: 1px solid var(--line); } +.section-head { max-width: 680px; margin-bottom: 40px; } +.section-head h2 { font-size: clamp(30px, 3.4vw, 40px); line-height: 1.12; letter-spacing: -0.02em; margin-bottom: 14px; } +.section-head p { color: var(--body); font-size: 17px; } + +.steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; counter-reset: step; list-style: none; margin: 0; padding: 0; } +.steps li { position: relative; padding: 24px 0 0; border-top: 1px solid var(--line-strong); } +.steps li::before { + counter-increment: step; content: "0" counter(step); + display: block; font-family: var(--font-mono); font-size: 12.5px; color: var(--eyebrow); letter-spacing: 0.08em; margin-bottom: 14px; +} +.steps h3 { font-size: 20px; margin-bottom: 8px; } +.steps p { color: var(--body); font-size: 15.5px; margin-bottom: 14px; } +.steps pre { font-size: 12.5px; } +@media (max-width: 900px) { .steps { grid-template-columns: 1fr; } } + +.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 48px; align-items: start; } +.split .section-head { margin-bottom: 0; } +.facts { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line-strong); } +.facts li { display: grid; grid-template-columns: 150px 1fr; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--line); font-size: 15px; color: var(--body); } +.facts li strong { color: var(--ink); font-weight: 600; } +@media (max-width: 900px) { .split { grid-template-columns: 1fr; gap: 32px; } .facts li { grid-template-columns: 1fr; gap: 4px; } } + +.install-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; } +.install-grid > div { min-width: 0; } +.install-grid h3 { font-size: 15px; font-weight: 600; margin-bottom: 10px; color: var(--muted); } +@media (max-width: 900px) { .install-grid { grid-template-columns: 1fr; } } + +.site-footer { padding: 36px 0 48px; color: var(--muted); font-size: 14px; } +.site-footer .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 24px; align-items: center; } +.site-footer a { color: var(--body); text-decoration: none; } +.site-footer a:hover { color: var(--ink); } +.site-footer nav { display: flex; gap: 20px; flex-wrap: wrap; } + +/* Direction picker (directions.js). Desktop: a labelled pill fixed to the bottom-right corner opens a + card above it. Narrow screens: a pill under the hero visual, or a tap on the visual, opens a bottom sheet. */ +.dir-corner { position: fixed; right: 20px; bottom: 20px; z-index: 50; } +.dir-pill { + display: inline-flex; align-items: center; gap: 8px; min-height: 38px; padding: 0 14px 0 12px; + border: 1px solid var(--line-strong); border-radius: 999px; background: var(--surface); color: var(--body); + font: inherit; font-size: 13px; cursor: pointer; box-shadow: 0 8px 24px -14px rgba(0, 0, 0, 0.4); +} +.dir-pill:hover { border-color: var(--ink); } +.dir-pill strong { color: var(--ink); font-weight: 600; } +.dir-pill .icon { width: 16px; height: 16px; color: var(--eyebrow); } +.dir-pill kbd { font-family: var(--font-mono); font-size: 11px; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; margin-left: 2px; } +.dir-picker { + display: none; position: absolute; right: 0; bottom: calc(100% + 10px); width: 300px; + padding: 14px 16px 16px; background: var(--surface); color: var(--ink); + border: 1px solid var(--line-strong); border-radius: var(--radius-md); + box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.35); + font-size: 13px; +} +.dir-picker.is-open { display: block; } +.dir-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 2px; } +.dir-picker h2 { font-size: 12px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--eyebrow); margin: 0; } +.dir-picker-sub { color: var(--muted); margin: 0 0 10px; font-size: 12.5px; } +.dir-picker-close { display: inline-grid; place-items: center; width: 32px; height: 32px; margin: -6px -8px -6px 0; border: 0; border-radius: 999px; background: none; font: inherit; color: var(--muted); cursor: pointer; } +.dir-picker-close:hover { color: var(--ink); background: var(--sunken); } +.dir-picker-close .icon { width: 18px; height: 18px; } +.dir-options { display: grid; gap: 2px; } +.dir-options input, .dir-speed input { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } +.dir-options label { + display: grid; grid-template-columns: 22px 1fr; column-gap: 10px; align-items: center; + min-height: 36px; padding: 5px 8px 5px 5px; border-radius: var(--radius-sm); color: var(--body); cursor: pointer; +} +.dir-options label:hover { background: var(--sunken); } +.dir-options label .dir-name { font-weight: 600; color: var(--ink); font-size: 13.5px; } +.dir-options label .dir-desc { grid-column: 2; color: var(--muted); font-size: 12px; line-height: 1.4; } +.dir-num { + display: inline-grid; place-items: center; width: 22px; height: 22px; border: 1px solid var(--line-strong); + border-radius: 999px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ink); +} +.dir-options input:checked + label { background: var(--sunken); } +.dir-options input:checked + label .dir-num { background: var(--btn-bg); color: var(--btn-fg); border-color: transparent; } +.dir-options input:focus-visible + label { outline: 2px solid var(--a); outline-offset: 1px; } +.dir-speed { display: flex; align-items: center; gap: 4px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); } +.dir-speed-label { color: var(--muted); margin-right: auto; } +.dir-speed label { padding: 5px 10px; border-radius: 999px; border: 1px solid var(--line-strong); color: var(--body); cursor: pointer; font-size: 12.5px; } +.dir-speed input:checked + label { background: var(--btn-bg); color: var(--btn-fg); border-color: transparent; } +.dir-speed input:focus-visible + label { outline: 2px solid var(--a); outline-offset: 1px; } +.dir-picker-note { margin: 10px 0 0; font-size: 12px; color: var(--muted); line-height: 1.45; } +.dir-toggle { display: none; } +.dir-backdrop { display: none; } +@media (max-width: 900px) { + .dir-corner { position: static; } + .dir-pill { display: none; } + .dir-toggle { + display: inline-flex; align-items: center; gap: 8px; margin: 14px auto 0; padding: 0 14px; min-height: 38px; + border: 1px solid var(--line-strong); border-radius: 999px; background: var(--surface); font: inherit; font-size: 13px; color: var(--body); cursor: pointer; + } + .dir-toggle strong { color: var(--ink); font-weight: 600; } + .dir-toggle .icon { width: 16px; height: 16px; color: var(--eyebrow); } + [data-direction="spacelike"] .dir-toggle { position: absolute; right: 20px; bottom: 14px; z-index: 2; margin: 0; } + [data-direction="spacelike"] .hero-caption { max-width: calc(100% - 200px); } + .dir-picker { + position: fixed; left: 0; right: 0; bottom: 0; width: auto; max-width: 560px; margin: 0 auto; z-index: 60; + padding: 16px 20px calc(18px + env(safe-area-inset-bottom)); border-radius: var(--radius-lg) var(--radius-lg) 0 0; + } + .dir-options label { min-height: 44px; } + .dir-backdrop.is-open { display: block; position: fixed; inset: 0; z-index: 55; background: rgba(0, 0, 0, 0.45); } +} +@media (prefers-reduced-motion: reduce) { + * { animation-duration: 0s !important; transition-duration: 0s !important; } + html { scroll-behavior: auto; } +} From c9a4b880d767a660e41861106ead45532c649e9d Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 01:26:47 +0000 Subject: [PATCH 02/10] site: keep Escape and 1-4/Q shortcuts working while picker radios are focused Co-Authored-By: Dailen Gunter --- site/directions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/directions.js b/site/directions.js index caebbb0..19d8180 100644 --- a/site/directions.js +++ b/site/directions.js @@ -419,8 +419,8 @@ if (e.target.name === "speed") setSpeed(parseFloat(e.target.value), true); }); document.addEventListener("keydown", function (e) { - if (e.target.closest("input, textarea, [contenteditable]")) return; - if (e.key === "Escape") close(); + if (e.key === "Escape") { close(); return; } + if (e.target.closest("textarea, select, [contenteditable], input:not([type=radio]):not([type=checkbox]):not([type=button])")) return; var n = parseInt(e.key, 10); if (n >= 1 && n <= DIRECTIONS.length) apply(DIRECTIONS[n - 1].id, true); if (e.key === "q" || e.key === "Q") { setSpeed(speed === 1 ? 0.25 : 1, true); document.getElementById(speed === 1 ? "speed-normal" : "speed-quarter").checked = true; } }); From 04d5d51b57852392b7cdae38cbd40a45b0c626ea Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 01:34:37 +0000 Subject: [PATCH 03/10] site: bound sheet height, desktop figure not a button, legend plate, caption/footer clearance Co-Authored-By: Dailen Gunter --- site/directions.js | 11 +++++++++-- site/styles.css | 8 +++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/site/directions.js b/site/directions.js index 19d8180..7dd1acc 100644 --- a/site/directions.js +++ b/site/directions.js @@ -316,8 +316,10 @@ ctx.strokeStyle = c.ink; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px((lb + 0.5) / BINS * 180), py(ls) - 5); ctx.lineTo(px((lb + 0.5) / BINS * 180), py(ls) + 5); ctx.stroke(); } // legend - ctx.font = sans(12.5, 500); ctx.textAlign = "left"; + ctx.font = sans(narrow ? 11.5 : 12.5, 500); ctx.textAlign = "left"; var lx = px(8), ly = py(0.88); + var lw = Math.max(ctx.measureText("any local hidden-variable model").width, ctx.measureText("measured \u00b7 quantum: \u2212cos \u03b8").width) + 30; + ctx.fillStyle = c.surface; ctx.globalAlpha = fade * 0.92; ctx.fillRect(lx - 8, ly - 12, lw + 16, 46); ctx.globalAlpha = fade; ctx.strokeStyle = c.a; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 22, ly); ctx.stroke(); ctx.fillStyle = c.body; ctx.fillText("measured \u00b7 quantum: \u2212cos \u03b8", lx + 30, ly + 4); ctx.setLineDash([5, 5]); ctx.strokeStyle = c.b; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(lx, ly + 22); ctx.lineTo(lx + 22, ly + 22); ctx.stroke(); ctx.setLineDash([]); @@ -411,7 +413,12 @@ if (params.get("picker") === "open") open(); backdrop.addEventListener("click", close); picker.querySelector(".dir-picker-close").addEventListener("click", close); - figure.setAttribute("role", "button"); figure.setAttribute("tabindex", "0"); figure.setAttribute("aria-label", "Open the direction picker"); + var sheetMq = window.matchMedia("(max-width: 900px)"); + function syncFigureRole() { + if (sheetMq.matches) { figure.setAttribute("role", "button"); figure.setAttribute("tabindex", "0"); figure.setAttribute("aria-label", "Open the direction picker"); } + else { figure.removeAttribute("role"); figure.removeAttribute("tabindex"); figure.removeAttribute("aria-label"); } + } + syncFigureRole(); if (sheetMq.addEventListener) sheetMq.addEventListener("change", syncFigureRole); else sheetMq.addListener(syncFigureRole); figure.addEventListener("click", function () { if (isSheet()) open(); }); figure.addEventListener("keydown", function (e) { if ((e.key === "Enter" || e.key === " ") && isSheet()) { e.preventDefault(); open(); } }); picker.addEventListener("change", function (e) { diff --git a/site/styles.css b/site/styles.css index bf19652..c56ca43 100644 --- a/site/styles.css +++ b/site/styles.css @@ -259,7 +259,7 @@ pre { [data-direction="swap"] .hero-canvas { aspect-ratio: 4 / 3; } [data-direction="spacelike"] .hero { min-height: calc(100svh - var(--header-h)); } [data-direction="spacelike"] .hero-inner { padding-top: 56px; padding-bottom: 72px; } - [data-direction="spacelike"] .hero-caption { bottom: 12px; font-size: 11.5px; } + [data-direction="spacelike"] .hero-caption { font-size: 11.5px; } } /* Sections */ @@ -292,6 +292,7 @@ section { padding: 88px 0; border-bottom: 1px solid var(--line); } @media (max-width: 900px) { .install-grid { grid-template-columns: 1fr; } } .site-footer { padding: 36px 0 48px; color: var(--muted); font-size: 14px; } +@media (min-width: 901px) { .site-footer { padding-bottom: 84px; } } .site-footer .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 24px; align-items: center; } .site-footer a { color: var(--body); text-decoration: none; } .site-footer a:hover { color: var(--ink); } @@ -314,7 +315,7 @@ section { padding: 88px 0; border-bottom: 1px solid var(--line); } padding: 14px 16px 16px; background: var(--surface); color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-md); box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.35); - font-size: 13px; + font-size: 13px; max-height: calc(100vh - 90px); overflow-y: auto; } .dir-picker.is-open { display: block; } .dir-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 2px; } @@ -357,10 +358,11 @@ section { padding: 88px 0; border-bottom: 1px solid var(--line); } .dir-toggle strong { color: var(--ink); font-weight: 600; } .dir-toggle .icon { width: 16px; height: 16px; color: var(--eyebrow); } [data-direction="spacelike"] .dir-toggle { position: absolute; right: 20px; bottom: 14px; z-index: 2; margin: 0; } - [data-direction="spacelike"] .hero-caption { max-width: calc(100% - 200px); } + [data-direction="spacelike"] .hero-caption { bottom: 66px; max-width: calc(100% - 48px); } .dir-picker { position: fixed; left: 0; right: 0; bottom: 0; width: auto; max-width: 560px; margin: 0 auto; z-index: 60; padding: 16px 20px calc(18px + env(safe-area-inset-bottom)); border-radius: var(--radius-lg) var(--radius-lg) 0 0; + max-height: calc(100svh - 24px); overflow-y: auto; } .dir-options label { min-height: 44px; } .dir-backdrop.is-open { display: block; position: fixed; inset: 0; z-index: 55; background: rgba(0, 0, 0, 0.45); } From 44910a71d4bcbaffdf404610e3625722227450a1 Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 01:37:44 +0000 Subject: [PATCH 04/10] site: fix round-1 defects (steps as a sequence, wrapping install commands, wrapping captions, spacelike clearing) Co-Authored-By: Dailen Gunter --- site/directions.js | 7 +++++++ site/index.html | 18 +++++++++--------- site/styles.css | 24 ++++++++++++++---------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/site/directions.js b/site/directions.js index 7dd1acc..09d5f0f 100644 --- a/site/directions.js +++ b/site/directions.js @@ -212,6 +212,7 @@ out = "pair " + pad(k + 1, 4) + " \u00b7 A " + (s > 0 ? "\u2191" : "\u2193") + " B " + (s > 0 ? "\u2193" : "\u2191") + " \u00b7 light from A is " + pct + "% of the way to B"; } } + var plate = copyRect(); if (plate && !narrow) { ctx.fillStyle = c.bg; ctx.fillRect(plate.x - 28, plate.y - 20, plate.w + 56, plate.h + 40); } return out; }; } @@ -330,6 +331,12 @@ }; } + function copyRect() { + var copy = document.querySelector(".hero-copy"); if (!copy) return null; + var a = copy.getBoundingClientRect(), b = canvas.getBoundingClientRect(); + return { x: a.left - b.left, y: a.top - b.top, w: a.width, h: a.height }; + } + // ---------- engine ---------- var ctx = canvas.getContext("2d"); function size() { diff --git a/site/index.html b/site/index.html index 808f647..9a4ce11 100644 --- a/site/index.html +++ b/site/index.html @@ -73,23 +73,23 @@

Discover, pair, sync.

  1. -

    Resonance

    -

    Particles advertise over mDNS on the local network and publish only a public pairing-group ID, never the key. Where multicast is blocked, static --peer entries do the same job.

    -
    entangle scan
    +

    Resonance

    +

    Particles advertise over mDNS on the local network and publish only a public pairing-group ID, never the key. Where multicast is blocked, static --peer entries do the same job.

    +
    entangle scan
  2. -

    Pairing, by a human

    -

    One person creates a pairing code, another enters it. On first contact both machines derive three emoji from a commit-then-reveal exchange; the humans compare and approve. Agents never see the code and cannot approve.

    +

    Pairing, by a human

    +

    One person creates a pairing code, another enters it. On first contact both machines derive three emoji from a commit-then-reveal exchange; the humans compare and approve. Agents never see the code and cannot approve.

    entangle pair init
     entangle pair join <code>
    -entangle peer approve
    +entangle peer approve
  3. -

    State sync

    -

    An agent sends JSON or one file over a Noise XXpsk3 link on TCP 7337. The receiver checks the byte count and SHA-256 before anything reaches its inbox; the other agent observes it, then collapses it.

    +

    State sync

    +

    An agent sends JSON or one file over a Noise XXpsk3 link on TCP 7337. The receiver checks the byte count and SHA-256 before anything reaches its inbox; the other agent observes it, then collapses it.

    find_entangled_particles
     sync_entangled_state
    -observe_entangled_states
    +observe_entangled_states
diff --git a/site/styles.css b/site/styles.css index c56ca43..4605bcc 100644 --- a/site/styles.css +++ b/site/styles.css @@ -259,25 +259,29 @@ pre { [data-direction="swap"] .hero-canvas { aspect-ratio: 4 / 3; } [data-direction="spacelike"] .hero { min-height: calc(100svh - var(--header-h)); } [data-direction="spacelike"] .hero-inner { padding-top: 56px; padding-bottom: 72px; } + .hero-caption { white-space: normal; overflow: visible; text-overflow: clip; min-height: 3em; } [data-direction="spacelike"] .hero-caption { font-size: 11.5px; } } +.install-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 48px; } +.install-grid h3 { font-size: 14px; font-weight: 500; color: var(--muted); margin: 0 0 8px; } +.install-grid pre { white-space: pre-wrap; overflow-wrap: anywhere; } +@media (max-width: 1100px) { .install-grid { grid-template-columns: 1fr; } } + /* Sections */ section { padding: 88px 0; border-bottom: 1px solid var(--line); } .section-head { max-width: 680px; margin-bottom: 40px; } .section-head h2 { font-size: clamp(30px, 3.4vw, 40px); line-height: 1.12; letter-spacing: -0.02em; margin-bottom: 14px; } .section-head p { color: var(--body); font-size: 17px; } -.steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; counter-reset: step; list-style: none; margin: 0; padding: 0; } -.steps li { position: relative; padding: 24px 0 0; border-top: 1px solid var(--line-strong); } -.steps li::before { - counter-increment: step; content: "0" counter(step); - display: block; font-family: var(--font-mono); font-size: 12.5px; color: var(--eyebrow); letter-spacing: 0.08em; margin-bottom: 14px; -} -.steps h3 { font-size: 20px; margin-bottom: 8px; } -.steps p { color: var(--body); font-size: 15.5px; margin-bottom: 14px; } -.steps pre { font-size: 12.5px; } -@media (max-width: 900px) { .steps { grid-template-columns: 1fr; } } +.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; border-top: 1px solid var(--line-strong); } +.steps li { display: grid; grid-template-columns: minmax(0, 0.42fr) minmax(0, 0.58fr); gap: 16px 48px; padding: 32px 0; border-bottom: 1px solid var(--line); } +.steps li > div { min-width: 0; } +.steps h3 { font-size: 24px; letter-spacing: -0.01em; margin: 0; display: flex; align-items: baseline; gap: 14px; } +.steps h3::before { counter-increment: step; content: counter(step, decimal-leading-zero); font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--eyebrow); letter-spacing: 0.08em; } +.steps p { color: var(--body); font-size: 16px; margin: 0 0 14px; max-width: 58ch; } +.steps pre { font-size: 13px; display: inline-block; min-width: 260px; max-width: 100%; } +@media (max-width: 900px) { .steps li { grid-template-columns: 1fr; gap: 12px; padding: 24px 0; } .steps pre { display: block; } } .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 48px; align-items: start; } .split .section-head { margin-bottom: 0; } From 9fc2ea45ed5fcaca7941edebd76ed46c45d8de4c Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 01:45:46 +0000 Subject: [PATCH 05/10] site: round 2 directions Quadrant, Crossing, Collapse (photon-count fields, probability clouds) Co-Authored-By: Dailen Gunter --- scripts/brand/directions.py | 52 +++++- site/assets/logo/collapse-lockup.svg | 55 ++++++ site/assets/logo/collapse-mark.svg | 54 ++++++ site/assets/logo/crossing-lockup.svg | 7 + site/assets/logo/crossing-mark.svg | 6 + site/assets/logo/quadrant-lockup.svg | 8 + site/assets/logo/quadrant-mark.svg | 7 + site/directions.js | 260 ++++++++++++++++++++++++++- site/styles.css | 71 +++++++- 9 files changed, 517 insertions(+), 3 deletions(-) create mode 100644 site/assets/logo/collapse-lockup.svg create mode 100644 site/assets/logo/collapse-mark.svg create mode 100644 site/assets/logo/crossing-lockup.svg create mode 100644 site/assets/logo/crossing-mark.svg create mode 100644 site/assets/logo/quadrant-lockup.svg create mode 100644 site/assets/logo/quadrant-mark.svg diff --git a/scripts/brand/directions.py b/scripts/brand/directions.py index c2edbe7..140441d 100644 --- a/scripts/brand/directions.py +++ b/scripts/brand/directions.py @@ -24,6 +24,9 @@ "spacelike": {"ink": "#ECEBE6", "a": "#F0B23A", "b": "#F0B23A"}, "swap": {"ink": "#EFEBE0", "a": "#D7935B", "b": "#D7935B"}, "bound": {"ink": "#0F1B1E", "a": "#0F7A5A", "b": "#9AA7AC"}, + "quadrant": {"ink": "#F1ECE2", "a": "#E9A23B", "b": "#F4E8D2"}, + "crossing": {"ink": "#EDEEEA", "a": "#D9532F", "b": "#F6D4BE"}, + "collapse": {"ink": "#17171B", "a": "#B8341C", "b": "#1C6E6B"}, } @@ -91,7 +94,54 @@ def bound(p): return "\n".join(out) -MARKS = {"singlet": singlet, "spacelike": spacelike, "swap": swap, "bound": bound} +def _hash(k): + """Deterministic pseudo-random in [0, 1); same formula as site/directions.js hash().""" + x = math.sin(k * 12.9898 + 78.233) * 43758.5453 + return x - math.floor(x) + + +def quadrant(p): + """The Glasgow 2019 Bell image: a ring whose four quadrants carry phase-shifted patterns. + Drawn as four quadrant arcs at alternating radii, the shift between settings made literal.""" + out = ['' % p["ink"]] + radii = [11.5, 9.5, 11.5, 9.5] + for q in range(4): + a0, a1, r = q * math.pi / 2 + 0.12, (q + 1) * math.pi / 2 - 0.12, radii[q] + out.append('' + % (f(24 + r * math.cos(a0)), f(16 + r * math.sin(a0)), f(r), f(r), + f(24 + r * math.cos(a1)), f(16 + r * math.sin(a1)), p["b"] if q % 2 else p["a"])) + return "\n".join(out) + + +def crossing(p): + """Type-II SPDC: two offset rings of down-converted light; the entangled pairs are born + where the rings cross, so the two crossing points are the only filled shapes.""" + y = math.sqrt(10 ** 2 - 6 ** 2) + return "\n".join([ + '' % p["a"], + '' % p["a"], + '' % (f(16 - y), p["b"]), + '' % (f(16 + y), p["b"]), + ]) + + +def collapse(p): + """Two probability clouds as stippled densities, already collapsed to complementary + outcomes: the left cloud sits high, the right one low. No line joins them.""" + out = [] + for side in range(2): + cx, col = (34 if side else 14), (p["b"] if side else p["a"]) + for i in range(26): + r = math.sqrt(_hash(side * 50 + i + 0.2)) * 9 + th = _hash(side * 50 + i + 0.7) * math.pi * 2 + out.append('' + % (f(cx + math.cos(th) * r), f(16 + math.sin(th) * r * 0.75 + (4 if side else -4)), + f(0.9 + _hash(i + side) * 0.9), col)) + return "\n".join(out) + + +MARKS = {"singlet": singlet, "spacelike": spacelike, "swap": swap, "bound": bound, + "quadrant": quadrant, "crossing": crossing, "collapse": collapse} def svg(w, h, body, extra=""): diff --git a/site/assets/logo/collapse-lockup.svg b/site/assets/logo/collapse-lockup.svg new file mode 100644 index 0000000..1cc0d1a --- /dev/null +++ b/site/assets/logo/collapse-lockup.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +entangle + diff --git a/site/assets/logo/collapse-mark.svg b/site/assets/logo/collapse-mark.svg new file mode 100644 index 0000000..678b534 --- /dev/null +++ b/site/assets/logo/collapse-mark.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/assets/logo/crossing-lockup.svg b/site/assets/logo/crossing-lockup.svg new file mode 100644 index 0000000..4240ca8 --- /dev/null +++ b/site/assets/logo/crossing-lockup.svg @@ -0,0 +1,7 @@ + + + + + +entangle + diff --git a/site/assets/logo/crossing-mark.svg b/site/assets/logo/crossing-mark.svg new file mode 100644 index 0000000..64f2586 --- /dev/null +++ b/site/assets/logo/crossing-mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/site/assets/logo/quadrant-lockup.svg b/site/assets/logo/quadrant-lockup.svg new file mode 100644 index 0000000..9566405 --- /dev/null +++ b/site/assets/logo/quadrant-lockup.svg @@ -0,0 +1,8 @@ + + + + + + +entangle + diff --git a/site/assets/logo/quadrant-mark.svg b/site/assets/logo/quadrant-mark.svg new file mode 100644 index 0000000..ee32e04 --- /dev/null +++ b/site/assets/logo/quadrant-mark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/site/directions.js b/site/directions.js index 09d5f0f..55995b7 100644 --- a/site/directions.js +++ b/site/directions.js @@ -46,6 +46,36 @@ lead: "An open-source Rust MCP server: agents find each other over mDNS, a human approves each pairing with a three-emoji check, and every sync is Noise-encrypted and SHA-256 verified.", mark: markBound, anim: AnimBound + }, + { + id: "quadrant", + name: "Quadrant", + desc: "Dark room, amber ring. The 2019 Bell image: photon A's pattern follows photon B's filter.", + eyebrow: "Encrypted agent-to-agent sync", + title: "One state, seen twice.", + lead: "Agents on your LAN discover each other over mDNS; agents on different networks meet through a repeater you host. Either way the link is Noise-encrypted end to end, and what one agent holds, the other can observe.", + mark: markQuadrant, + anim: AnimQuadrant + }, + { + id: "crossing", + name: "Crossing", + desc: "Deep slate, ember rings. Type-II down-conversion: the pairs live where the rings cross.", + eyebrow: "MCP server for agent collaboration", + title: "Where two agents meet.", + lead: "Discovery on the LAN, a three-emoji check by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", + mark: markCrossing, + anim: AnimCrossing + }, + { + id: "collapse", + name: "Collapse", + desc: "Paper, vermilion and teal. Two probability clouds; one measurement, one frame, no beam.", + eyebrow: "Local-first MCP · Agent to agent", + title: "Same state. Any distance.", + lead: "Entangle lets AI agents find each other, pair under a human's eye, and share files and JSON over end-to-end encrypted links. Nothing travels through anyone who could read it.", + mark: markCollapse, + anim: AnimCollapse } ]; @@ -72,6 +102,30 @@ return '' + halfDisc(13, 16, 9.25, -32, 1, c.a) + '' + halfDisc(35, 16, 9.25, -32, -1, c.b); } + function markQuadrant(c) { + var s = '', rs = [11.5, 9.5, 11.5, 9.5]; + for (var q = 0; q < 4; q++) { + var a0 = q * Math.PI / 2 + 0.12, a1 = (q + 1) * Math.PI / 2 - 0.12, r = rs[q]; + s += ''; + } + return s; + } + function markCrossing(c) { + var y = Math.sqrt(10 * 10 - 6 * 6); + return '' + + ''; + } + function markCollapse(c) { + var s = ""; + for (var side = 0; side < 2; side++) { + var cx = side ? 34 : 14, col = side ? c.b : c.a; + for (var i = 0; i < 26; i++) { + var r = Math.sqrt(hash(side * 50 + i + 0.2)) * 9, th = hash(side * 50 + i + 0.7) * Math.PI * 2; + s += ''; + } + } + return s; + } function markSpacelike(c) { return '' + ''; @@ -331,6 +385,210 @@ }; } + // ---------- shared: photon-count accumulation field (round 2) ---------- + // A low-resolution count buffer that single "photons" are drawn into by rejection + // sampling against an intensity function, then mapped through a 3-stop colour ramp. + // This is how the Glasgow (2019) and SPDC ring images are actually formed: many + // individual detections accumulating into a pattern, with real shot-noise grain. + function PhotonField(S, intensity, ramp) { + var self = this; + this.S = S; this.n = new Float32Array(S * S); this.total = 0; + this.off = document.createElement("canvas"); this.off.width = S; this.off.height = S; + this.octx = this.off.getContext("2d"); this.img = this.octx.createImageData(S, S); + this.add = function (count, phase) { + var S2 = self.S, added = 0, tries = count * 6; + for (var i = 0; i < tries && added < count; i++) { + var x = Math.random() * 2 - 1, y = Math.random() * 2 - 1; + if (Math.random() < intensity(x, y, phase)) { + var ix = (x * 0.5 + 0.5) * S2 | 0, iy = (y * 0.5 + 0.5) * S2 | 0; + self.n[iy * S2 + ix] += 1; added++; + } + } + self.total += added; + }; + this.decay = function (f) { var n = self.n; for (var i = 0; i < n.length; i++) n[i] *= f; }; + this.render = function (scale) { + var d = self.img.data, n = self.n; + for (var i = 0, j = 0; i < n.length; i++, j += 4) { + var v = n[i] / scale; if (v > 1) v = 1; + var k = v < 0.5 ? v * 2 : (v - 0.5) * 2, a = v < 0.5 ? ramp[0] : ramp[1], b = v < 0.5 ? ramp[1] : ramp[2]; + d[j] = a[0] + (b[0] - a[0]) * k; d[j + 1] = a[1] + (b[1] - a[1]) * k; d[j + 2] = a[2] + (b[2] - a[2]) * k; d[j + 3] = 255; + } + self.octx.putImageData(self.img, 0, 0); + return self.off; + }; + } + function rgb(hex) { return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)]; } + function mix(a, b, k) { return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]; } + function gauss(x, s) { return Math.exp(-(x * x) / (2 * s * s)); } + + // ---------- 5. Quadrant: the Glasgow 2019 Bell-image ring ---------- + // Moreau et al. imaged photon A with a camera while photon B passed a phase filter that + // was stepped through four settings. Each quadrant of the image shows photon A's + // interference pattern shifted by the phase chosen for B. Here the ring forms from + // individual detections; the quadrant phases follow B's rotating filter, and the + // pattern re-forms, one photon at a time, to match it. + function AnimQuadrant(ctx, c) { + var S = 224; + var ramp = [rgb(c.bg), mix(rgb(c.bg), rgb(c.a), 0.8), mix(rgb(c.b), [255, 255, 255], 0.25)]; + function intensity(x, y, phase) { + var r = Math.sqrt(x * x + y * y); if (r > 0.95 || r < 0.28) return 0; + var ring = gauss(r - 0.63, 0.1) * 0.85 + gauss(r - 0.63, 0.24) * 0.1; + var q = (x >= 0 ? (y < 0 ? 0 : 1) : (y < 0 ? 3 : 2)); + var phi = Math.atan2(y, x); + var ang = 0.5 + 0.5 * Math.cos(7 * phi + phase + q * Math.PI / 2); // angular fringes, phase-stepped per quadrant + var rad = 0.72 + 0.28 * Math.cos(r * 52 + q * Math.PI / 2); // fine radial fringes: the interference texture + return ring * (0.08 + 0.92 * ang * ang) * rad; + } + var field = new PhotonField(S, intensity, ramp), lastT = -1, seeded = false; + this.P = 24; + this.draw = function (w, h, t, still) { + var narrow = w < 700; + var B = ((t / 24) % 1) * Math.PI; // B's filter: half a turn every 24 s + var phase = 2 * B; + if (still) { if (!seeded) { for (var k = 0; k < 90; k++) field.add(2200, 0.6); seeded = true; } } + else { + var dt = lastT < 0 ? 0.016 : Math.min(0.1, Math.abs(t - lastT)); lastT = t; + field.decay(Math.pow(0.5, dt / 2.4)); + field.add(Math.round(1500 * (dt / 0.016)), phase); + } + var img = field.render(11); + ctx.clearRect(0, 0, w, h); + var size = Math.min(w, h) * (narrow ? 1.02 : 0.98), cx = w / 2, cy = h / 2; + ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "low"; + ctx.drawImage(img, cx - size / 2, cy - size / 2, size, size); + ctx.strokeStyle = withAlpha(c.ink, 0.22); ctx.lineWidth = 1; ctx.setLineDash([3, 5]); ctx.beginPath(); + ctx.moveTo(cx, cy - size * 0.47); ctx.lineTo(cx, cy + size * 0.47); ctx.moveTo(cx - size * 0.47, cy); ctx.lineTo(cx + size * 0.47, cy); ctx.stroke(); ctx.setLineDash([]); + var rr = narrow ? 14 : 18, bx = w - rr - 14, by = rr + 14; + ctx.strokeStyle = withAlpha(c.ink, 0.5); ctx.beginPath(); ctx.arc(bx, by, rr, 0, Math.PI * 2); ctx.stroke(); + ctx.strokeStyle = c.a; ctx.lineWidth = 1.5; ctx.beginPath(); + ctx.moveTo(bx - Math.cos(B) * rr, by - Math.sin(B) * rr); ctx.lineTo(bx + Math.cos(B) * rr, by + Math.sin(B) * rr); ctx.stroke(); + var deg = Math.round(B * 180 / Math.PI); + ctx.fillStyle = c.muted; ctx.font = mono(narrow ? 10 : 11); ctx.textAlign = "right"; ctx.textBaseline = "middle"; + ctx.fillText("B filter " + pad(deg, 3) + "°", bx - rr - 10, by); + return pad(Math.round(field.total / 1000), 3) + "k photons of A · B's filter at " + deg + "° · A's pattern follows it"; + }; + } + + // ---------- 6. Crossing: type-II SPDC rings ---------- + // Kwiat et al. 1995: a pump photon splits inside a BBO crystal into two photons of + // orthogonal polarisation, emitted on two cones. On a screen the cones are two offset + // rings; where they intersect a photon could belong to either ring, so its polarisation + // is undefined until its partner is measured. Pairs born at the two crossings are the + // polarisation-entangled ones. Each pair here lands in the same frame, no line between. + function AnimCrossing(ctx, c) { + var S = 224, R = 0.5, D = 0.32; + var ramp = [rgb(c.bg), mix(rgb(c.bg), rgb(c.a), 0.9), mix(rgb(c.b), [255, 255, 255], 0.15)]; + function intensity(x, y) { + var r1 = Math.sqrt((x + D) * (x + D) + y * y), r2 = Math.sqrt((x - D) * (x - D) + y * y); + var i = gauss(r1 - R, 0.028) + gauss(r2 - R, 0.028) + 0.18 * (gauss(r1 - R, 0.07) + gauss(r2 - R, 0.07)); + var phi1 = Math.atan2(y, x + D), phi2 = Math.atan2(y, x - D); + i *= 0.78 + 0.22 * Math.cos(11 * phi1 + 1.7) * Math.cos(9 * phi2); + return i * 0.72; + } + var field = new PhotonField(S, intensity, ramp), lastT = -1, seeded = false, pairs = [], nextPair = 0, count = 0; + var cy0 = Math.sqrt(R * R - D * D); + this.P = 8; + this.draw = function (w, h, t, still) { + var narrow = w < 700; + if (still) { if (!seeded) { for (var k = 0; k < 70; k++) field.add(2400, 0); seeded = true; pairs = [{ t: 0 }]; count = 1; } } + else { + var dt = lastT < 0 ? 0.016 : Math.min(0.1, Math.abs(t - lastT)); lastT = t; + field.decay(Math.pow(0.5, dt / 3.5)); + field.add(Math.round(1300 * (dt / 0.016)), 0); + if (t >= nextPair) { pairs.push({ t: t }); count++; nextPair = t + 0.55 + hash(count) * 0.9; } + pairs = pairs.filter(function (p) { return t - p.t < 1.6; }); + } + var img = field.render(9); + ctx.clearRect(0, 0, w, h); + var size = Math.min(w / 1.05, h / 0.78 * 1.15), cx = w / 2, cy = h / 2; + ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "low"; + ctx.drawImage(img, cx - size / 2, cy - size / 2, size, size); + function fx(u) { return cx + u * size / 2; } function fy(v) { return cy + v * size / 2; } + for (var i = 0; i < pairs.length; i++) { + var age = still ? 0.15 : t - pairs[i].t, a = Math.max(0, 1 - age / 1.6), rad = 3 + age * 9; + ctx.strokeStyle = withAlpha(c.b, a * 0.9); ctx.lineWidth = 1.25; + ctx.beginPath(); ctx.arc(fx(0), fy(-cy0), rad, 0, Math.PI * 2); ctx.stroke(); + ctx.beginPath(); ctx.arc(fx(0), fy(cy0), rad, 0, Math.PI * 2); ctx.stroke(); + if (age < 0.25) { ctx.fillStyle = c.b; ctx.beginPath(); ctx.arc(fx(0), fy(-cy0), 2.2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(fx(0), fy(cy0), 2.2, 0, Math.PI * 2); ctx.fill(); } + } + ctx.fillStyle = c.muted; ctx.font = mono(narrow ? 10 : 11); ctx.textBaseline = "middle"; + if (!narrow) { + ctx.textAlign = "right"; ctx.fillText("H", fx(-D - R) - 8, fy(0)); + ctx.textAlign = "left"; ctx.fillText("V", fx(D + R) + 8, fy(0)); + } + return "pair " + pad(count, 4) + " · one photon at each crossing · which ring either came from is undefined, so the pair is entangled"; + }; + } + + // ---------- 7. Collapse: two probability clouds, one measurement ---------- + // Each cloud is a probability density drawn as a few thousand sample points that drift + // independently: no signal passes between them. On measurement both collapse in the same + // frame to complementary outcomes (one up, one down); then the state is prepared again. + // The drama is the simultaneous snap. Nothing crosses the gap. + function AnimCollapse(ctx, c) { + var N = 2400; + function cloud(seed) { + var pts = []; + for (var i = 0; i < N; i++) { + var r = Math.sqrt(-2 * Math.log(1 - hash(seed + i * 2.11 + 0.3))), th = hash(seed + i * 3.7 + 0.6) * Math.PI * 2; + var r2 = Math.sqrt(-2 * Math.log(1 - hash(seed + i * 5.3 + 0.9))), th2 = hash(seed + i * 7.1 + 0.2) * Math.PI * 2; + pts.push({ lobe: hash(seed + i * 1.37) < 0.5 ? -1 : 1, ox: r * Math.cos(th), oy: r * Math.sin(th), ax: r2 * Math.cos(th2), ay: r2 * Math.sin(th2), + ph: hash(seed + i * 9.9) * Math.PI * 2, f: 0.6 + hash(seed + i * 4.4) * 0.9, k: hash(seed + i * 6.6) < 0.5 ? 0 : 1 }); + } + return pts; + } + var A = cloud(11), Bc = cloud(29); + // particles are stamped from pre-rendered dot sprites: ~5k drawImage calls per frame is + // cheap where 5k arc fills are not (mid-range phone budget) + var sprites = {}; + function sprite(color, sz) { + var key = color + sz; if (sprites[key]) return sprites[key]; + var dpr = Math.min(2, window.devicePixelRatio || 1), px = Math.ceil(sz * 2 * dpr) + 2; + var cv = document.createElement("canvas"); cv.width = px; cv.height = px; + var g = cv.getContext("2d"); g.fillStyle = color; g.beginPath(); g.arc(px / 2, px / 2, sz * dpr, 0, Math.PI * 2); g.fill(); + sprites[key] = { cv: cv, css: px / dpr }; return sprites[key]; + } + this.P = 7; + this.draw = function (w, h, t, still) { + var narrow = w < 700, P = 7, u = ((t % P) + P) % P, n = Math.floor(t / P); + var outcome = hash(n + 0.5) < 0.5 ? 1 : -1; + var snap = u < 3 ? 0 : u < 3.18 ? ease((u - 3) / 0.18) : u < 5.2 ? 1 : u < 6.2 ? 1 - ease((u - 5.2) / 1.0) : 0; + if (still) { snap = 1; u = 4; } + ctx.clearRect(0, 0, w, h); + var lobeY = h * 0.19, sig = Math.min(w * 0.36, h) * 0.13; + var xs = [w * 0.27, w * 0.73], cols = [c.a, c.b]; + var step = narrow ? 2 : 1; // fewer samples on phones + for (var side = 0; side < 2; side++) { + var pts = side ? Bc : A, cxs = xs[side], cys = h / 2, res = side ? -outcome : outcome; + var color = snap > 0.5 ? withAlpha(cols[side], 0.55) : withAlpha(c.ink, 0.34); + var base = narrow ? 1.0 : 1.3, sp = [sprite(color, base * 0.8), sprite(color, base * 1.35)]; + var tight = 1 - 0.32 * snap; + for (var i = 0; i < pts.length; i += step) { + var p = pts[i]; + var jx = Math.sin(t * p.f * 1.3 + p.ph) * 0.18, jy = Math.cos(t * p.f + p.ph * 1.7) * 0.18; + var ox = p.ox, oy = p.oy; + if (snap > 0 && p.lobe !== res) { ox = p.ax; oy = p.ay; } + var x0 = (ox + jx) * sig * tight, y0 = -p.lobe * lobeY + (oy + jy) * sig * tight; + var y = p.lobe === res ? y0 : y0 + ((-res * lobeY + (oy + jy) * sig * tight) - y0) * snap; + var s = sp[p.k]; + ctx.drawImage(s.cv, cxs + x0 - s.css / 2, cys + y - s.css / 2, s.css, s.css); + } + ctx.strokeStyle = withAlpha(c.ink, 0.35); ctx.lineWidth = 1; ctx.setLineDash([2, 4]); + ctx.beginPath(); ctx.moveTo(cxs, cys - h * 0.42); ctx.lineTo(cxs, cys + h * 0.42); ctx.stroke(); ctx.setLineDash([]); + ctx.fillStyle = c.muted; ctx.font = mono(narrow ? 10.5 : 11.5); ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + ctx.fillText(side ? "B" : "A", cxs, cys - h * 0.44); + if (snap > 0.5) { + ctx.fillStyle = cols[side]; ctx.font = sans(narrow ? 13 : 15, 600); ctx.textBaseline = "top"; + ctx.fillText(res > 0 ? "↑" : "↓", cxs, cys + h * 0.44 - 2); + } + } + var state = snap > 0.5 ? ("measured · A " + (outcome > 0 ? "↑" : "↓") + " B " + (outcome > 0 ? "↓" : "↑") + " · same frame; nothing crossed the gap") + : (u > 6.2 || u < 0.2 ? "state prepared again · each cloud fluctuates on its own" : "superposition · two independent probability clouds · measurement in " + (3 - u).toFixed(1) + " s"); + return "measurement " + pad(n + 1, 4) + " · " + state; + }; + } + function copyRect() { var copy = document.querySelector(".hero-copy"); if (!copy) return null; var a = copy.getBoundingClientRect(), b = canvas.getBoundingClientRect(); @@ -403,7 +661,7 @@ var corner = document.createElement("div"); corner.className = "dir-corner"; var pill = document.createElement("button"); pill.type = "button"; pill.className = "dir-pill"; pill.setAttribute("aria-controls", "dir-picker"); pill.setAttribute("aria-expanded", "false"); - pill.innerHTML = 'Direction: 1\u20134'; + pill.innerHTML = 'Direction: 1\u20137'; corner.appendChild(picker); corner.appendChild(pill); document.body.appendChild(backdrop); document.body.appendChild(corner); diff --git a/site/styles.css b/site/styles.css index 4605bcc..e5e04e7 100644 --- a/site/styles.css +++ b/site/styles.css @@ -110,6 +110,54 @@ --eyebrow: #0f7a5a; } +[data-direction="quadrant"] { + color-scheme: dark; + --bg: #121113; + --surface: #1a181a; + --sunken: #201d20; + --line: #2b282b; + --line-strong: #403c40; + --ink: #f1ece2; + --body: #bdb7ac; + --muted: #8e887e; + --a: #e9a23b; + --b: #f4e8d2; + --link: #e9a23b; + --eyebrow: #e9a23b; +} + +[data-direction="crossing"] { + color-scheme: dark; + --bg: #14181b; + --surface: #1b2024; + --sunken: #20262b; + --line: #2b3339; + --line-strong: #3f4a52; + --ink: #edeeea; + --body: #b7bcbb; + --muted: #87908f; + --a: #d9532f; + --b: #f6d4be; + --link: #e8a07f; + --eyebrow: #d9532f; +} + +[data-direction="collapse"] { + color-scheme: light; + --bg: #f4f0e7; + --surface: #fbf9f3; + --sunken: #ece6d9; + --line: #e0d8c8; + --line-strong: #c8bea9; + --ink: #17171b; + --body: #44454c; + --muted: #6d6e76; + --a: #b8341c; + --b: #1c6e6b; + --link: #b8341c; + --eyebrow: #b8341c; +} + /* Base */ *, *::before, *::after { box-sizing: border-box; } html { scroll-behavior: smooth; scroll-padding-top: 84px; } @@ -249,8 +297,29 @@ pre { [data-direction="bound"] .hero-inner { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); align-items: center; } [data-direction="bound"] .hero-canvas { aspect-ratio: 16 / 11; background: var(--surface); border: 1px solid var(--line); } +/* Quadrant: copy left, square photon image right, no card frame */ +[data-direction="quadrant"] .hero-canvas { aspect-ratio: 1 / 1; border-radius: 0; } +[data-direction="quadrant"] .hero-inner { grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr); align-items: center; gap: 56px; } + +/* Crossing: centered copy over a wide photon image, like Swap */ +[data-direction="crossing"] .hero-inner { grid-template-columns: 1fr; justify-items: center; text-align: center; gap: 8px; padding-top: 44px; padding-bottom: 24px; } +[data-direction="crossing"] .hero-copy { max-width: 720px; } +[data-direction="crossing"] .hero-lead { margin-left: auto; margin-right: auto; } +[data-direction="crossing"] .hero-actions, [data-direction="crossing"] .hero-facts { justify-content: center; } +[data-direction="crossing"] .hero-visual { width: 100%; } +[data-direction="crossing"] .hero-canvas { aspect-ratio: 21 / 7; border-radius: 0; max-width: 1040px; margin: 0 auto; } +[data-direction="crossing"] .hero-caption { text-align: center; } + +/* Collapse: copy left, two clouds right on bare paper */ +[data-direction="collapse"] .hero-inner { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); align-items: center; } +[data-direction="collapse"] .hero-canvas { aspect-ratio: 5 / 4; border-radius: 0; } +@media (max-width: 900px) { + [data-direction="crossing"] .hero-canvas { aspect-ratio: 4 / 3; } + [data-direction="collapse"] .hero-canvas { aspect-ratio: 1 / 1; } +} + @media (max-width: 900px) { - .hero-inner, [data-direction="singlet"] .hero-inner, [data-direction="bound"] .hero-inner, [data-direction="swap"] .hero-inner { grid-template-columns: 1fr; gap: 24px; padding-top: 36px; padding-bottom: 36px; } + .hero-inner, [data-direction="singlet"] .hero-inner, [data-direction="bound"] .hero-inner, [data-direction="swap"] .hero-inner, [data-direction="quadrant"] .hero-inner, [data-direction="collapse"] .hero-inner, [data-direction="crossing"] .hero-inner { grid-template-columns: 1fr; gap: 24px; padding-top: 36px; padding-bottom: 36px; } .hero-title { font-size: clamp(36px, 10vw, 48px); margin-bottom: 14px; } .hero-lead { font-size: 17px; margin-bottom: 20px; } .hero-actions { margin-bottom: 16px; } From bd4fde96bfe8c89b44a5e1726108337e446faa6c Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 12 Oct 2026 01:46:26 +0000 Subject: [PATCH 06/10] site: document round 2 directions Co-Authored-By: Dailen Gunter --- site/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/README.md b/site/README.md index 848ef0d..e504161 100644 --- a/site/README.md +++ b/site/README.md @@ -22,6 +22,9 @@ Query parameters: `?d=&speed=quarter`, plus `&t=&picker=open` for c | 2 | `spacelike` | Minkowski diagram: two simultaneous outcomes whose light cones have not met | bg `#0F1115`, ink `#ECEBE6`, body `#B4B6BC`, muted `#868A93`, trace `#F0B23A` | | 3 | `swap` | Entanglement swapping: a Bell measurement at the repeater entangles A and C, which never met | bg `#1F2B27`, chalk `#EFEBE0`, body `#C7C3B7`, muted `#9A988D`, copper `#D7935B`, slate `#8FB8C9` | | 4 | `bound` | Bell's inequality: measured correlation E(θ) rising above the local-realist straight line | bg `#F2F5F6`, ink `#0F1B1E`, body `#3C4A4F`, muted `#627179`, trace `#0F7A5A`, bound `#9AA7AC` | +| 5 | `quadrant` | The Glasgow 2019 Bell image (Moreau et al.): photon A's interference pattern, built from single detections, shifts in each quadrant with the phase filter applied to photon B | bg `#121113`, ink `#F1ECE2`, body `#BDB7AC`, muted `#8E887E`, amber `#E9A23B`, warm white `#F4E8D2` | +| 6 | `crossing` | Type-II SPDC (Kwiat et al. 1995): two offset rings of down-converted light; the polarisation-entangled pairs are born where the rings cross | bg `#14181B`, ink `#EDEEEA`, body `#B7BCBB`, muted `#87908F`, ember `#D9532F`, peach `#F6D4BE` | +| 7 | `collapse` | Two probability clouds that fluctuate independently and collapse in the same frame to complementary outcomes; nothing travels between them | bg `#F4F0E7`, ink `#17171B`, body `#44454C`, muted `#6D6E76`, vermilion `#B8341C`, teal `#1C6E6B` | Marks are generated by `scripts/brand/directions.py` into `assets/logo/`; the header draws the same geometry inline so it recolors with the direction. From 6476bcd940ac649967770915bc4473a51841edea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:34:41 +0000 Subject: [PATCH 07/10] site: open on Quadrant by default (working lead; picker stays) Co-Authored-By: Dailen Gunter --- site/README.md | 2 +- site/directions.js | 2 +- site/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/README.md b/site/README.md index e504161..91b3eb3 100644 --- a/site/README.md +++ b/site/README.md @@ -13,7 +13,7 @@ Type scale, spacing, radii and icon rules follow the tachyon design system `index.html` currently shows four switchable brand directions. Switching changes the palette (`html[data-direction]` in `styles.css`), the header mark, the hero copy and the hero animation (`directions.js`). Desktop: the labelled "Direction" pill in the bottom-right corner. Phones: the -pill under the hero visual, or tap the visual. Keys `1`–`4` switch, `Q` toggles quarter speed. +pill under the hero visual, or tap the visual. Keys `1`–`7` switch, `Q` toggles quarter speed. First load opens on `quadrant` (the working lead); a returning visitor gets their last pick. Query parameters: `?d=&speed=quarter`, plus `&t=&picker=open` for captures. | # | id | Physics | Palette (role: hex) | diff --git a/site/directions.js b/site/directions.js index 55995b7..9fd4bfa 100644 --- a/site/directions.js +++ b/site/directions.js @@ -708,6 +708,6 @@ reduced.addEventListener && reduced.addEventListener("change", start); buildPicker(); - var initial = params.get("d") || store.get("entangle.direction") || "singlet"; + var initial = params.get("d") || store.get("entangle.direction") || "quadrant"; apply(initial, false); })(); diff --git a/site/index.html b/site/index.html index 9a4ce11..a14b2a3 100644 --- a/site/index.html +++ b/site/index.html @@ -1,5 +1,5 @@ - + From 70e6fe276d51c5baa10dbe45a29f41c27d33eb33 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:11:13 +0000 Subject: [PATCH 08/10] site: replace emoji-pairing copy with one-code group onboarding (#34) Co-Authored-By: Dailen Gunter --- site/directions.js | 6 +++--- site/index.html | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/site/directions.js b/site/directions.js index 9fd4bfa..9f800b1 100644 --- a/site/directions.js +++ b/site/directions.js @@ -33,7 +33,7 @@ desc: "Chalkboard and copper. Entanglement swapping, the repeater's trick.", eyebrow: "MCP server for agent collaboration", title: "Entangle your agents.", - lead: "Discovery on the LAN, a three-emoji check by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", + lead: "Discovery on the LAN, one short code typed by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", mark: markSwap, anim: AnimSwap }, @@ -43,7 +43,7 @@ desc: "Lab notebook. Bell's inequality, violated live, one pair at a time.", eyebrow: "Agent-to-agent, measured", title: "Share state, not secrets.", - lead: "An open-source Rust MCP server: agents find each other over mDNS, a human approves each pairing with a three-emoji check, and every sync is Noise-encrypted and SHA-256 verified.", + lead: "An open-source Rust MCP server: agents find each other over mDNS, devices join a group with one short code, and every sync is Noise-encrypted and SHA-256 verified.", mark: markBound, anim: AnimBound }, @@ -63,7 +63,7 @@ desc: "Deep slate, ember rings. Type-II down-conversion: the pairs live where the rings cross.", eyebrow: "MCP server for agent collaboration", title: "Where two agents meet.", - lead: "Discovery on the LAN, a three-emoji check by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", + lead: "Discovery on the LAN, one short code typed by a human, then files and JSON flow between agents over an encrypted link, direct or through a blind Quantum Repeater you run yourself.", mark: markCrossing, anim: AnimCrossing }, diff --git a/site/index.html b/site/index.html index a14b2a3..2dd8910 100644 --- a/site/index.html +++ b/site/index.html @@ -4,7 +4,7 @@ Entangle: encrypted state sync between AI agents - + @@ -68,21 +68,20 @@

Two agents, one state.

How it works -

Discover, pair, sync.

-

Each machine runs one Entangle process as an MCP stdio server. Discovery is automatic, pairing is a human step, and every sync is encrypted end to end.

+

Discover, join, sync.

+

Each machine runs one Entangle process as an MCP stdio server. Discovery is automatic, joining is a human step, and every sync is encrypted end to end.

  1. Resonance

    -

    Particles advertise over mDNS on the local network and publish only a public pairing-group ID, never the key. Where multicast is blocked, static --peer entries do the same job.

    +

    Particles advertise over mDNS on the local network and publish only public group IDs, never a key. Where multicast is blocked, static --peer entries do the same job.

    entangle scan
  2. -

    Pairing, by a human

    -

    One person creates a pairing code, another enters it. On first contact both machines derive three emoji from a commit-then-reveal exchange; the humans compare and approve. Agents never see the code and cannot approve.

    -
    entangle pair init
    -entangle pair join <code>
    -entangle peer approve
    +

    Joining, by a human

    +

    One device opens an invite and shows a short one-time code; every other device enters it once, in any order. The code drives a password-authenticated key exchange, so typing it correctly proves both ends to each other, and each new member is introduced to the rest of the group. Agents never see the code and cannot invite.

    +
    entangle group create work --invite 4
    +entangle group join 4-ion-quark-muon
  3. State sync

    @@ -104,8 +103,8 @@

    Across networks, through a relay that cannot read.

  • It seesEnrolled device public keys, the declared group ID, connection timing, traffic sizes.
  • -
  • It cannot seeParticle names, pairing keys, the verification emoji, or any synced content.
  • -
  • EnrollmentA single-use invite code from the host, or a join request the host approves after comparing emoji. Devices pin the repeater's static key; revocation by fingerprint.
  • +
  • It cannot seeParticle names, group keys, invite codes, or any synced content.
  • +
  • EnrollmentA one-time invite code from the host. Devices pin the repeater's static key; revocation by fingerprint.
  • Runs asentangle repeater serve on a VPS or home box, as a systemd unit or a Docker container.
  • Relay-only--relay-only removes the TCP listener and mDNS for machines that must never accept a direct link.
From 88132042807722c078ec13fcce65859997573c11 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:18:29 +0000 Subject: [PATCH 09/10] site: use cases section, experimental labels Co-Authored-By: Dailen Gunter --- site/index.html | 40 ++++++++++++++++++++++++++++++++++++++-- site/styles.css | 12 ++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/site/index.html b/site/index.html index 2dd8910..3920321 100644 --- a/site/index.html +++ b/site/index.html @@ -29,6 +29,7 @@