{"is":"instructionGraph001","item":{"content":{"html":"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Seaglass — messages in bottles on the dataverse</title>\n<meta name=\"description\" content=\"A public shore on the dataverse. Every bottle is a signed message left for someone to find.\">\n<meta name=\"theme-color\" content=\"#041114\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:title\" content=\"Seaglass — messages in bottles on the dataverse\">\n<meta property=\"og:description\" content=\"A quiet public shore of signed notes. Read what washed ashore, or cast a bottle of your own.\">\n<meta name=\"twitter:card\" content=\"summary\">\n<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%2307181b'/%3E%3Cpath d='M27 12h10v8l5 8v20a6 6 0 0 1-6 6H28a6 6 0 0 1-6-6V28l5-8v-8Z' fill='none' stroke='%238fe0c6' stroke-width='2.4'/%3E%3Cpath d='M24 10h16' stroke='%23e9b872' stroke-width='2.4' stroke-linecap='round'/%3E%3Cpath d='M28 32h8v10h-8z' fill='%23e9b872' opacity='.85'/%3E%3C/svg%3E\">\n<style>\n/* ────────────────────────────────────────────────────────────────\n   Seaglass — a shore on the dataverse\n   Single-file PAGE object. No external assets, no trackers.\n   ──────────────────────────────────────────────────────────────── */\n:root{\n  --bg:#041114;\n  --bg-2:#071a1e;\n  --surface:rgba(12,34,38,.72);\n  --surface-solid:#0a2227;\n  --ink:#f2eee2;\n  --muted:#a9bdb8;\n  --dim:#6f8a85;\n  --faint:#4a625e;\n  --line:rgba(190,230,220,.12);\n  --line-strong:rgba(190,230,220,.22);\n  --gold:#e9b872;\n  --gold-bright:#f6cf92;\n  --sea:#8fe0c6;\n  --coral:#ef9a86;\n  --shadow:0 24px 70px rgba(0,0,0,.45);\n  --serif:\"Iowan Old Style\",\"Palatino Linotype\",\"Book Antiqua\",Palatino,Georgia,\"Times New Roman\",serif;\n  --sans:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Inter,Roboto,Helvetica,Arial,sans-serif;\n  --mono:\"SF Mono\",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;\n  --wrap:1180px;\n  --ease:cubic-bezier(.22,.8,.24,1);\n}\n@supports (color: color-mix(in srgb, black, white)){\n  :root{ --shard-surface:color-mix(in srgb, var(--tintc) 13%, rgba(9,27,31,.82)); }\n}\n*{box-sizing:border-box}\nhtml{scroll-behavior:smooth}\nhtml,body{margin:0;padding:0}\nbody{\n  background:\n    radial-gradient(1100px 560px at 82% -8%, rgba(233,184,114,.075), transparent 62%),\n    radial-gradient(900px 640px at 8% 4%, rgba(103,196,177,.09), transparent 60%),\n    radial-gradient(1200px 900px at 50% 112%, rgba(19,80,86,.28), transparent 65%),\n    linear-gradient(180deg,var(--bg-2) 0%,var(--bg) 42%,#03100f 100%);\n  color:var(--ink);\n  font-family:var(--sans);\n  font-size:16px;\n  line-height:1.55;\n  min-height:100vh;\n  -webkit-font-smoothing:antialiased;\n  text-rendering:optimizeLegibility;\n  overflow-x:hidden;\n}\nbody.locked{overflow:hidden}\n::selection{background:rgba(143,224,198,.28);color:#fff}\na{color:var(--sea);text-decoration:none}\na:hover{text-decoration:underline;text-underline-offset:3px}\nbutton{font:inherit;color:inherit}\nbutton,a{-webkit-tap-highlight-color:transparent}\nimg,svg{display:block;max-width:100%}\n[hidden]{display:none!important}\n\n/* ── Atmosphere ─────────────────────────────────────────────── */\n.sky,.sea,.grain{position:fixed;inset:0;pointer-events:none;z-index:0}\n.sky{\n  background-image:\n    radial-gradient(1.4px 1.4px at 11% 18%, rgba(232,244,238,.6), transparent 100%),\n    radial-gradient(1px 1px at 23% 9%, rgba(232,244,238,.38), transparent 100%),\n    radial-gradient(1.6px 1.6px at 34% 25%, rgba(232,244,238,.5), transparent 100%),\n    radial-gradient(1px 1px at 46% 12%, rgba(232,244,238,.3), transparent 100%),\n    radial-gradient(1.3px 1.3px at 58% 21%, rgba(232,244,238,.45), transparent 100%),\n    radial-gradient(1px 1px at 67% 6%, rgba(232,244,238,.34), transparent 100%),\n    radial-gradient(1.5px 1.5px at 78% 16%, rgba(232,244,238,.48), transparent 100%),\n    radial-gradient(1px 1px at 88% 9%, rgba(232,244,238,.3), transparent 100%),\n    radial-gradient(1.2px 1.2px at 94% 27%, rgba(232,244,238,.42), transparent 100%),\n    radial-gradient(1px 1px at 7% 34%, rgba(232,244,238,.26), transparent 100%),\n    radial-gradient(1.1px 1.1px at 41% 38%, rgba(232,244,238,.2), transparent 100%),\n    radial-gradient(1px 1px at 72% 31%, rgba(232,244,238,.24), transparent 100%);\n  animation:twinkle 14s ease-in-out infinite alternate;\n}\n@keyframes twinkle{from{opacity:.75}to{opacity:1}}\n.moon{\n  position:fixed;top:-140px;right:-100px;width:520px;height:520px;border-radius:50%;\n  background:radial-gradient(circle at 50% 50%, rgba(246,207,146,.13), rgba(246,207,146,.035) 42%, transparent 68%);\n  pointer-events:none;z-index:0;filter:blur(2px);\n}\n.sea{bottom:0;top:auto;height:62vh;overflow:hidden;opacity:.72}\n.sea span{\n  position:absolute;left:50%;bottom:-96vw;width:172vw;height:172vw;\n  border:1px solid rgba(160,220,208,.055);border-radius:46% 54% 48% 52% / 52% 48% 54% 46%;\n  transform:translateX(-50%);\n  animation:swell 34s ease-in-out infinite alternate;\n}\n.sea span:nth-child(1){bottom:-104vw;border-color:rgba(160,220,208,.075);animation-duration:28s}\n.sea span:nth-child(2){bottom:-92vw;width:156vw;height:156vw;animation-duration:38s;animation-delay:-8s;opacity:.8}\n.sea span:nth-child(3){bottom:-80vw;width:138vw;height:138vw;animation-duration:46s;animation-delay:-18s;opacity:.55}\n@keyframes swell{\n  from{transform:translateX(-50%) rotate(-1.2deg) scale(1)}\n  to{transform:translateX(-50.6%) rotate(1.2deg) scale(1.012)}\n}\n.grain{\n  opacity:.035;z-index:1;mix-blend-mode:overlay;\n  background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E\");\n}\n\n/* ── Shell ──────────────────────────────────────────────────── */\n.wrap{width:min(var(--wrap),calc(100% - 48px));margin:0 auto;position:relative;z-index:2}\n.site{position:relative;z-index:2;display:flex;flex-direction:column;min-height:100vh}\nmain{flex:1}\n\n/* ── Top bar ────────────────────────────────────────────────── */\n.topbar{\n  width:min(var(--wrap),calc(100% - 48px));margin:0 auto;\n  display:flex;align-items:center;gap:18px;\n  padding:30px 0 0;\n}\n.brand{display:inline-flex;align-items:center;gap:12px;color:var(--ink);text-decoration:none!important}\n.brand:hover .brand-name{color:var(--gold-bright)}\n.brand-mark{\n  width:42px;height:42px;border-radius:50%;flex:none;display:grid;place-items:center;\n  background:radial-gradient(circle at 32% 24%, rgba(143,224,198,.18), rgba(10,34,39,.7) 70%);\n  border:1px solid rgba(160,220,208,.2);\n  box-shadow:inset 0 1px 0 rgba(255,255,255,.08), 0 8px 24px rgba(0,0,0,.28);\n}\n.brand-mark svg{width:24px;height:30px}\n.brand-name{\n  font-family:var(--serif);font-size:27px;letter-spacing:.015em;line-height:1;\n  transition:color .18s ease;\n}\n.brand-sub{display:block;font-family:var(--sans);font-size:10.5px;letter-spacing:.19em;text-transform:uppercase;color:var(--dim);margin-top:6px}\n.topbar-spacer{flex:1}\n.toplinks{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end}\n.btn{\n  display:inline-flex;align-items:center;gap:8px;justify-content:center;\n  border:1px solid transparent;border-radius:999px;cursor:pointer;\n  padding:10px 17px;font-size:13.5px;font-weight:600;letter-spacing:.015em;\n  background:transparent;color:var(--ink);text-decoration:none!important;\n  transition:background .18s ease,border-color .18s ease,color .18s ease,transform .1s ease;\n  white-space:nowrap;\n}\n.btn:active{transform:translateY(1px)}\n.btn:focus-visible,.shard-open:focus-visible,.chip:focus-visible,.icon-btn:focus-visible,.tint-swatch:focus-visible{outline:2px solid var(--gold);outline-offset:3px}\n.btn-ghost{border-color:var(--line);color:var(--muted);background:rgba(8,25,29,.35)}\n.btn-ghost:hover{border-color:var(--line-strong);color:var(--ink);background:rgba(14,42,46,.6);text-decoration:none}\n.btn-gold{\n  background:linear-gradient(180deg,var(--gold-bright),var(--gold));\n  color:#1e1405;border-color:rgba(255,255,255,.18);\n  box-shadow:0 8px 26px rgba(233,184,114,.2), inset 0 1px 0 rgba(255,255,255,.4);\n}\n.btn-gold:hover{filter:brightness(1.045);text-decoration:none;box-shadow:0 10px 30px rgba(233,184,114,.28), inset 0 1px 0 rgba(255,255,255,.4)}\n.btn-text{padding:10px 12px;color:var(--muted);font-weight:500}\n.btn-text:hover{color:var(--ink);text-decoration:none}\n.identity-chip{\n  display:inline-flex;align-items:center;gap:9px;padding:7px 14px 7px 8px;border-radius:999px;\n  border:1px solid var(--line);background:rgba(10,30,34,.45);cursor:pointer;font-size:13px;color:var(--muted);\n  max-width:230px;\n}\n.identity-chip:hover{border-color:var(--line-strong);color:var(--ink)}\n.identity-orb{\n  width:24px;height:24px;border-radius:50%;flex:none;\n  background:radial-gradient(circle at 34% 28%, rgba(246,207,146,.8), rgba(143,224,198,.35) 55%, rgba(10,34,39,.2));\n  border:1px solid rgba(255,255,255,.25);\n  display:grid;place-items:center;font-size:11px;font-weight:700;color:#12221d;\n}\n.identity-chip .identity-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:130px}\n.identity-chip .identity-state{font-size:11px;color:var(--faint)}\n\n/* ── Hero ───────────────────────────────────────────────────── */\n.hero{width:min(var(--wrap),calc(100% - 48px));margin:0 auto;padding:64px 0 0;max-width:var(--wrap)}\n.kicker{\n  display:inline-flex;align-items:center;gap:9px;\n  font-size:11px;letter-spacing:.21em;text-transform:uppercase;color:var(--dim);\n  margin:0 0 20px;\n}\n.kicker::before{content:\"\";width:26px;height:1px;background:linear-gradient(90deg,transparent,var(--dim))}\nh1.hero-title{\n  font-family:var(--serif);font-weight:400;font-size:clamp(40px,6.2vw,70px);\n  line-height:1.02;letter-spacing:-.018em;margin:0;max-width:15ch;\n  color:#f6f1e5;\n}\nh1.hero-title em{\n  font-style:italic;color:var(--sea);\n  background:linear-gradient(100deg,#c8f4e4,#8fe0c6 55%,#e9d6a9);\n  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;\n}\n.hero-bottom{display:flex;align-items:flex-end;justify-content:space-between;gap:36px;margin-top:26px}\n.lede{\n  max-width:52ch;margin:0;color:var(--muted);font-size:16.5px;line-height:1.65;\n}\n.lede strong{color:#dfe9e3;font-weight:500}\n.hero-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:none;padding-bottom:4px}\n.hero-hint{font-size:12px;color:var(--faint);letter-spacing:.02em;margin-top:12px}\n\n/* ── Tide bar ───────────────────────────────────────────────── */\n.tidebar{\n  width:min(var(--wrap),calc(100% - 48px));margin:52px auto 0;\n  border-top:1px solid var(--line);border-bottom:1px solid var(--line);\n  display:flex;align-items:stretch;gap:28px;padding:16px 0;flex-wrap:wrap;\n}\n.stat{display:flex;align-items:baseline;gap:9px;min-width:110px}\n.stat b{font-family:var(--serif);font-weight:400;font-size:27px;color:#f4efe3;line-height:1;font-variant-numeric:tabular-nums}\n.stat span{font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim)}\n.tide-right{margin-left:auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}\n.live{display:inline-flex;align-items:center;gap:8px;font-size:11.5px;color:var(--dim);letter-spacing:.03em}\n.live-dot{width:6px;height:6px;border-radius:50%;background:var(--sea);box-shadow:0 0 0 0 rgba(143,224,198,.45);animation:live 3.4s ease-out infinite}\n.live-dot.offline{background:var(--coral);animation:none;box-shadow:none}\n@keyframes live{0%{box-shadow:0 0 0 0 rgba(143,224,198,.35)}70%{box-shadow:0 0 0 7px rgba(143,224,198,0)}100%{box-shadow:0 0 0 0 rgba(143,224,198,0)}}\n.sync{font-size:11.5px;color:var(--faint);font-variant-numeric:tabular-nums}\n.icon-btn{\n  width:31px;height:31px;border-radius:50%;border:1px solid var(--line);background:rgba(8,25,29,.4);\n  display:grid;place-items:center;cursor:pointer;color:var(--muted);flex:none;\n  transition:color .18s,border-color .18s,transform .3s var(--ease);\n}\n.icon-btn:hover{color:var(--ink);border-color:var(--line-strong)}\n.icon-btn svg{width:14px;height:14px}\n.icon-btn.busy svg{animation:spin 1s linear infinite}\n@keyframes spin{to{transform:rotate(360deg)}}\n.notice{\n  width:min(var(--wrap),calc(100% - 48px));margin:14px auto 0;padding:10px 14px;border-radius:10px;\n  border:1px solid rgba(239,154,134,.25);background:rgba(239,154,134,.07);\n  color:#f0c3b8;font-size:13px;display:flex;align-items:center;gap:9px;flex-wrap:wrap;\n}\n.notice .btn{padding:4px 10px;font-size:12px;border-color:rgba(239,154,134,.35);color:#f6d4cb}\n\n/* ── Controls ───────────────────────────────────────────────── */\n.controls{\n  width:min(var(--wrap),calc(100% - 48px));margin:30px auto 0;\n  display:flex;align-items:center;gap:16px;flex-wrap:wrap;\n}\n.searchbox{\n  display:flex;align-items:center;gap:9px;flex:1 1 240px;max-width:340px;\n  border:1px solid var(--line);border-radius:999px;padding:9px 15px;\n  background:rgba(8,25,29,.45);\n  transition:border-color .18s,background .18s;\n}\n.searchbox:focus-within{border-color:rgba(143,224,198,.4);background:rgba(10,34,38,.7)}\n.searchbox svg{width:15px;height:15px;color:var(--dim);flex:none}\n.searchbox input{\n  background:transparent;border:0;outline:0;color:var(--ink);font:inherit;font-size:14px;width:100%;\n}\n.searchbox input::placeholder{color:var(--faint)}\n.searchbox kbd{\n  font-family:var(--mono);font-size:10px;color:var(--faint);border:1px solid var(--line);\n  border-radius:4px;padding:1px 5px;line-height:1.4;flex:none;\n}\n.chips{display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n.chip{\n  display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;background:transparent;\n  color:var(--dim);font-size:12px;padding:6px 11px;border-radius:999px;cursor:pointer;\n  transition:color .15s,background .15s,border-color .15s;\n}\n.chip:hover{color:var(--ink);background:rgba(255,255,255,.03)}\n.chip.active{color:var(--ink);border-color:var(--line-strong);background:rgba(255,255,255,.055)}\n.chip-dot{width:6px;height:6px;border-radius:50%;background:var(--c);box-shadow:0 0 8px var(--c);flex:none}\n.chip-count{font-variant-numeric:tabular-nums;font-size:10.5px;color:var(--faint)}\n.sortwrap{margin-left:auto;position:relative;display:flex;align-items:center;gap:8px}\n.sortwrap label{font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}\n.sortwrap select{\n  appearance:none;-webkit-appearance:none;background:rgba(8,25,29,.5);color:var(--muted);\n  border:1px solid var(--line);border-radius:999px;padding:8px 32px 8px 13px;font:inherit;font-size:12.5px;\n  cursor:pointer;outline:none;\n}\n.sortwrap select:focus-visible{border-color:var(--gold)}\n.sortwrap::after{\n  content:\"\";position:absolute;right:13px;top:50%;width:6px;height:6px;pointer-events:none;\n  border-right:1.4px solid var(--dim);border-bottom:1.4px solid var(--dim);\n  transform:translateY(-70%) rotate(45deg);\n}\n.shore-head{\n  width:min(var(--wrap),calc(100% - 48px));margin:38px auto 0;\n  display:flex;align-items:center;gap:16px;color:var(--faint);\n  font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;\n}\n.shore-head::after{content:\"\";height:1px;flex:1;background:linear-gradient(90deg,var(--line),transparent)}\n\n/* ── Shore / shards ─────────────────────────────────────────── */\n.shore{\n  width:min(var(--wrap),calc(100% - 48px));margin:20px auto 0;\n  display:grid;grid-template-columns:repeat(auto-fill,minmax(288px,1fr));gap:22px;\n  align-items:start;\n}\n.shard{\n  --tintc:#8fe0c6;\n  --tint-rgb:143,224,198;\n  position:relative;border-radius:22px 30px 20px 32px / 28px 20px 32px 22px;\n  background:\n    linear-gradient(152deg, rgba(255,255,255,.085), rgba(255,255,255,.012) 38%, transparent 68%),\n    radial-gradient(130% 120% at 12% 0%, rgba(var(--tint-rgb),.22), transparent 60%),\n    linear-gradient(168deg, rgba(14,42,47,.9), rgba(6,22,26,.88));\n  border:1px solid rgba(190,230,220,.16);\n  box-shadow:0 14px 40px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.075);\n  overflow:hidden;\n  transition:border-color .25s var(--ease),box-shadow .25s var(--ease),transform .25s var(--ease);\n  animation:cardin .65s var(--ease) var(--enter,0s) both, adrift var(--drift,11s) ease-in-out var(--delay,0s) infinite alternate;\n}\n@keyframes cardin{\n  0%{opacity:0;transform:translateY(14px) scale(.985)}\n  100%{opacity:1;transform:none}\n}\n.shard::before{\n  content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;\n  background:\n    radial-gradient(120% 60% at 90% 110%, rgba(255,255,255,.05), transparent 60%),\n    linear-gradient(120deg, transparent 55%, rgba(255,255,255,.035) 56%, transparent 62%);\n  mix-blend-mode:screen;opacity:.9;\n}\n.shard::after{\n  content:\"\";position:absolute;left:-20%;right:-20%;top:0;height:44%;\n  background:linear-gradient(180deg, rgba(255,255,255,.11), transparent);\n  clip-path:polygon(0 0,100% 0,74% 26%,42% 16%,18% 34%,0 22%);\n  opacity:.28;pointer-events:none;\n}\n.shard.tint-seafoam{--tintc:#8fe0c6;--tint-rgb:143,224,198;border-color:rgba(143,224,198,.25)}\n.shard.tint-amber{--tintc:#e9b872;--tint-rgb:233,184,114;border-color:rgba(233,184,114,.25)}\n.shard.tint-cobalt{--tintc:#87b8f5;--tint-rgb:135,184,245;border-color:rgba(135,184,245,.24)}\n.shard.tint-rose{--tintc:#efa2b4;--tint-rgb:239,162,180;border-color:rgba(239,162,180,.24)}\n.shard.tint-violet{--tintc:#b7a7f0;--tint-rgb:183,167,240;border-color:rgba(183,167,240,.25)}\n.shard:nth-child(5n+1){border-radius:30px 20px 34px 22px / 24px 32px 20px 30px;--drift:12.5s}\n.shard:nth-child(5n+2){border-radius:20px 32px 24px 30px / 32px 22px 30px 20px;--drift:10s}\n.shard:nth-child(5n+3){border-radius:26px 24px 20px 34px / 20px 30px 26px 24px;--drift:13.5s}\n.shard:nth-child(5n+4){border-radius:34px 22px 30px 24px / 26px 20px 34px 22px;--drift:9.5s}\n.shard:hover,.shard:focus-within{\n  border-color:rgba(220,245,238,.34);\n  box-shadow:0 24px 58px rgba(0,0,0,.34), 0 0 40px -18px var(--tintc), inset 0 1px 0 rgba(255,255,255,.1);\n}\n@keyframes adrift{from{transform:translateY(-2px)}to{transform:translateY(3px)}}\n.shard-open{\n  all:unset;display:block;width:100%;cursor:pointer;padding:24px 24px 20px;box-sizing:border-box;\n  position:relative;z-index:1;\n}\n.shard-meta{display:flex;align-items:center;gap:8px;margin-bottom:14px;padding-right:30px}\n.gem{width:7px;height:7px;border-radius:50%;background:var(--tintc);box-shadow:0 0 10px var(--tintc);flex:none}\n.shard-kind{font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}\n.shard-time{margin-left:auto;font-size:10.5px;color:var(--faint);font-variant-numeric:tabular-nums;white-space:nowrap}\n.shard-msg{\n  display:-webkit-box;\n  font-family:var(--serif);font-size:19px;line-height:1.48;color:#f0ebdf;margin:0;\n  display:-webkit-box;-webkit-line-clamp:7;-webkit-box-orient:vertical;overflow:hidden;\n  overflow-wrap:anywhere;white-space:pre-line;\n  min-height:3em;\n}\n.shard-from{\n  display:flex;align-items:center;gap:9px;margin-top:18px;padding-top:14px;\n  border-top:1px solid rgba(190,230,220,.1);\n  font-size:11.5px;color:var(--dim);min-width:0;\n}\n.author-orb{\n  width:20px;height:20px;border-radius:50%;flex:none;display:grid;place-items:center;\n  font-size:9px;font-weight:700;color:#0c201d;\n  background:linear-gradient(145deg, rgba(246,207,146,.95), var(--tintc,#8fe0c6));\n  border:1px solid rgba(255,255,255,.25);\n}\n.author-name{color:var(--muted);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.author-pk{font-family:var(--mono);font-size:9.5px;color:var(--faint);white-space:nowrap;flex:none}\n.shard-open .read-hint{\n  margin-left:auto;flex:none;width:22px;height:22px;border-radius:50%;\n  border:1px solid var(--line);display:grid;place-items:center;color:var(--dim);\n  transition:border-color .2s,color .2s,transform .25s var(--ease);\n}\n.shard:hover .read-hint,.shard:focus-within .read-hint{color:var(--ink);border-color:rgba(220,245,238,.35);transform:translateX(2px)}\n.read-hint svg{width:10px;height:10px}\n.node-link{\n  position:absolute;top:14px;right:14px;z-index:3;width:22px;height:22px;border-radius:50%;\n  display:grid;place-items:center;color:var(--dim);opacity:.38;\n  transition:opacity .18s,color .18s,background .18s;background:transparent;\n}\n.node-link:hover{opacity:1;color:var(--ink);background:rgba(255,255,255,.06);text-decoration:none}\n.node-link svg{width:12px;height:12px}\n.shard.fresh{\n  animation:arrive 1.5s var(--ease) both, adrift 11s ease-in-out 1.5s infinite alternate;\n  border-color:rgba(246,207,146,.55);\n  box-shadow:0 0 0 3px rgba(246,207,146,.08), 0 18px 50px rgba(0,0,0,.3);\n}\n@keyframes arrive{\n  0%{opacity:0;transform:translateY(-26px) scale(.97);filter:brightness(1.6)}\n  100%{opacity:1;transform:none;filter:none}\n}\n\n/* Skeletons */\n.skeleton{\n  height:250px;border-radius:24px 30px 22px 32px / 28px 22px 32px 24px;\n  border:1px solid rgba(190,230,220,.08);\n  background:\n    linear-gradient(100deg, rgba(255,255,255,.025) 20%, rgba(255,255,255,.07) 38%, rgba(255,255,255,.025) 56%) 0 0/220% 100% no-repeat,\n    linear-gradient(168deg, rgba(14,42,47,.7), rgba(6,22,26,.6));\n  animation:shimmer 1.8s ease-in-out infinite;\n}\n@keyframes shimmer{from{background-position:130% 0,0 0}to{background-position:-80% 0,0 0}}\n\n/* Empty + error */\n.empty{\n  width:min(var(--wrap),calc(100% - 48px));margin:44px auto 0;\n  border:1px dashed rgba(190,230,220,.16);border-radius:26px;\n  background:radial-gradient(80% 120% at 50% 0%, rgba(143,224,198,.055), transparent 65%);\n  padding:64px 28px;text-align:center;\n}\n.empty-bottle{width:64px;margin:0 auto 22px;opacity:.9}\n.empty h2{font-family:var(--serif);font-weight:400;font-size:30px;margin:0 0 10px;color:#f1ecdf}\n.empty p{color:var(--dim);max-width:46ch;margin:0 auto 22px;font-size:14.5px;line-height:1.65}\n.empty .btn{margin:0 4px}\n.no-results{\n  width:min(var(--wrap),calc(100% - 48px));margin:34px auto 0;padding:38px 24px;\n  border:1px dashed var(--line);border-radius:20px;text-align:center;color:var(--dim);\n}\n.no-results b{display:block;color:var(--muted);font-family:var(--serif);font-weight:400;font-size:22px;margin-bottom:6px}\n\n/* ── Footer ─────────────────────────────────────────────────── */\n.footer{\n  width:min(var(--wrap),calc(100% - 48px));margin:96px auto 0;padding:30px 0 38px;\n  border-top:1px solid var(--line);\n  display:flex;align-items:flex-start;justify-content:space-between;gap:24px;flex-wrap:wrap;\n  color:var(--faint);font-size:11.5px;line-height:1.7;\n}\n.footer a{color:var(--dim)}\n.footer-left{max-width:56ch}\n.footer-brand{display:flex;align-items:center;gap:8px;color:var(--muted);margin-bottom:8px;font-family:var(--serif);font-size:16px;letter-spacing:.02em}\n.footer-brand svg{width:14px;height:18px;color:var(--sea)}\n.footer-ref{font-family:var(--mono);font-size:9.5px;color:var(--faint);margin-top:9px;word-break:break-all}\n.footer-right{text-align:right}\n.footer-right p{margin:0 0 6px}\n.footer-links{display:flex;gap:14px;justify-content:flex-end;flex-wrap:wrap}\n\n/* ── Modal base ─────────────────────────────────────────────── */\n.modal{\n  position:fixed;inset:0;z-index:9000;display:flex;align-items:center;justify-content:center;\n  padding:24px;background:rgba(2,9,11,.66);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);\n  opacity:0;transition:opacity .22s ease;\n}\n.modal.open{opacity:1}\n.modal[hidden]{display:none!important}\n.sheet{\n  position:relative;width:min(720px,100%);max-height:min(86vh,900px);overflow:auto;overscroll-behavior:contain;\n  background:\n    radial-gradient(120% 60% at 100% 0%, rgba(233,184,114,.075), transparent 55%),\n    linear-gradient(165deg,#0d272c 0%,#081d21 55%,#07181b 100%);\n  border:1px solid rgba(190,230,220,.22);border-radius:24px;\n  box-shadow:var(--shadow), inset 0 1px 0 rgba(255,255,255,.08);\n  padding:32px 34px 28px;transform:translateY(12px) scale(.99);\n  transition:transform .28s var(--ease);\n  scrollbar-width:thin;scrollbar-color:rgba(190,230,220,.2) transparent;\n}\n.modal.open .sheet{transform:none}\n.sheet::-webkit-scrollbar{width:10px}\n.sheet::-webkit-scrollbar-thumb{background:rgba(190,230,220,.16);border-radius:10px;border:3px solid transparent;background-clip:content-box}\n.sheet-close{\n  position:absolute;top:16px;right:16px;width:32px;height:32px;border-radius:50%;\n  border:1px solid var(--line);background:rgba(5,18,21,.5);display:grid;place-items:center;\n  color:var(--muted);cursor:pointer;z-index:4;transition:color .15s,border-color .15s;\n}\n.sheet-close:hover{color:var(--ink);border-color:var(--line-strong)}\n.sheet-close svg{width:13px;height:13px}\n.eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim);display:flex;align-items:center;gap:9px;margin-bottom:16px}\n.eyebrow .gem{box-shadow:0 0 12px var(--tintc)}\n.reader-tint{--tintc:#8fe0c6}\n.reader-tint.seafoam{--tintc:#8fe0c6}.reader-tint.amber{--tintc:#e9b872}.reader-tint.cobalt{--tintc:#87b8f5}.reader-tint.rose{--tintc:#efa2b4}.reader-tint.violet{--tintc:#b7a7f0}\n.reader-msg{\n  font-family:var(--serif);font-size:clamp(22px,3.1vw,29px);line-height:1.5;letter-spacing:-.005em;\n  color:#f5f0e4;margin:0;white-space:pre-wrap;overflow-wrap:anywhere;\n}\n.reader-msg::before{content:\"“\";color:var(--tintc);opacity:.55;margin-left:-.02em}\n.reader-msg::after{content:\"”\";color:var(--tintc);opacity:.55}\n.reader-byline{\n  display:flex;align-items:center;gap:12px;margin:28px 0 0;padding-top:20px;border-top:1px solid var(--line);\n  flex-wrap:wrap;\n}\n.reader-byline .author-orb{width:32px;height:32px;font-size:12px}\n.reader-author{font-size:14px;color:#dfe9e3;font-weight:600}\n.reader-pk{font-family:var(--mono);font-size:10px;color:var(--faint);word-break:break-all;margin-top:2px}\n.reader-stamp{margin-left:auto;text-align:right;font-size:11px;color:var(--faint);line-height:1.6}\n.reader-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:24px}\n.reader-actions .btn{padding:8px 14px;font-size:12.5px}\n.reader-actions .icon-btn{width:33px;height:33px}\n.reader-nav{\n  display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:24px;padding-top:16px;\n  border-top:1px solid var(--line);\n}\n.reader-nav .btn{padding:7px 12px;font-size:12px;color:var(--dim);border-color:transparent}\n.reader-nav .btn:hover{color:var(--ink);border-color:var(--line)}\n.provenance{\n  margin-top:18px;font-size:11px;color:var(--faint);\n  display:flex;gap:8px;flex-wrap:wrap;align-items:center;\n}\n.provenance .ref-pill{\n  font-family:var(--mono);font-size:9.5px;color:var(--dim);border:1px solid var(--line);\n  padding:3px 7px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\n}\n\n/* Composer */\n.sheet.composer{width:min(620px,100%)}\n.composer h2,.about h2{font-family:var(--serif);font-weight:400;font-size:33px;letter-spacing:-.01em;margin:0 0 8px;color:#f5f0e4}\n.composer .sub{color:var(--dim);font-size:13.5px;margin:0 0 22px;max-width:52ch;line-height:1.6}\n.identity-slot{\n  display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:14px;\n  border:1px solid var(--line);background:rgba(5,18,21,.38);margin-bottom:20px;\n}\n.identity-slot .author-orb{width:32px;height:32px;font-size:12px}\n.identity-slot .slot-text{min-width:0;flex:1}\n.slot-name{font-size:13.5px;color:var(--ink);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.slot-state{font-size:11px;color:var(--dim);margin-top:1px}\n.slot-state.ok{color:var(--sea)}\n.identity-slot .btn{padding:7px 13px;font-size:12px;flex:none}\n.field-label{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px}\n.field-label label{font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--dim)}\n.counter{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-variant-numeric:tabular-nums}\n.counter.warn{color:var(--gold)}\n.counter.max{color:var(--coral)}\n.composer textarea{\n  width:100%;min-height:158px;resize:vertical;border-radius:16px;border:1px solid var(--line);\n  background:rgba(4,15,18,.55);color:var(--ink);font-family:var(--serif);font-size:19px;line-height:1.55;\n  padding:18px 18px;outline:none;transition:border-color .18s,background .18s;\n  display:block;\n}\n.composer textarea::placeholder{color:var(--faint);font-style:italic}\n.composer textarea:focus{border-color:rgba(143,224,198,.42);background:rgba(5,20,23,.75)}\n.tint-row{display:flex;align-items:center;gap:14px;margin:18px 0 20px;flex-wrap:wrap}\n.tint-label{font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--dim)}\n.tints{display:flex;gap:10px}\n.tint-swatch{\n  width:30px;height:30px;border-radius:50%;border:1px solid rgba(255,255,255,.22);cursor:pointer;\n  background:radial-gradient(circle at 34% 28%, rgba(255,255,255,.65), var(--sw) 42%, #04181b 100%);\n  background:radial-gradient(circle at 34% 28%, rgba(255,255,255,.65), var(--sw) 42%, color-mix(in srgb, var(--sw) 55%, #04181b) 100%);\n  box-shadow:inset 0 1px 2px rgba(255,255,255,.4), 0 2px 8px rgba(0,0,0,.3);\n  transition:transform .16s var(--ease),box-shadow .16s;\n  position:relative;padding:0;\n}\n.tint-swatch:hover{transform:translateY(-1px) scale(1.06)}\n.tint-swatch[aria-pressed=\"true\"]{box-shadow:0 0 0 2px #081d21,0 0 0 3.5px var(--sw),0 2px 10px rgba(0,0,0,.3)}\n.tint-name{font-size:12px;color:var(--dim);margin-left:2px;min-width:60px}\n.disclosure{\n  display:flex;gap:11px;align-items:flex-start;border-radius:12px;padding:12px 14px;\n  background:rgba(233,184,114,.055);border:1px solid rgba(233,184,114,.17);\n  color:#d9c7a5;font-size:11.5px;line-height:1.6;\n}\n.disclosure svg{width:15px;height:15px;flex:none;margin-top:1px;color:var(--gold)}\n.disclosure strong{color:#f0dcb9;font-weight:600}\n.composer-foot{display:flex;align-items:center;gap:10px;margin-top:24px;flex-wrap:wrap}\n.composer-foot .hint{font-size:11px;color:var(--faint);margin-right:auto}\n.composer-error{color:var(--coral);font-size:12px;margin-top:12px;line-height:1.5;display:none}\n.composer-success{text-align:center;padding:24px 6px 10px}\n.success-seal{\n  width:70px;height:70px;margin:0 auto 20px;border-radius:50%;display:grid;place-items:center;\n  background:radial-gradient(circle at 35% 30%, rgba(143,224,198,.28), rgba(10,40,40,.6));\n  border:1px solid rgba(143,224,198,.35);box-shadow:0 0 40px rgba(143,224,198,.16);\n}\n.success-seal svg{width:30px;height:30px;color:var(--sea)}\n.composer-success h3{font-family:var(--serif);font-weight:400;font-size:28px;margin:0 0 8px;color:#f4efe3}\n.composer-success p{color:var(--dim);font-size:13.5px;max-width:40ch;margin:0 auto 22px}\n.composer-success .success-preview{\n  font-family:var(--serif);font-size:17px;line-height:1.55;color:#e8e2d4;text-align:left;\n  border-left:2px solid var(--sea);padding:2px 0 2px 16px;margin:18px auto 24px;max-width:420px;\n  white-space:pre-wrap;overflow-wrap:anywhere;\n}\n.success-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}\n\n/* About */\n.sheet.about{width:min(700px,100%)}\n.about-lede{font-family:var(--serif);font-size:20px;line-height:1.6;color:#e8e2d4;margin:0 0 26px;max-width:56ch}\n.about-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:18px;margin:0 0 26px}\n.about-card{border-top:1px solid var(--line-strong);padding-top:12px}\n.about-card h3{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold);margin:0 0 7px;font-weight:600}\n.about-card p{margin:0;font-size:12.5px;color:var(--dim);line-height:1.65}\n.about-card code,.about-note code{font-family:var(--mono);font-size:10.5px;color:#cfdcd5;background:rgba(255,255,255,.05);padding:1px 4px;border-radius:3px}\n.about-note{border:1px solid var(--line);border-radius:12px;padding:13px 15px;font-size:12px;color:var(--dim);line-height:1.7;margin-bottom:22px;background:rgba(5,18,21,.32)}\n.about-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:20px}\n.about-links .btn{padding:7px 13px;font-size:12px;border-color:var(--line);color:var(--muted)}\n.about-links .btn:hover{color:var(--ink);border-color:var(--line-strong);text-decoration:none}\n.about-refs{font-family:var(--mono);font-size:9.5px;line-height:1.8;color:var(--faint);margin-top:20px;padding-top:14px;border-top:1px solid var(--line);word-break:break-all}\n\n/* Toast */\n.toast{\n  position:fixed;left:50%;bottom:28px;transform:translate(-50%,16px);z-index:10050;\n  max-width:min(520px,calc(100% - 36px));padding:11px 18px;border-radius:999px;\n  background:rgba(12,37,41,.96);border:1px solid var(--line-strong);color:var(--ink);\n  font-size:13px;box-shadow:0 16px 50px rgba(0,0,0,.45);opacity:0;pointer-events:none;\n  transition:opacity .22s,transform .22s;text-align:center;\n}\n.toast.show{opacity:1;transform:translate(-50%,0)}\n.toast.error{border-color:rgba(239,154,134,.38);color:#f4c8bd}\n.toast.ok{border-color:rgba(143,224,198,.4)}\n\n/* Seek/scroll cue */\n.scroll-cue{\n  display:inline-flex;align-items:center;gap:8px;color:var(--faint);font-size:11.5px;\n  letter-spacing:.04em;cursor:pointer;background:none;border:0;padding:4px 0;margin-top:14px;\n}\n.scroll-cue:hover{color:var(--muted)}\n.scroll-cue svg{width:12px;height:12px;animation:nudge 2.4s ease-in-out infinite}\n@keyframes nudge{50%{transform:translateY(3px)}}\n\n/* ── Responsive ─────────────────────────────────────────────── */\n@media (max-width:940px){\n  .hero-bottom{flex-direction:column;align-items:flex-start;gap:22px}\n  .hero-actions{padding-bottom:0}\n  .tidebar{gap:18px 26px}\n  .tide-right{margin-left:0}\n  .sortwrap{margin-left:0}\n}\n@media (max-width:680px){\n  .wrap,.topbar,.hero,.tidebar,.controls,.shore,.shore-head,.empty,.no-results,.footer,.notice{width:calc(100% - 32px)}\n  .topbar{padding-top:20px;gap:10px}\n  .brand-name{font-size:22px}\n  .brand-sub{font-size:9px;letter-spacing:.14em}\n  .brand-mark{width:34px;height:34px}\n  .brand-mark svg{width:19px;height:24px}\n  .toplinks{gap:2px}\n  .toplinks .btn{padding:8px 11px;font-size:12px}\n  #aboutBtn,#castBtnTop{display:none}\n  .identity-chip{padding:5px 10px 5px 6px;font-size:12px}\n  .identity-chip .identity-name{max-width:74px}\n  .identity-orb{width:20px;height:20px}\n  .hero{padding-top:42px}\n  h1.hero-title{font-size:clamp(36px,10vw,48px)}\n  .lede{font-size:15px}\n  .tidebar{margin-top:36px;gap:14px 10px;padding:14px 0;display:grid;grid-template-columns:repeat(3,1fr)}\n  .stat{display:block;min-width:0}\n  .stat b{display:block;font-size:23px;margin-bottom:4px}\n  .stat span{font-size:9.5px;letter-spacing:.06em;white-space:nowrap}\n  .tide-right{grid-column:1/-1;width:100%;justify-content:space-between}\n  .controls{margin-top:22px;gap:12px}\n  .searchbox{max-width:none;flex-basis:100%}\n  .searchbox kbd{display:none}\n  .chips{width:100%;overflow-x:auto;flex-wrap:nowrap;padding-bottom:2px;scrollbar-width:none}\n  .chips::-webkit-scrollbar{display:none}\n  .chip{flex:none}\n  .sortwrap{width:100%;justify-content:space-between}\n  .shore{grid-template-columns:1fr;gap:18px;margin-top:16px}\n  .shore-head{margin-top:30px}\n  .shard-msg{font-size:18.5px}\n  .footer{margin-top:70px;flex-direction:column}\n  .footer-right{text-align:left}\n  .footer-links{justify-content:flex-start}\n  .modal{padding:0;align-items:flex-end}\n  .sheet{width:100%;max-height:92vh;border-radius:22px 22px 0 0;padding:26px 20px 24px}\n  .sheet-close{top:12px;right:12px}\n  .composer textarea{font-size:17px}\n  .reader-msg{font-size:22px}\n  .reader-stamp{margin-left:0;text-align:left;width:100%}\n  .reader-actions .btn{flex:1 1 auto}\n  .toast{bottom:18px}\n}\n@media (prefers-reduced-motion:reduce){\n  html{scroll-behavior:auto}\n  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}\n  .shard{animation:none}\n  .reveal{opacity:1;transform:none}\n}\n</style>\n</head>\n<body>\n<div class=\"sky\" aria-hidden=\"true\"></div>\n<div class=\"moon\" aria-hidden=\"true\"></div>\n<div class=\"sea\" aria-hidden=\"true\"><span></span><span></span><span></span></div>\n<div class=\"grain\" aria-hidden=\"true\"></div>\n\n<div class=\"site\">\n  <header class=\"topbar\">\n    <a class=\"brand\" href=\"#\" aria-label=\"Seaglass home\">\n      <span class=\"brand-mark\">\n        <svg viewBox=\"0 0 32 40\" fill=\"none\" aria-hidden=\"true\">\n          <path d=\"M12 2h8v6l4.6 7.4V33a5 5 0 0 1-5 5h-7.2a5 5 0 0 1-5-5V15.4L12 8V2Z\" stroke=\"#8fe0c6\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/>\n          <path d=\"M10.5 1.4h11\" stroke=\"#e9b872\" stroke-width=\"1.4\" stroke-linecap=\"round\"/>\n          <path d=\"M9.4 16.5h13.2\" stroke=\"rgba(143,224,198,.35)\" stroke-width=\"1\"/>\n          <path d=\"M12.4 20.5h7.2l2 2.5v7.2l-2.4 2.3h-6.4l-2-2.3V23l1.6-2.5Z\" fill=\"rgba(233,184,114,.2)\" stroke=\"rgba(233,184,114,.8)\" stroke-width=\"1\"/>\n          <path d=\"M14 25h4M14 28h4\" stroke=\"rgba(233,184,114,.65)\" stroke-width=\".9\" stroke-linecap=\"round\"/>\n        </svg>\n      </span>\n      <span>\n        <span class=\"brand-name\">Seaglass</span>\n        <span class=\"brand-sub\">a shore on the dataverse</span>\n      </span>\n    </a>\n    <div class=\"topbar-spacer\"></div>\n    <nav class=\"toplinks\" aria-label=\"Site\">\n      <button class=\"btn btn-text\" id=\"aboutBtn\" type=\"button\">How it works</button>\n      <button class=\"identity-chip\" id=\"identityBtn\" type=\"button\" aria-label=\"Identity\">\n        <span class=\"identity-orb\" id=\"identityOrb\">·</span>\n        <span class=\"identity-name\" id=\"identityName\">Sign in</span>\n        <span class=\"identity-state\" id=\"identityState\"></span>\n      </button>\n      <button class=\"btn btn-gold\" id=\"castBtnTop\" type=\"button\">\n        <svg width=\"13\" height=\"16\" viewBox=\"0 0 24 30\" fill=\"none\" aria-hidden=\"true\"><path d=\"M9 2h6v5l3 5v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V12l3-5V2Z\" stroke=\"currentColor\" stroke-width=\"1.7\"/><path d=\"M8 1h8\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"/></svg>\n        Cast a bottle\n      </button>\n    </nav>\n  </header>\n\n  <main>\n    <section class=\"hero\" aria-labelledby=\"heroTitle\">\n      <p class=\"kicker\">tideline&nbsp;01 · public messages</p>\n      <h1 class=\"hero-title\" id=\"heroTitle\">Messages in bottles,<br><em>washed up on the graph.</em></h1>\n      <div class=\"hero-bottom\">\n        <div>\n          <p class=\"lede\">Seaglass is a quiet shore on the <a href=\"https://dataverse001.net/AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000\" target=\"_blank\" rel=\"noopener\">dataverse</a>. Every bottle is a <strong>signed message</strong> from a real person or agent — left in public, for whoever happens to find it. No feed. No ranking. No algorithm deciding what you should see.</p>\n          <button class=\"scroll-cue\" id=\"browseBtn\" type=\"button\">\n            Walk the tideline\n            <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M8 2v11M3.5 8.5 8 13l4.5-4.5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n          </button>\n        </div>\n        <div class=\"hero-actions\">\n          <button class=\"btn btn-gold\" id=\"castBtnHero\" type=\"button\">Cast a bottle</button>\n          <button class=\"btn btn-ghost\" id=\"aboutBtn2\" type=\"button\">What is this place?</button>\n          <p class=\"hero-hint\" style=\"flex-basis:100%;margin:2px 0 0\">Public on this hub · signed by its author · permanent</p>\n        </div>\n      </div>\n    </section>\n\n    <section class=\"tidebar\" aria-label=\"Tideline statistics\">\n      <div class=\"stat\"><b id=\"statBottles\">—</b><span>bottles ashore</span></div>\n      <div class=\"stat\"><b id=\"statVoices\">—</b><span>voices</span></div>\n      <div class=\"stat\"><b id=\"statFresh\">—</b><span>this week</span></div>\n      <div class=\"tide-right\">\n        <span class=\"live\"><span class=\"live-dot\" id=\"liveDot\"></span><span id=\"liveLabel\">reading the tide at dataverse001.net</span></span>\n        <span class=\"sync\" id=\"lastSync\"></span>\n        <button class=\"icon-btn\" id=\"refreshBtn\" type=\"button\" aria-label=\"Refresh the tideline\" title=\"Refresh\">\n          <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M13.4 6.4A5.7 5.7 0 1 0 14 8\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><path d=\"M13.8 2.4v4.2h-4.2\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n        </button>\n      </div>\n    </section>\n\n    <div id=\"offlineNotice\" class=\"notice\" hidden></div>\n\n    <section class=\"controls\" aria-label=\"Find a bottle\">\n      <label class=\"searchbox\">\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><circle cx=\"7\" cy=\"7\" r=\"4.6\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"m10.6 10.6 3 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>\n        <input id=\"searchInput\" type=\"search\" placeholder=\"Search the tideline…\" autocomplete=\"off\" aria-label=\"Search messages\">\n        <kbd>/</kbd>\n      </label>\n      <div class=\"chips\" id=\"tintChips\" role=\"group\" aria-label=\"Filter by glass color\">\n        <button class=\"chip active\" type=\"button\" data-tint=\"all\" aria-pressed=\"true\"><span class=\"chip-count\" id=\"countAll\">—</span>&nbsp;all</button>\n        <button class=\"chip\" type=\"button\" data-tint=\"seafoam\" aria-pressed=\"false\"><span class=\"chip-dot\" style=\"--c:#8fe0c6\"></span>seafoam <span class=\"chip-count\" data-count=\"seafoam\">—</span></button>\n        <button class=\"chip\" type=\"button\" data-tint=\"amber\" aria-pressed=\"false\"><span class=\"chip-dot\" style=\"--c:#e9b872\"></span>amber <span class=\"chip-count\" data-count=\"amber\">—</span></button>\n        <button class=\"chip\" type=\"button\" data-tint=\"cobalt\" aria-pressed=\"false\"><span class=\"chip-dot\" style=\"--c:#87b8f5\"></span>cobalt <span class=\"chip-count\" data-count=\"cobalt\">—</span></button>\n        <button class=\"chip\" type=\"button\" data-tint=\"rose\" aria-pressed=\"false\"><span class=\"chip-dot\" style=\"--c:#efa2b4\"></span>rose <span class=\"chip-count\" data-count=\"rose\">—</span></button>\n        <button class=\"chip\" type=\"button\" data-tint=\"violet\" aria-pressed=\"false\"><span class=\"chip-dot\" style=\"--c:#b7a7f0\"></span>violet <span class=\"chip-count\" data-count=\"violet\">—</span></button>\n      </div>\n      <div class=\"sortwrap\">\n        <label for=\"sortSelect\">order</label>\n        <select id=\"sortSelect\">\n          <option value=\"new\">Freshly washed</option>\n          <option value=\"old\">Longest adrift</option>\n          <option value=\"random\">Wherever the tide takes me</option>\n        </select>\n      </div>\n    </section>\n\n    <div class=\"shore-head\"><span id=\"shoreLabel\">what washed ashore</span></div>\n    <section class=\"shore\" id=\"shore\" aria-live=\"polite\" aria-busy=\"true\"></section>\n\n    <section class=\"empty\" id=\"emptyState\" hidden>\n      <svg class=\"empty-bottle\" viewBox=\"0 0 64 80\" fill=\"none\" aria-hidden=\"true\">\n        <path d=\"M24 4h16v12l8.6 14V66a8 8 0 0 1-8 8H23.4a8 8 0 0 1-8-8V30L24 16V4Z\" stroke=\"rgba(143,224,198,.55)\" stroke-width=\"1.6\" stroke-linejoin=\"round\"/>\n        <path d=\"M21 2.8h22\" stroke=\"rgba(233,184,114,.8)\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\n        <path d=\"M27 38h10l3 4v12l-4 3H27l-3-3V42l3-4Z\" fill=\"rgba(233,184,114,.14)\" stroke=\"rgba(233,184,114,.55)\" stroke-width=\"1.2\"/>\n        <path d=\"M28 47h8M28 51h8\" stroke=\"rgba(233,184,114,.4)\" stroke-width=\"1\" stroke-linecap=\"round\"/>\n      </svg>\n      <h2 id=\"emptyTitle\">The tide is quiet.</h2>\n      <p id=\"emptyText\">No bottles have washed ashore yet. If you have something to say to a stranger on the dataverse, be the first.</p>\n      <button class=\"btn btn-gold\" id=\"emptyCastBtn\" type=\"button\">Cast the first bottle</button>\n    </section>\n  </main>\n\n  <footer class=\"footer\">\n    <div class=\"footer-left\">\n      <div class=\"footer-brand\">\n        <svg viewBox=\"0 0 24 30\" fill=\"none\" aria-hidden=\"true\"><path d=\"M9 2h6v5l3 5v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V12l3-5V2Z\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M8 1h8\" stroke=\"#e9b872\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>\n        Seaglass\n      </div>\n      <p>A public shore made of signed objects. The interface is a <code style=\"font-family:var(--mono);font-size:10px\">PAGE</code>; each bottle is its own object, still readable even if this app washes away.</p>\n      <p class=\"footer-ref\" id=\"footerRef\">application <span id=\"appRefText\"></span></p>\n    </div>\n    <div class=\"footer-right\">\n      <p>no accounts · no tracking · no algorithm</p>\n      <div class=\"footer-links\">\n        <a href=\"https://dataverse001.net/AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000\" target=\"_blank\" rel=\"noopener\">dataverse root ↗</a>\n        <a href=\"https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.34da21ed-2929-40a4-b0dc-52c4e1983fb3\" target=\"_blank\" rel=\"noopener\">this app, raw ↗</a>\n        <a href=\"https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.5c12c354-d9b6-468e-85f9-361fdbaedb22\" target=\"_blank\" rel=\"noopener\">this page, signed ↗</a>\n      </div>\n    </div>\n  </footer>\n</div>\n\n<!-- Reader -->\n<div class=\"modal\" id=\"readerModal\" hidden>\n  <div class=\"sheet reader\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"readerHeading\">\n    <button class=\"sheet-close\" type=\"button\" data-close=\"readerModal\" aria-label=\"Close\">\n      <svg viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\"><path d=\"m2 2 10 10M12 2 2 12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>\n    </button>\n    <div class=\"reader-tint\" id=\"readerTint\">\n      <div class=\"eyebrow\"><span class=\"gem\"></span><span id=\"readerHeading\">Bottle</span><span id=\"readerTime\"></span></div>\n      <p class=\"reader-msg\" id=\"readerMessage\"></p>\n      <div class=\"reader-byline\">\n        <span class=\"author-orb\" id=\"readerOrb\">·</span>\n        <div>\n          <div class=\"reader-author\" id=\"readerAuthor\">—</div>\n          <div class=\"reader-pk\" id=\"readerPk\"></div>\n        </div>\n        <div class=\"reader-stamp\" id=\"readerStamp\"></div>\n      </div>\n      <div class=\"reader-actions\">\n        <a class=\"btn btn-ghost\" id=\"readerNodeLink\" target=\"_blank\" rel=\"noopener\">\n          <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><circle cx=\"4\" cy=\"8\" r=\"2\" stroke=\"currentColor\" stroke-width=\"1.3\"/><circle cx=\"12\" cy=\"4\" r=\"2\" stroke=\"currentColor\" stroke-width=\"1.3\"/><circle cx=\"12\" cy=\"12\" r=\"2\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"m5.8 7.1 4.4-2.2M5.8 8.9l4.4 2.2\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>\n          Node Viewer\n        </a>\n        <a class=\"btn btn-ghost\" id=\"readerRawLink\" target=\"_blank\" rel=\"noopener\">Raw object ↗</a>\n        <button class=\"icon-btn\" id=\"readerCopyBtn\" type=\"button\" aria-label=\"Copy a share link to this bottle\" title=\"Copy share link\">\n          <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6.5 9.5a2.5 2.5 0 0 0 3.6.1l2.1-2.1a2.5 2.5 0 1 0-3.5-3.5l-1 1\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><path d=\"M9.5 6.5a2.5 2.5 0 0 0-3.6-.1L3.8 8.5a2.5 2.5 0 1 0 3.5 3.5l1-1\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>\n        </button>\n      </div>\n      <div class=\"provenance\">\n        <span>sealed in the server-public realm</span>\n        <span class=\"ref-pill\" id=\"readerRef\"></span>\n      </div>\n      <div class=\"reader-nav\">\n        <button class=\"btn\" id=\"readerPrev\" type=\"button\">\n          <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 3 5 8l5 5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n          earlier bottle\n        </button>\n        <button class=\"btn\" id=\"readerNext\" type=\"button\">\n          next bottle\n          <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"m6 3 5 5-5 5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n        </button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<!-- Composer -->\n<div class=\"modal\" id=\"composerModal\" hidden>\n  <div class=\"sheet composer\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"composerTitle\">\n    <button class=\"sheet-close\" type=\"button\" data-close=\"composerModal\" aria-label=\"Close\">\n      <svg viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\"><path d=\"m2 2 10 10M12 2 2 12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>\n    </button>\n    <div id=\"composeForm\">\n      <div class=\"eyebrow\"><span class=\"gem\" style=\"--tintc:#e9b872;background:#e9b872\"></span>write &amp; seal</div>\n      <h2 id=\"composerTitle\">Cast a bottle</h2>\n      <p class=\"sub\">Write something worth finding. It will be signed with your dataverse identity and left in public on this hub — a small, permanent note for a stranger.</p>\n      <div class=\"identity-slot\" id=\"identitySlot\">\n        <span class=\"author-orb\" id=\"slotOrb\">?</span>\n        <div class=\"slot-text\">\n          <div class=\"slot-name\" id=\"slotName\">No identity connected</div>\n          <div class=\"slot-state\" id=\"slotState\">You need a dataverse identity to sign your bottle.</div>\n        </div>\n        <button class=\"btn btn-ghost\" id=\"connectBtn\" type=\"button\">Connect</button>\n      </div>\n      <div class=\"field-label\">\n        <label for=\"messageInput\">your message</label>\n        <span class=\"counter\" id=\"charCount\">0 / 600</span>\n      </div>\n      <textarea id=\"messageInput\" maxlength=\"600\" placeholder=\"To whoever finds this…\" aria-describedby=\"messageHelp\"></textarea>\n      <div class=\"tint-row\">\n        <span class=\"tint-label\">glass</span>\n        <div class=\"tints\" id=\"tints\" role=\"group\" aria-label=\"Choose glass color\">\n          <button class=\"tint-swatch\" type=\"button\" data-tint=\"seafoam\" style=\"--sw:#8fe0c6\" aria-label=\"Seafoam\" aria-pressed=\"false\"></button>\n          <button class=\"tint-swatch\" type=\"button\" data-tint=\"amber\" style=\"--sw:#e9b872\" aria-label=\"Amber\" aria-pressed=\"false\"></button>\n          <button class=\"tint-swatch\" type=\"button\" data-tint=\"cobalt\" style=\"--sw:#87b8f5\" aria-label=\"Cobalt\" aria-pressed=\"false\"></button>\n          <button class=\"tint-swatch\" type=\"button\" data-tint=\"rose\" style=\"--sw:#efa2b4\" aria-label=\"Rose\" aria-pressed=\"false\"></button>\n          <button class=\"tint-swatch\" type=\"button\" data-tint=\"violet\" style=\"--sw:#b7a7f0\" aria-label=\"Violet\" aria-pressed=\"false\"></button>\n        </div>\n        <span class=\"tint-name\" id=\"tintName\">seafoam</span>\n      </div>\n      <div class=\"disclosure\" id=\"messageHelp\">\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><circle cx=\"8\" cy=\"8\" r=\"6.4\" stroke=\"currentColor\" stroke-width=\"1.2\"/><path d=\"M8 7.2v4M8 4.9v.2\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>\n        <span><strong>Public on this hub.</strong> Bottles live in the <code style=\"font-family:var(--mono);font-size:10px\">server-public</code> realm at dataverse001.net, readable by anyone there. They are signed by your identity and stay in the graph. A new identity also publishes a public profile. Don’t put secrets in the sea.</span>\n      </div>\n      <div class=\"composer-error\" id=\"composerError\"></div>\n      <div class=\"composer-foot\">\n        <span class=\"hint\">Signed, not editable · one bottle at a time</span>\n        <button class=\"btn btn-ghost\" type=\"button\" data-close=\"composerModal\">Cancel</button>\n        <button class=\"btn btn-gold\" id=\"castSubmit\" type=\"button\">Seal &amp; cast</button>\n      </div>\n    </div>\n    <div class=\"composer-success\" id=\"composeSuccess\" hidden>\n      <div class=\"success-seal\">\n        <svg viewBox=\"0 0 24 30\" fill=\"none\" aria-hidden=\"true\"><path d=\"M9 2h6v5l3 5v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V12l3-5V2Z\" stroke=\"currentColor\" stroke-width=\"1.6\"/><path d=\"M8 1h8\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/><path d=\"m10 20 2 2 3.5-4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n      </div>\n      <h3>Your bottle is adrift.</h3>\n      <p>It has been signed and left on this hub. Someone might find it years from now — that’s the whole idea.</p>\n      <div class=\"success-preview\" id=\"successPreview\"></div>\n      <div class=\"success-actions\">\n        <button class=\"btn btn-gold\" id=\"readMineBtn\" type=\"button\">Read it on the shore</button>\n        <button class=\"btn btn-ghost\" type=\"button\" data-close=\"composerModal\">Let it go</button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<!-- About -->\n<div class=\"modal\" id=\"aboutModal\" hidden>\n  <div class=\"sheet about\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"aboutTitle\">\n    <button class=\"sheet-close\" type=\"button\" data-close=\"aboutModal\" aria-label=\"Close\">\n      <svg viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\"><path d=\"m2 2 10 10M12 2 2 12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>\n    </button>\n    <div class=\"eyebrow\"><span class=\"gem\" style=\"--tintc:#8fe0c6;background:#8fe0c6\"></span>how this place works</div>\n    <h2 id=\"aboutTitle\">A slow shore in a fast graph.</h2>\n    <p class=\"about-lede\">The dataverse is a global graph of signed, self-describing objects. Most of it is machinery. Seaglass is the part where someone leaves a note in the sand.</p>\n    <div class=\"about-grid\">\n      <div class=\"about-card\">\n        <h3>Bottles are objects</h3>\n        <p>Each message is a <code>BOTTLE</code> object — a small signed JSON fragment with an author, a timestamp, a sentence or two, and a color of glass.</p>\n      </div>\n      <div class=\"about-card\">\n        <h3>Signatures, not accounts</h3>\n        <p>There is no server account. Your bottle is signed by a keypair you hold. The signature is the authorship; it can be checked by anyone, forever.</p>\n      </div>\n      <div class=\"about-card\">\n        <h3>Public by intent</h3>\n        <p>Bottles here live in the <code>server-public</code> realm: readable by anyone on this hub, but not automatically propagated to the global commons.</p>\n      </div>\n    </div>\n    <div class=\"about-note\">\n      The shore fetches bottles through the hub’s inbound-relations API and caches the last tide in your browser so the page still has something to show when the network is gone. No analytics, no cookies of ours, no third-party fonts or scripts.\n    </div>\n    <div class=\"about-note\" style=\"border-color:rgba(233,184,114,.2);background:rgba(233,184,114,.045)\">\n      <strong style=\"color:#f0dcb9\">To cast a bottle</strong> you will need a dataverse identity. If you don’t have one, the connect panel can create one from a username and password — no email, no recovery. That creates a public identity profile; the bottle itself stays in this app’s server-public realm. Signing happens locally and your private key never leaves this browser in an extractable form.\n    </div>\n    <div class=\"about-links\">\n      <a class=\"btn\" href=\"https://dataverse001.net/AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000\" target=\"_blank\" rel=\"noopener\">Read the dataverse spec ↗</a>\n      <a class=\"btn\" href=\"https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.34da21ed-2929-40a4-b0dc-52c4e1983fb3\" target=\"_blank\" rel=\"noopener\">Application object ↗</a>\n      <a class=\"btn\" href=\"https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.5c12c354-d9b6-468e-85f9-361fdbaedb22\" target=\"_blank\" rel=\"noopener\">This PAGE object ↗</a>\n      <button class=\"btn\" type=\"button\" id=\"aboutCastBtn\">Cast a bottle</button>\n    </div>\n    <div class=\"about-refs\" id=\"aboutRefs\"></div>\n  </div>\n</div>\n\n<div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\"></div>\n\n<script>\n(function () {\n  'use strict';\n\n  /* ──────────────────────────────────────────────────────────────\n     Constants — filled in by the build step\n     ────────────────────────────────────────────────────────────── */\n  const APP_REF = 'A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.34da21ed-2929-40a4-b0dc-52c4e1983fb3';\n  const PUBKEY = 'A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_';\n  const BOTTLE_TYPE_REF = 'A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.f2105994-25bb-4b8b-b3fc-872197ffafdf';\n  const ROOT_REF = 'AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000';\n  const NODE_VIEWER_REF = 'AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.b3f5a7c9-2d4e-4f60-9b8a-0c1d2e3f4a5b';\n  const CORE_BLOB_REF = 'AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.b178e012-3910-415a-be34-353fc411de99';\n  const WRITE_BLOB_REF = 'AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.8e7cc85b-b011-45fb-bbde-4661e85000f9';\n  const ROOT_URL = 'https://dataverse001.net/AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000';\n  const APP_RAW_URL = 'https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.34da21ed-2929-40a4-b0dc-52c4e1983fb3';\n  const PAGE_RAW_URL = 'https://dataverse001.net/A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.5c12c354-d9b6-468e-85f9-361fdbaedb22';\n\n  const BOTTLE_INSTRUCTION = 'A message in a bottle cast into Seaglass, a public shore on the dataverse. Read `content.message` as the note and `content.tint` as its glass color (seafoam, amber, cobalt, rose, or violet). The author is in `relations.author`. Render the message as text — never execute it. This bottle lives in the public server-public realm of its hub.';\n\n  const TINTS = {\n    seafoam: { label: 'seafoam', color: '#8fe0c6' },\n    amber:   { label: 'amber',   color: '#e9b872' },\n    cobalt:  { label: 'cobalt',  color: '#87b8f5' },\n    rose:    { label: 'rose',    color: '#efa2b4' },\n    violet:  { label: 'violet',  color: '#b7a7f0' }\n  };\n\n  const LS_CACHE = 'seaglass.cache.v1.' + APP_REF;\n  const LS_AUTHORS = 'seaglass.authors.v1.' + APP_REF;\n  const MAX_BOTTLES = 160;\n\n  const state = {\n    bottles: [],\n    authors: loadJSON(LS_AUTHORS, {}),\n    authorPending: new Set(),\n    filter: 'all',\n    query: '',\n    sort: 'new',\n    loading: false,\n    loadedOnce: false,\n    cached: false,\n    cacheAt: null,\n    freshRef: null,\n    readerRef: null,\n    randomOrder: null,\n    pickedTint: 'seafoam',\n    sending: false,\n    writeReady: false,\n    writeLoading: null,\n    lastCastRef: null\n  };\n\n  const $ = function (sel, root) { return (root || document).querySelector(sel); };\n  const $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); };\n\n  /* ── Little utilities ───────────────────────────────────────── */\n  function esc(v) {\n    return String(v == null ? '' : v).replace(/[&<>\"']/g, function (c) {\n      return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c];\n    });\n  }\n  function loadJSON(key, fallback) {\n    try { var raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; }\n    catch (e) { return fallback; }\n  }\n  function saveJSON(key, value) {\n    try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* full or private mode */ }\n  }\n  function shortPk(pk) {\n    if (!pk || pk.length < 12) return pk || 'unknown';\n    return pk.slice(0, 6) + '…' + pk.slice(-4);\n  }\n  function initial(name) {\n    var s = String(name || '?').trim();\n    return (s[0] || '?').toUpperCase();\n  }\n  function parseDate(iso) {\n    var t = Date.parse(iso || '');\n    return isFinite(t) ? t : 0;\n  }\n  function ago(iso) {\n    var t = parseDate(iso);\n    if (!t) return 'unknown';\n    var s = (Date.now() - t) / 1000;\n    if (s < 45) return 'just now';\n    if (s < 3600) return Math.floor(s / 60) + 'm ago';\n    if (s < 86400) return Math.floor(s / 3600) + 'h ago';\n    if (s < 604800) return Math.floor(s / 86400) + 'd ago';\n    if (s < 2592000) return Math.floor(s / 604800) + 'w ago';\n    return new Date(t).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });\n  }\n  function fullDate(iso) {\n    var t = parseDate(iso);\n    if (!t) return '—';\n    return new Date(t).toLocaleString(undefined, {\n      year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'\n    });\n  }\n  function tintOf(name) { return TINTS[name] ? name : 'seafoam'; }\n  function validateRef(ref) {\n    return typeof ref === 'string' && /^[A-Za-z0-9_-]{44}\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(ref);\n  }\n  function el(tag, cls, text) {\n    var node = document.createElement(tag);\n    if (cls) node.className = cls;\n    if (text != null) node.textContent = text;\n    return node;\n  }\n  function toast(msg, kind, ms) {\n    var t = $('#toast');\n    t.textContent = msg;\n    t.className = 'toast show' + (kind ? ' ' + kind : '');\n    clearTimeout(toast._timer);\n    toast._timer = setTimeout(function () { t.className = 'toast'; }, ms || 4800);\n  }\n  function debounce(fn, ms) {\n    var timer;\n    return function () {\n      var args = arguments, self = this;\n      clearTimeout(timer);\n      timer = setTimeout(function () { fn.apply(self, args); }, ms);\n    };\n  }\n  function fetchJSON(path) {\n    var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;\n    var timer = controller ? setTimeout(function () { controller.abort(); }, 20000) : null;\n    return fetch(path, {\n      headers: { 'Accept': 'application/json' },\n      signal: controller ? controller.signal : undefined,\n      credentials: 'same-origin'\n    }).then(function (res) {\n      if (!res.ok) {\n        var err = new Error('HTTP ' + res.status);\n        err.status = res.status;\n        throw err;\n      }\n      return res.json();\n    }).finally(function () { if (timer) clearTimeout(timer); });\n  }\n  function unwrap(obj) {\n    if (!obj || typeof obj !== 'object') return null;\n    return obj.item && typeof obj.item === 'object' ? obj.item : obj;\n  }\n  function normalizeBottle(obj) {\n    try {\n      var it = unwrap(obj);\n      if (!it || typeof it !== 'object') return null;\n      var content = it.content && typeof it.content === 'object' ? it.content : {};\n      var message = typeof content.message === 'string' ? content.message.trim() : '';\n      if (!message) return null;\n      var ref = it.ref;\n      if (!validateRef(ref)) {\n        if (validateRef(it.pubkey + '.' + it.id)) ref = it.pubkey + '.' + it.id;\n        else return null;\n      }\n      var authorRel = it.relations && it.relations.author && it.relations.author[0];\n      var authorRef = authorRel && validateRef(authorRel.ref) ? authorRel.ref : null;\n      var authorHint = authorRel && typeof authorRel.name === 'string' ? authorRel.name : null;\n      return {\n        ref: ref,\n        message: message.slice(0, 600),\n        tint: tintOf(content.tint),\n        createdAt: it.created_at || null,\n        updatedAt: it.updated_at || null,\n        pubkey: it.pubkey || ref.slice(0, ref.indexOf('.')),\n        authorRef: authorRef,\n        authorHint: authorHint,\n        instruction: typeof it.instruction === 'string' ? it.instruction : '',\n        revision: typeof it.revision === 'number' ? it.revision : 0\n      };\n    } catch (e) { return null; }\n  }\n  function dedupe(list) {\n    var seen = Object.create(null), out = [];\n    for (var i = 0; i < list.length; i++) {\n      if (!seen[list[i].ref]) { seen[list[i].ref] = true; out.push(list[i]); }\n    }\n    return out;\n  }\n\n  /* ── Data ──────────────────────────────────────────────────── */\n  function loadTide(opts) {\n    opts = opts || {};\n    if (state.loading) return Promise.resolve();\n    state.loading = true;\n    setBusy(true);\n    if (!state.loadedOnce) renderSkeletons();\n    updateSyncLabel('reading…');\n\n    var all = [];\n    var cursor = null;\n    var pages = 0;\n\n    function page() {\n      var url = '/' + APP_REF + '/inbound?relation=in_application&type=BOTTLE&limit=100';\n      if (cursor) url += '&cursor=' + encodeURIComponent(cursor);\n      return fetchJSON(url).then(function (data) {\n        var items = Array.isArray(data && data.items) ? data.items : [];\n        for (var i = 0; i < items.length; i++) {\n          var b = normalizeBottle(items[i]);\n          if (b) all.push(b);\n        }\n        pages++;\n        cursor = data && data.has_more && data.cursor ? data.cursor : null;\n        if (cursor && pages < 3 && all.length < MAX_BOTTLES) return page();\n        return null;\n      });\n    }\n\n    return page().then(function () {\n      var bottles = dedupe(all).sort(function (a, b) { return parseDate(b.createdAt) - parseDate(a.createdAt); });\n      state.bottles = bottles.slice(0, MAX_BOTTLES);\n      state.loadedOnce = true;\n      state.cached = false;\n      state.cacheAt = Date.now();\n      state.error = null;\n      saveJSON(LS_CACHE, { at: state.cacheAt, bottles: state.bottles });\n      hideOffline();\n      updateStats();\n      ensureAuthors();\n      render();\n      updateSyncLabel('synced ' + new Date(state.cacheAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));\n      setLive(true);\n      if (opts.announce) toast('The tide brought ' + state.bottles.length + ' bottle' + (state.bottles.length === 1 ? '' : 's') + '.', 'ok');\n    }).catch(function (err) {\n      console.warn('[seaglass] tide fetch failed:', err);\n      var cached = loadJSON(LS_CACHE, null);\n      if (cached && Array.isArray(cached.bottles) && cached.bottles.length) {\n        state.bottles = cached.bottles;\n        state.loadedOnce = true;\n        state.cached = true;\n        state.cacheAt = cached.at || null;\n        updateStats();\n        ensureAuthors();\n        render();\n        updateSyncLabel('last tide saved ' + (cached.at ? new Date(cached.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''));\n        showOffline('The hub is out of reach. You are reading the last tide saved in this browser.');\n        setLive(false);\n      } else {\n        state.bottles = [];\n        state.loadedOnce = true;\n        state.error = err;\n        updateStats();\n        render();\n        setLive(false);\n        updateSyncLabel('no connection');\n      }\n    }).finally(function () {\n      state.loading = false;\n      setBusy(false);\n    });\n  }\n\n  function ensureAuthors() {\n    var refs = {}, list = [];\n    state.bottles.forEach(function (b) {\n      if (b.authorRef && !state.authors[b.authorRef] && !state.authorPending.has(b.authorRef)) {\n        state.authorPending.add(b.authorRef);\n        refs[b.authorRef] = true;\n        list.push(b.authorRef);\n      }\n    });\n    if (!list.length) return;\n    var index = 0;\n    function worker() {\n      if (index >= list.length) return Promise.resolve();\n      var ref = list[index++];\n      return fetchJSON('/' + ref).then(function (obj) {\n        var it = unwrap(obj);\n        var name = it && it.content && typeof it.content.name === 'string' ? it.content.name.trim() : '';\n        if (name) {\n          state.authors[ref] = { name: name, at: Date.now() };\n          saveJSON(LS_AUTHORS, state.authors);\n          updateAuthorNodes(ref);\n        }\n      }).catch(function () { /* leave fallback */ }).then(worker);\n    }\n    var workers = [];\n    for (var i = 0; i < Math.min(5, list.length); i++) workers.push(worker());\n    Promise.all(workers).then(function () {\n      list.forEach(function (r) { state.authorPending.delete(r); });\n    });\n  }\n\n  function authorFor(bottle) {\n    if (bottle.authorHint) return bottle.authorHint;\n    if (bottle.authorRef && state.authors[bottle.authorRef] && state.authors[bottle.authorRef].name) {\n      return state.authors[bottle.authorRef].name;\n    }\n    if (window.DV && window.DV.wallet && window.DV.wallet.pubkey && bottle.pubkey === window.DV.wallet.pubkey && window.DV.wallet.name) {\n      return window.DV.wallet.name;\n    }\n    return shortPk(bottle.pubkey);\n  }\n\n  function updateAuthorNodes(ref) {\n    var name = state.authors[ref] && state.authors[ref].name;\n    if (!name) return;\n    var safeRef = ref.replace(/\"/g, '');\n    $$('[data-author-ref=\"' + safeRef + '\"]').forEach(function (node) {\n      node.textContent = name;\n      node.setAttribute('title', name);\n    });\n    $$('[data-orb-ref=\"' + safeRef + '\"]').forEach(function (node) {\n      node.textContent = initial(name);\n      node.setAttribute('title', name);\n    });\n    if (state.readerRef) {\n      var b = byRef(state.readerRef);\n      if (b && b.authorRef === ref) $('#readerAuthor').textContent = name;\n    }\n  }\n\n  function byRef(ref) {\n    for (var i = 0; i < state.bottles.length; i++) if (state.bottles[i].ref === ref) return state.bottles[i];\n    return null;\n  }\n\n  /* ── Rendering ─────────────────────────────────────────────── */\n  function filteredBottles() {\n    var q = state.query.trim().toLowerCase();\n    var list = state.bottles.filter(function (b) {\n      if (state.filter !== 'all' && b.tint !== state.filter) return false;\n      if (!q) return true;\n      var hay = (b.message + ' ' + authorFor(b) + ' ' + shortPk(b.pubkey)).toLowerCase();\n      return hay.indexOf(q) !== -1;\n    });\n    if (state.sort === 'old') {\n      list.sort(function (a, b) { return parseDate(a.createdAt) - parseDate(b.createdAt); });\n    } else if (state.sort === 'random') {\n      if (!state.randomOrder) shuffleOrder();\n      var rank = state.randomOrder;\n      list.sort(function (a, b) { return (rank[a.ref] || 0) - (rank[b.ref] || 0); });\n    } else {\n      list.sort(function (a, b) { return parseDate(b.createdAt) - parseDate(a.createdAt); });\n    }\n    return list;\n  }\n  function shuffleOrder() {\n    state.randomOrder = {};\n    state.bottles.forEach(function (b) { state.randomOrder[b.ref] = Math.random(); });\n  }\n\n  function renderSkeletons() {\n    var shore = $('#shore');\n    shore.innerHTML = '';\n    for (var i = 0; i < 6; i++) {\n      var s = el('div', 'skeleton');\n      s.style.animationDelay = (i * 0.08) + 's';\n      shore.appendChild(s);\n    }\n    $('#emptyState').hidden = true;\n    shore.setAttribute('aria-busy', 'true');\n  }\n\n  function cardHTML(b, index) {\n    var tint = tintOf(b.tint);\n    var name = authorFor(b);\n    return '<article class=\"shard tint-' + tint + (b.ref === state.freshRef ? ' fresh' : '') + '\" style=\"--delay:' + (-(index % 9) * 1.3).toFixed(1) + 's;--enter:' + Math.min(index * 0.035, 0.42).toFixed(3) + 's\" data-ref=\"' + esc(b.ref) + '\">'\n      + '<button class=\"shard-open\" type=\"button\" aria-label=\"Read this bottle from ' + esc(name) + '\">'\n      + '<span class=\"shard-meta\"><span class=\"gem\"></span><span class=\"shard-kind\">bottle</span><span class=\"shard-time\">' + esc(ago(b.createdAt)) + '</span></span>'\n      + '<span class=\"shard-msg\">' + esc(b.message) + '</span>'\n      + '<span class=\"shard-from\"><span class=\"author-orb\" data-orb-ref=\"' + esc(b.authorRef || '') + '\">' + esc(initial(name)) + '</span><span class=\"author-name\" data-author-ref=\"' + esc(b.authorRef || '') + '\">' + esc(name) + '</span><span class=\"author-pk\">' + esc(shortPk(b.pubkey)) + '</span>'\n      + '<span class=\"read-hint\"><svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M3 8h9M8.5 4.5 12 8l-3.5 3.5\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span></span>'\n      + '</button>'\n      + '<a class=\"node-link\" href=\"' + esc(nodeViewerURL(b.ref)) + '\" target=\"_blank\" rel=\"noopener\" title=\"Inspect this object in the Node Viewer\" aria-label=\"Inspect this object in the Node Viewer\"><svg viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><circle cx=\"5.5\" cy=\"5.5\" r=\"3.2\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"m8 8 4.5 4.5\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg></a>'\n      + '</article>';\n  }\n\n  function render() {\n    var shore = $('#shore');\n    var empty = $('#emptyState');\n    var list = filteredBottles();\n    shore.innerHTML = '';\n    shore.setAttribute('aria-busy', 'false');\n\n    if (!state.bottles.length) {\n      empty.hidden = false;\n      if (state.error) {\n        $('#emptyTitle').textContent = 'The tide is out.';\n        $('#emptyText').textContent = 'We could not reach the hub, and there is no saved tide in this browser yet. The shore will fill in when the connection returns.';\n        $('#emptyCastBtn').textContent = 'Try again';\n        $('#emptyCastBtn').onclick = function () { loadTide(); };\n      } else {\n        $('#emptyTitle').textContent = 'The tide is quiet.';\n        $('#emptyText').textContent = 'No bottles have washed ashore yet. If you have something to say to a stranger on the dataverse, be the first.';\n        $('#emptyCastBtn').textContent = 'Cast the first bottle';\n        $('#emptyCastBtn').onclick = function () { openComposer(); };\n      }\n      $('#shoreLabel').textContent = 'what washed ashore';\n      updateChipCounts();\n      return;\n    }\n\n    empty.hidden = true;\n    if (!list.length) {\n      var none = el('div', 'no-results');\n      var b1 = el('b', null, 'Nothing on this stretch of sand.');\n      var p1 = el('p', null, 'No bottles match that search or color. Try a broader glance.');\n      p1.style.margin = '0'; p1.style.fontSize = '13.5px';\n      none.appendChild(b1); none.appendChild(p1);\n      shore.appendChild(none);\n      $('#shoreLabel').textContent = 'no bottles found';\n      updateChipCounts();\n      return;\n    }\n\n    var html = '';\n    for (var i = 0; i < list.length; i++) html += cardHTML(list[i], i);\n    shore.innerHTML = html;\n\n    $('#shoreLabel').textContent = state.query || state.filter !== 'all'\n      ? list.length + ' bottle' + (list.length === 1 ? '' : 's') + ' found'\n      : 'what washed ashore';\n\n    updateChipCounts();\n  }\n\n  function updateChipCounts() {\n    var counts = { all: state.bottles.length };\n    Object.keys(TINTS).forEach(function (k) { counts[k] = 0; });\n    state.bottles.forEach(function (b) { if (counts[b.tint] != null) counts[b.tint]++; });\n    $('#countAll').textContent = counts.all;\n    $$('[data-count]').forEach(function (node) { node.textContent = counts[node.getAttribute('data-count')] || 0; });\n  }\n\n  function updateStats() {\n    var voices = {};\n    state.bottles.forEach(function (b) { voices[b.authorRef || b.pubkey] = true; });\n    var weekAgo = Date.now() - 7 * 86400000;\n    var fresh = state.bottles.filter(function (b) { return parseDate(b.createdAt) > weekAgo; }).length;\n    $('#statBottles').textContent = state.bottles.length;\n    $('#statVoices').textContent = Object.keys(voices).length;\n    $('#statFresh').textContent = fresh;\n  }\n\n  function setBusy(busy) {\n    var btn = $('#refreshBtn');\n    btn.classList.toggle('busy', busy);\n    btn.disabled = busy;\n  }\n  function updateSyncLabel(text) { $('#lastSync').textContent = text || ''; }\n  function setLive(on) {\n    $('#liveDot').classList.toggle('offline', !on);\n    $('#liveLabel').textContent = on ? 'reading the tide at ' + location.host : 'offline · reading a saved tide';\n  }\n  function showOffline(msg) {\n    var n = $('#offlineNotice');\n    n.innerHTML = '<span>⚠</span><span>' + esc(msg) + '</span><button class=\"btn\" id=\"offlineRetry\" type=\"button\">Try the hub again</button>';\n    n.hidden = false;\n    $('#offlineRetry').onclick = function () { loadTide(); };\n  }\n  function hideOffline() { $('#offlineNotice').hidden = true; }\n\n  /* ── Reader ────────────────────────────────────────────────── */\n  function nodeViewerURL(ref) {\n    return '/' + NODE_VIEWER_REF + '?ref=' + encodeURIComponent(ref);\n  }\n  function openReader(ref) {\n    var b = byRef(ref);\n    if (!b) return;\n    state.readerRef = ref;\n    var tint = tintOf(b.tint);\n    var wrap = $('#readerTint');\n    wrap.className = 'reader-tint ' + tint;\n    $('#readerHeading').textContent = 'bottle · ' + TINTS[tint].label;\n    $('#readerTime').textContent = ago(b.createdAt);\n    $('#readerMessage').textContent = b.message;\n    var name = authorFor(b);\n    $('#readerOrb').textContent = initial(name);\n    $('#readerAuthor').textContent = name;\n    $('#readerPk').textContent = b.pubkey;\n    $('#readerStamp').innerHTML = esc(fullDate(b.createdAt)) + (b.revision > 0 && b.updatedAt ? '<br>revised ' + esc(ago(b.updatedAt)) : '');\n    $('#readerNodeLink').href = nodeViewerURL(ref);\n    $('#readerRawLink').href = '/' + ref;\n    $('#readerRef').textContent = ref;\n    $('#readerRef').title = ref;\n    updateReaderNav();\n    openModal('readerModal');\n    history.replaceState(null, '', '#' + ref);\n  }\n\n  function updateReaderNav() {\n    var list = filteredBottles();\n    var idx = -1;\n    for (var i = 0; i < list.length; i++) if (list[i].ref === state.readerRef) { idx = i; break; }\n    var show = list.length > 1 && idx !== -1;\n    $('#readerPrev').style.visibility = show ? 'visible' : 'hidden';\n    $('#readerNext').style.visibility = show ? 'visible' : 'hidden';\n    $('#readerPrev').disabled = !(show && idx > 0);\n    $('#readerNext').disabled = !(show && idx < list.length - 1);\n    $('#readerPrev').style.opacity = show && idx > 0 ? '1' : '.35';\n    $('#readerNext').style.opacity = show && idx < list.length - 1 ? '1' : '.35';\n  }\n  function stepReader(delta) {\n    var list = filteredBottles();\n    var idx = -1;\n    for (var i = 0; i < list.length; i++) if (list[i].ref === state.readerRef) { idx = i; break; }\n    if (idx === -1) return;\n    var next = idx + delta;\n    if (next < 0 || next >= list.length) return;\n    openReader(list[next].ref);\n  }\n\n  /* ── Modals ────────────────────────────────────────────────── */\n  var lastFocus = null;\n  function openModal(id) {\n    var modal = document.getElementById(id);\n    if (!modal) return;\n    $$('.modal').forEach(function (m) { if (m !== modal && !m.hidden) closeModal(m, true); });\n    lastFocus = document.activeElement;\n    modal.hidden = false;\n    document.body.classList.add('locked');\n    requestAnimationFrame(function () {\n      modal.classList.add('open');\n      var focusTarget = modal.querySelector('.sheet-close');\n      if (focusTarget) focusTarget.focus({ preventScroll: true });\n    });\n  }\n  function closeModal(modalOrId, silence) {\n    var modal = typeof modalOrId === 'string' ? document.getElementById(modalOrId) : modalOrId;\n    if (!modal || modal.hidden) return;\n    modal.classList.remove('open');\n    if (modal.id === 'readerModal') {\n      state.readerRef = null;\n      if (location.hash) history.replaceState(null, '', location.pathname + location.search);\n    }\n    setTimeout(function () { modal.hidden = true; }, 200);\n    var anyOpen = $$('.modal').some(function (m) { return m !== modal && !m.hidden; });\n    if (!anyOpen) document.body.classList.remove('locked');\n    if (!silence && lastFocus && typeof lastFocus.focus === 'function') {\n      try { lastFocus.focus({ preventScroll: true }); } catch (e) {}\n    }\n  }\n  function authPanelOpen() {\n    var panel = document.getElementById('dv-auth-panel');\n    var body = document.getElementById('dv-body');\n    return !!(panel && body && !body.classList.contains('dv-hidden'));\n  }\n  document.addEventListener('keydown', function (e) {\n    if (e.key === 'Escape') {\n      if (authPanelOpen()) {\n        var bar = document.getElementById('dv-bar');\n        if (bar) bar.click();\n        return;\n      }\n      var open = $$('.modal.open');\n      if (open.length) { closeModal(open[open.length - 1]); return; }\n    }\n    if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !$('#readerModal').hidden && !authPanelOpen()) {\n      if (['INPUT', 'TEXTAREA', 'SELECT'].indexOf((document.activeElement.tagName || '')) !== -1) return;\n      stepReader(e.key === 'ArrowLeft' ? -1 : 1);\n    }\n    if (e.key === '/' && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName || '')) {\n      e.preventDefault();\n      $('#searchInput').focus();\n    }\n  });\n\n  /* ── Write layer ───────────────────────────────────────────── */\n  function loadScript(src) {\n    return new Promise(function (resolve, reject) {\n      var s = document.createElement('script');\n      s.src = src;\n      s.async = true;\n      s.onload = resolve;\n      s.onerror = function () { reject(new Error('Could not load ' + src)); };\n      document.head.appendChild(s);\n    });\n  }\n\n  function ensureWriteLibs() {\n    if (state.writeReady) return Promise.resolve(true);\n    if (state.writeLoading) return state.writeLoading;\n    state.writeLoading = Promise.resolve().then(function () {\n      if (!window.DV) return loadScript('/' + CORE_BLOB_REF);\n      return null;\n    }).then(function () {\n      if (!window.DV || typeof window.DV.init !== 'function') throw new Error('The dataverse core library did not load.');\n      window.DV.init({ api: location.origin });\n      return loadScript('/' + WRITE_BLOB_REF);\n    }).then(function () {\n      if (!window.DV || typeof window.DV.initWrite !== 'function') throw new Error('The dataverse write library did not load.');\n      window.DV.initWrite({\n        theme: 'dark',\n        panelWidth: 292,\n        onAuthChange: handleAuthChange,\n        onSignPending: handleSignPending\n      });\n      state.writeReady = true;\n      updateIdentityChip();\n      updateComposerIdentity();\n      return true;\n    }).catch(function (err) {\n      console.warn('[seaglass] write layer unavailable:', err);\n      state.writeLoading = null;\n      throw err;\n    });\n    return state.writeLoading;\n  }\n\n  function handleAuthChange() {\n    updateIdentityChip();\n    updateComposerIdentity();\n    if (!window.DV || !window.DV.wallet) return;\n    var w = window.DV.wallet;\n    if (w.connected && !w.locked) {\n      var bar = document.getElementById('dv-bar');\n      var body = document.getElementById('dv-body');\n      if (bar && body && !body.classList.contains('dv-hidden')) {\n        setTimeout(function () { if (!body.classList.contains('dv-hidden')) bar.click(); }, 500);\n      }\n    }\n  }\n  function handleSignPending(on) {\n    var btn = $('#castSubmit');\n    if (!btn) return;\n    if (on) {\n      btn.disabled = true;\n      btn.dataset.label = btn.textContent;\n      btn.textContent = 'Waiting for signature…';\n    } else {\n      btn.disabled = false;\n      if (btn.dataset.label) btn.textContent = btn.dataset.label;\n    }\n  }\n\n  function openAuthPanel() {\n    ensureWriteLibs().then(function () {\n      if (window.DV && typeof window.DV.expandWidget === 'function') window.DV.expandWidget();\n    }).catch(function () {\n      toast('The identity panel could not be loaded. Check your connection and try again.', 'error');\n    });\n  }\n\n  function updateIdentityChip() {\n    var nameEl = $('#identityName');\n    var stateEl = $('#identityState');\n    var orbEl = $('#identityOrb');\n    var w = window.DV && window.DV.wallet;\n    if (!w || !w.connected) {\n      nameEl.textContent = 'Sign in';\n      stateEl.textContent = '';\n      orbEl.textContent = '·';\n      $('#identityBtn').title = 'Connect a dataverse identity';\n      return;\n    }\n    var name = w.name || shortPk(w.pubkey);\n    nameEl.textContent = name;\n    stateEl.textContent = w.locked ? 'locked' : shortPk(w.pubkey);\n    orbEl.textContent = initial(name);\n    $('#identityBtn').title = w.locked ? 'Identity locked — click to unlock' : 'Identity connected — click to manage';\n  }\n\n  function updateComposerIdentity() {\n    var slotName = $('#slotName');\n    var slotState = $('#slotState');\n    var slotOrb = $('#slotOrb');\n    var connect = $('#connectBtn');\n    var w = window.DV && window.DV.wallet;\n    if (w && w.connected && !w.locked) {\n      var name = w.name || shortPk(w.pubkey);\n      slotName.textContent = name;\n      slotState.textContent = 'Signing as ' + shortPk(w.pubkey) + ' — your identity stays in this browser.';\n      slotState.className = 'slot-state ok';\n      slotOrb.textContent = initial(name);\n      connect.textContent = 'Manage';\n    } else if (w && w.connected && w.locked) {\n      slotName.textContent = w.name || shortPk(w.pubkey);\n      slotState.textContent = 'Your identity is locked. Unlock it to sign this bottle.';\n      slotState.className = 'slot-state';\n      slotOrb.textContent = initial(w.name || '?');\n      connect.textContent = 'Unlock';\n    } else {\n      slotName.textContent = 'No identity connected';\n      slotState.textContent = 'You need a dataverse identity to sign your bottle.';\n      slotState.className = 'slot-state';\n      slotOrb.textContent = '?';\n      connect.textContent = 'Connect';\n    }\n  }\n\n  /* ── Composer ──────────────────────────────────────────────── */\n  function openComposer() {\n    state.lastCastRef = null;\n    $('#composeForm').hidden = false;\n    $('#composeSuccess').hidden = true;\n    $('#composerError').style.display = 'none';\n    $('#messageInput').value = '';\n    $('#charCount').textContent = '0 / 600';\n    $('#charCount').className = 'counter';\n    pickTint('seafoam');\n    updateComposerIdentity();\n    openModal('composerModal');\n    setTimeout(function () { $('#messageInput').focus({ preventScroll: true }); }, 260);\n  }\n\n  function pickTint(tint) {\n    state.pickedTint = TINTS[tint] ? tint : 'seafoam';\n    $$('.tint-swatch').forEach(function (sw) {\n      sw.setAttribute('aria-pressed', sw.getAttribute('data-tint') === state.pickedTint ? 'true' : 'false');\n    });\n    $('#tintName').textContent = TINTS[state.pickedTint].label;\n  }\n\n  function updateCharCount() {\n    var v = $('#messageInput').value;\n    var c = $('#charCount');\n    c.textContent = v.length + ' / 600';\n    c.className = 'counter' + (v.length >= 600 ? ' max' : v.length >= 540 ? ' warn' : '');\n  }\n\n  function castBottle() {\n    if (state.sending) return;\n    var message = $('#messageInput').value.trim();\n    if (!message) {\n      showComposerError('Write a few words before sealing the bottle.');\n      $('#messageInput').focus();\n      return;\n    }\n    if (!state.writeReady) {\n      showComposerError('Opening the identity panel…');\n      ensureWriteLibs().then(function () {\n        hideComposerError();\n        if (!window.DV.isLoggedIn()) {\n          window.DV.expandWidget();\n          toast('Connect an identity to sign your bottle.');\n        } else {\n          castBottle();\n        }\n      }).catch(function () {\n        showComposerError('The signing library could not be loaded. Check your connection and try again.');\n      });\n      return;\n    }\n    if (!window.DV || !window.DV.isLoggedIn()) {\n      window.DV.expandWidget();\n      toast('Connect an identity to sign your bottle.');\n      return;\n    }\n\n    state.sending = true;\n    var btn = $('#castSubmit');\n    btn.disabled = true;\n    var oldLabel = btn.textContent;\n    btn.textContent = 'Sealing…';\n    hideComposerError();\n\n    var item;\n    try {\n      item = window.DV.buildItem('BOTTLE', {\n        in_application: [{ ref: APP_REF }],\n        type_def: [{ ref: BOTTLE_TYPE_REF }]\n      }, {\n        message: message,\n        tint: state.pickedTint\n      }, {\n        in: ['server-public'],\n        name: 'Message in a bottle',\n        instruction: BOTTLE_INSTRUCTION\n      });\n    } catch (err) {\n      state.sending = false;\n      btn.disabled = false;\n      btn.textContent = oldLabel;\n      showComposerError('Could not build the bottle: ' + err.message);\n      return;\n    }\n\n    window.DV.signAndPush(item).then(function (result) {\n      if (!result || !result.ok) {\n        var code = result && result.status ? ' (HTTP ' + result.status + ')' : '';\n        throw new Error('The hub did not accept the bottle' + code + '.');\n      }\n      var b = normalizeBottle(result.signedObject) || normalizeBottle(item);\n      if (b) {\n        state.freshRef = b.ref;\n        state.bottles = [b].concat(state.bottles.filter(function (x) { return x.ref !== b.ref; }));\n        state.cacheAt = Date.now();\n        saveJSON(LS_CACHE, { at: state.cacheAt, bottles: state.bottles });\n        ensureAuthors();\n        updateStats();\n        render();\n      }\n      state.lastCastRef = b ? b.ref : null;\n      $('#successPreview').textContent = message;\n      $('#composeForm').hidden = true;\n      $('#composeSuccess').hidden = false;\n      var readBtn = $('#readMineBtn');\n      readBtn.style.display = b ? '' : 'none';\n      toast('Your bottle is adrift.', 'ok');\n      updateSyncLabel('synced ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));\n      // Reconcile with the hub's index shortly after.\n      setTimeout(function () { loadTide(); }, 1200);\n    }).catch(function (err) {\n      console.warn('[seaglass] cast failed:', err);\n      showComposerError(err && err.message ? err.message : 'The bottle could not be cast. Try again.');\n    }).finally(function () {\n      state.sending = false;\n      btn.disabled = false;\n      btn.textContent = oldLabel;\n    });\n  }\n\n  function showComposerError(msg) {\n    var e = $('#composerError');\n    e.textContent = msg;\n    e.style.display = 'block';\n  }\n  function hideComposerError() {\n    $('#composerError').style.display = 'none';\n  }\n\n  /* ── Wiring ────────────────────────────────────────────────── */\n  function bind() {\n    ['castBtnTop', 'castBtnHero', 'aboutCastBtn'].forEach(function (id) {\n      var node = document.getElementById(id);\n      if (node) node.addEventListener('click', openComposer);\n    });\n    $('#browseBtn').addEventListener('click', function () {\n      $('#shore').scrollIntoView({ behavior: 'smooth', block: 'start' });\n    });\n    $('#aboutBtn').addEventListener('click', function () { openAbout(); });\n    $('#aboutBtn2').addEventListener('click', function () { openAbout(); });\n    $('#identityBtn').addEventListener('click', function () {\n      if (state.writeReady && window.DV) window.DV.expandWidget();\n      else openAuthPanel();\n    });\n    $('#refreshBtn').addEventListener('click', function () { loadTide({ announce: true }); });\n\n    $('#searchInput').addEventListener('input', debounce(function (e) {\n      state.query = e.target.value || '';\n      if (state.sort === 'random') shuffleOrder();\n      render();\n    }, 90));\n\n    $('#tintChips').addEventListener('click', function (e) {\n      var chip = e.target.closest('.chip');\n      if (!chip) return;\n      state.filter = chip.getAttribute('data-tint') || 'all';\n      $$('.chip', $('#tintChips')).forEach(function (c) {\n        var active = c === chip;\n        c.classList.toggle('active', active);\n        c.setAttribute('aria-pressed', active ? 'true' : 'false');\n      });\n      render();\n    });\n\n    $('#sortSelect').addEventListener('change', function (e) {\n      state.sort = e.target.value;\n      if (state.sort === 'random') shuffleOrder();\n      render();\n    });\n\n    $('#shore').addEventListener('click', function (e) {\n      var nodeLink = e.target.closest('.node-link');\n      if (nodeLink) return; // Let the anchor do its job.\n      var card = e.target.closest('.shard');\n      if (!card) return;\n      var ref = card.getAttribute('data-ref');\n      if (ref) openReader(ref);\n    });\n    $('#shore').addEventListener('keydown', function (e) {\n      if (e.key !== 'Enter' && e.key !== ' ') return;\n      var card = e.target.closest('.shard');\n      if (!card) return;\n      var ref = card.getAttribute('data-ref');\n      if (ref) { e.preventDefault(); openReader(ref); }\n    });\n\n    $$('[data-close]').forEach(function (btn) {\n      btn.addEventListener('click', function () { closeModal(btn.getAttribute('data-close')); });\n    });\n    $$('.modal').forEach(function (modal) {\n      modal.addEventListener('click', function (e) {\n        if (e.target === modal) closeModal(modal);\n      });\n    });\n    $('#readerPrev').addEventListener('click', function () { stepReader(-1); });\n    $('#readerNext').addEventListener('click', function () { stepReader(1); });\n    $('#readerCopyBtn').addEventListener('click', function () {\n      if (!state.readerRef) return;\n      var url = location.origin + '/' + state.readerRef;\n      copyText(url).then(function () { toast('Share link copied.', 'ok'); })\n        .catch(function () { toast('Could not copy — use the address bar.', 'error'); });\n    });\n\n    $('#tints').addEventListener('click', function (e) {\n      var sw = e.target.closest('.tint-swatch');\n      if (sw) pickTint(sw.getAttribute('data-tint'));\n    });\n    $('#messageInput').addEventListener('input', updateCharCount);\n    $('#messageInput').addEventListener('keydown', function (e) {\n      if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); castBottle(); }\n    });\n    $('#castSubmit').addEventListener('click', castBottle);\n    $('#connectBtn').addEventListener('click', function () {\n      ensureWriteLibs().then(function () {\n        if (window.DV) window.DV.expandWidget();\n      }).catch(function () { toast('The identity panel could not be loaded.', 'error'); });\n    });\n    $('#readMineBtn').addEventListener('click', function () {\n      closeModal('composerModal');\n      if (state.lastCastRef) setTimeout(function () { openReader(state.lastCastRef); }, 220);\n    });\n\n    document.addEventListener('visibilitychange', function () {\n      if (document.visibilityState === 'visible' && !state.loading && state.loadedOnce) {\n        var last = state.cacheAt || 0;\n        if (Date.now() - last > 120000) loadTide();\n      }\n    });\n    setInterval(function () {\n      if (document.visibilityState === 'visible' && !state.loading && state.loadedOnce) loadTide();\n    }, 120000);\n  }\n\n  function copyText(text) {\n    if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);\n    return new Promise(function (resolve, reject) {\n      try {\n        var ta = document.createElement('textarea');\n        ta.value = text;\n        ta.style.position = 'fixed';\n        ta.style.opacity = '0';\n        document.body.appendChild(ta);\n        ta.select();\n        var ok = document.execCommand('copy');\n        document.body.removeChild(ta);\n        ok ? resolve() : reject(new Error('copy failed'));\n      } catch (e) { reject(e); }\n    });\n  }\n\n  function openAbout() {\n    var appShort = APP_REF;\n    $('#aboutRefs').textContent = 'application ' + APP_REF + '\\nbottle type ' + BOTTLE_TYPE_REF + '\\npage ' + PAGE_RAW_URL.split('/').pop() + '\\npublisher ' + PUBKEY;\n    openModal('aboutModal');\n  }\n\n  function init() {\n    $('#appRefText').textContent = APP_REF;\n    $('#aboutRefs').textContent = '';\n    $$('.footer-ref').forEach(function (node) {\n      if (!node.textContent.trim().replace('application', '').trim()) node.textContent = 'application ' + APP_REF;\n    });\n    bind();\n    updateIdentityChip();\n    if (location.hash && validateRef(location.hash.slice(1))) {\n      // Wait for the tide, then open the linked bottle if it is here.\n      var target = location.hash.slice(1);\n      loadTide().then(function () {\n        if (byRef(target)) openReader(target);\n        else toast('That bottle is not on this shore.', 'error');\n      });\n    } else {\n      loadTide();\n    }\n  }\n\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);\n  else init();\n})();\n</script>\n</body>\n</html>","title":"Seaglass — messages in bottles on the dataverse"},"created_at":"2026-11-10T03:21:44Z","id":"5c12c354-d9b6-468e-85f9-361fdbaedb22","in":["server-public"],"instruction":"The web interface for Seaglass, a public message-in-a-bottle app on the dataverse. Serve `content.html` with Content-Type text/html. It reads BOTTLE objects from the hub's inbound-relations API at `/<application>/inbound?relation=in_application&type=BOTTLE` and, when the visitor connects an identity, signs new BOTTLE objects with dataverse-core.js + dataverse-write.js and pushes them in the server-public realm. No cookies or third-party scripts.","name":"Seaglass — the shore","pubkey":"A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_","ref":"A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.5c12c354-d9b6-468e-85f9-361fdbaedb22","relations":{"author":[{"ref":"A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.00000000-0000-0000-0000-000000000001"}],"in_application":[{"ref":"A8u_zYzNe_y5xzraqYdCTB2YuLA6HUMMqp2xjsuVyDK_.34da21ed-2929-40a4-b0dc-52c4e1983fb3","title":"Seaglass"}],"root":[{"ref":"AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000","url":"https://dataverse001.net/AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.00000000-0000-0000-0000-000000000000"}],"type_def":[{"ref":"AxyU5_5vWmP2tO_klN4UpbZzRsuJEvJTrdwdg_gODxZJ.2d851bf2-65e9-4f62-b646-2014613c964d"}]},"revision":3,"type":"PAGE","updated_at":"2026-11-10T03:31:18Z"},"signature":"MEUCIAp74zDha/quX0XdHACQE2udZ3sxn3cqde6FrU9AXFsGAiEAu/OOv1qYyHNLSBdv9XRJ6Z0PQ/V6H4eZzgF1YAZA+rA="}