diff --git a/site/bun.lock b/site/bun.lock index 20038a1..2c78b7c 100644 --- a/site/bun.lock +++ b/site/bun.lock @@ -9,7 +9,7 @@ "@hraness/site-footer": "github:hraness/site-footer#v0.19.2", "@hraness/ui": "github:hraness/ui#v0.5.18", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", - "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.22.4/hraness-wordcell-0.22.4.tgz", + "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.23.0/hraness-wordcell-0.23.0.tgz", "next": "16.3.3", "react": "19.2.6", "react-dom": "19.2.6", @@ -101,7 +101,7 @@ "@hraness/web-discovery": ["@hraness/web-discovery@github:hraness/web-discovery#c50a2b0", { "dependencies": { "@hraness/design-kit": "github:hraness/design-kit#v0.5.0" }, "peerDependencies": { "next": ">=16.2.0 <17.0.0", "react": ">=19.0.0 <20.0.0" } }, "hraness-web-discovery-c50a2b0", "sha512-r/ZxtWzBM3nGDX3RbW6VPkdqaOVJG7qo3Cs9ohi/0H5l3jU8DReoJHwL7qrAJH6OHLdFcIdNENzyi4rkfLVBBw=="], - "@hraness/wordcell": ["@hraness/wordcell@https://github.com/hraness/wordcell/releases/download/v0.22.4/hraness-wordcell-0.22.4.tgz", { "dependencies": { "@hraness/oh": "https://github.com/hraness/oh/releases/download/v0.12.0/hraness-oh-0.12.0.tgz", "@hraness/support-foundation": "git+https://github.com/hraness/support-foundation.git#b32c1c81bb2444f50509ed54388758ecfab1f1c0", "@steipete/sweet-cookie": "0.4.3", "@tobilu/qmd": "git+https://github.com/hraness/qmd.git#aa993dceb3ef8cfb71d470554ca437570f5a2b3c", "agent-browser": "0.32.3", "defuddle": "0.19.1", "effect": "3.22.1", "yaml": "^2.9.0", "zod": "^4.4.3" }, "bin": { "wordcell": "./dist/cli.js", "wordcell-evaluation-builder": "./dist/evaluation-builder.js" } }, "sha512-PpL/yry+otN0sARQriXgp8zvcO5y5g99XD5Z87If/UbPcmJi5+xm9Xh9zJsFHmou70FdMYA+dbBxxTN+PnVcNQ=="], + "@hraness/wordcell": ["@hraness/wordcell@https://github.com/hraness/wordcell/releases/download/v0.23.0/hraness-wordcell-0.23.0.tgz", { "dependencies": { "@hraness/oh": "https://github.com/hraness/oh/releases/download/v0.12.0/hraness-oh-0.12.0.tgz", "@hraness/support-foundation": "git+https://github.com/hraness/support-foundation.git#b32c1c81bb2444f50509ed54388758ecfab1f1c0", "@steipete/sweet-cookie": "0.4.3", "@tobilu/qmd": "git+https://github.com/hraness/qmd.git#aa993dceb3ef8cfb71d470554ca437570f5a2b3c", "agent-browser": "0.32.3", "defuddle": "0.19.1", "effect": "3.22.1", "yaml": "^2.9.0", "zod": "^4.4.3" }, "bin": { "wordcell": "./dist/cli.js", "wordcell-evaluation-builder": "./dist/evaluation-builder.js" } }, "sha512-pNBIJah5Ph0T600489gPwI6qL7wQui6N+PhjFIu9QKDlEZNNkTsM9pU0VTt54hVeFGXfHhiomfuNYkiKzul+6w=="], "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@4.2.3", "", {}, "sha512-fpiU0qFZkv4ABi23xJ2m4qNr0BBfuIaYjPboN8WDA6aj9D+OzXa7eykxKOpACZRuGbDz1U9S5DsNx1d5KNnsgA=="], @@ -1443,6 +1443,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@hraness/oh/@tobilu/qmd": ["@tobilu/qmd@github:hraness/qmd#aa993dc", { "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "better-sqlite3": "12.10.0", "fast-glob": "3.3.3", "node-llama-cpp": "3.18.1", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", "tree-sitter-go": "0.25.0", "tree-sitter-python": "0.25.0", "tree-sitter-rust": "0.24.0", "tree-sitter-typescript": "0.23.2", "web-tree-sitter": "0.26.8", "yaml": "2.9.0", "zod": "4.2.1" }, "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "qmd": "bin/qmd" } }, "hraness-qmd-aa993dc", "sha512-8vwNE/U5SWSscMokgqRHsdDnTT6V5wy7rsrvn3mw5vi5qdXesiIb1fb5qCsjkWSwiZhTbx0d1r+zGfzIO/T2jw=="], + "@hraness/web-discovery/@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#070cdd2", { "dependencies": { "@hugeicons/core-free-icons": "^4.2.2", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.4 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-070cdd2", "sha512-kO4akqptuEsvbtpbdH2JaNZqOr/o9YH+Kj53sgN1EWfYq790lrH2kWySfhElbmS739ALAmivmYGyvqV4AtEZOg=="], "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -1575,6 +1577,12 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@hraness/oh/@tobilu/qmd/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "@hraness/oh/@tobilu/qmd/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "@hraness/oh/@tobilu/qmd/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "@hraness/web-discovery/@hraness/design-kit/@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@4.3.2", "", {}, "sha512-31uDdIq71YcVxeltjcS/tsmYFt4VjmgyNTZfG1bY6mbeFmsYL6Nwz9PdDZetGtGPZOAEVg0bHoQFOoPWDC51bg=="], "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -1617,6 +1625,8 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@hraness/oh/@tobilu/qmd/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "htmlparser2/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], diff --git a/site/lib/hosted/reader.generated.ts b/site/lib/hosted/reader.generated.ts index 9bd1cdc..92b9206 100644 --- a/site/lib/hosted/reader.generated.ts +++ b/site/lib/hosted/reader.generated.ts @@ -1,5 +1,5 @@ // Generated from @hraness/wordcell dist/publish-reader by scripts/sync-reader.ts. Do not edit. -export const HOSTED_PACKAGE_VERSION = "0.22.4"; +export const HOSTED_PACKAGE_VERSION = "0.23.0"; export const HOSTED_READER_FILES: Readonly> = { "reader.js": "// src/publish-model.ts\nvar WORDCELL_SITE_FORMAT_V1 = \"hraness.wordcell.site.v1\";\nvar WORDCELL_SITE_CATALOG_FORMAT_V1 = \"hraness.wordcell.site-catalog.v1\";\nvar WORDCELL_SITE_DOCS_FORMAT_V1 = \"hraness.wordcell.site-docs.v1\";\nvar WORDCELL_SITE_TERMS_FORMAT_V1 = \"hraness.wordcell.site-terms.v1\";\nvar WORDCELL_SITE_POSTINGS_FORMAT_V1 = \"hraness.wordcell.site-postings.v1\";\nvar WORDCELL_SITE_NOTE_FORMAT_V1 = \"hraness.wordcell.site-note.v1\";\nvar WORDCELL_SITE_GRAPH_FORMAT_V1 = \"hraness.wordcell.site-graph.v1\";\nvar WORDCELL_SITE_LIMITS_V1 = Object.freeze({\n notes: 1e4,\n assets: 1e4,\n assetBytes: 16 * 1024 * 1024,\n totalAssetBytes: 512 * 1024 * 1024,\n indexTerms: 250000,\n contentShards: 256,\n shardPairs: 250000,\n docPreviewBytes: 240,\n fieldTextBytes: 4 * 1024,\n inlineTextBytes: 16 * 1024,\n inlineTotalBytes: 2 * 1024 * 1024,\n noteTextBytes: 32 * 1024,\n titleBytes: 512,\n descriptionBytes: 4 * 1024,\n searchHydration: 50,\n searchResults: 100\n});\nvar textEncoder = new TextEncoder;\nfunction isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction boundedText(value, maximumBytes, field) {\n if (typeof value !== \"string\" || textEncoder.encode(value).byteLength > maximumBytes) {\n throw new TypeError(`${field} must be a string of at most ${maximumBytes} bytes`);\n }\n return value;\n}\nfunction boundedCount(value, maximum, field) {\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 0 || value > maximum) {\n throw new TypeError(`${field} must be a safe integer from 0 through ${maximum}`);\n }\n return value;\n}\nfunction exactKeys(value, required, optional, field) {\n const allowed = new Set([...required, ...optional]);\n for (const key of Object.keys(value)) {\n if (!allowed.has(key))\n throw new TypeError(`${field} has unexpected key ${JSON.stringify(key)}`);\n }\n for (const key of required) {\n if (!(key in value))\n throw new TypeError(`${field} is missing ${JSON.stringify(key)}`);\n }\n}\nfunction stringArray(value, maximum, field) {\n if (!Array.isArray(value) || value.length > maximum) {\n throw new TypeError(`${field} must be an array of at most ${maximum} strings`);\n }\n for (const item of value) {\n if (typeof item !== \"string\")\n throw new TypeError(`${field} entries must be strings`);\n }\n return value;\n}\nfunction contentSearch(value) {\n if (value === \"inline\" || value === \"shards\" || value === \"none\")\n return value;\n throw new TypeError(\"search.content must be inline, shards, or none\");\n}\nfunction parseSiteManifestV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"manifest must be an object\");\n exactKeys(value, [\"format\", \"site\", \"generated\", \"source\", \"paths\", \"search\", \"counts\", \"truncated\"], [], \"manifest\");\n if (value.format !== WORDCELL_SITE_FORMAT_V1) {\n throw new TypeError(`manifest format must be ${WORDCELL_SITE_FORMAT_V1}`);\n }\n const { site, generated, source, paths, search, counts, truncated } = value;\n if (!isRecord(site))\n throw new TypeError(\"manifest.site must be an object\");\n exactKeys(site, [\"title\", \"basePath\"], [\"description\"], \"manifest.site\");\n if (!isRecord(generated))\n throw new TypeError(\"manifest.generated must be an object\");\n exactKeys(generated, [\"by\", \"version\"], [\"at\"], \"manifest.generated\");\n if (generated.by !== \"@hraness/wordcell\") {\n throw new TypeError(\"manifest.generated.by must be @hraness/wordcell\");\n }\n if (!isRecord(source))\n throw new TypeError(\"manifest.source must be an object\");\n exactKeys(source, [\"selection\", \"notes\", \"digest\"], [], \"manifest.source\");\n if (!isRecord(paths))\n throw new TypeError(\"manifest.paths must be an object\");\n exactKeys(paths, [\"catalog\", \"graph\", \"docs\", \"terms\", \"postingsPrefix\", \"notePrefix\", \"assetPrefix\", \"readerPrefix\"], [], \"manifest.paths\");\n if (!isRecord(search))\n throw new TypeError(\"manifest.search must be an object\");\n exactKeys(search, [\"mode\", \"content\", \"shards\", \"hash\"], [], \"manifest.search\");\n if (search.mode !== \"exact\")\n throw new TypeError(\"manifest.search.mode must be exact\");\n if (search.hash !== \"fnv1a32-8bit\") {\n throw new TypeError(\"manifest.search.hash must be fnv1a32-8bit\");\n }\n if (!isRecord(counts))\n throw new TypeError(\"manifest.counts must be an object\");\n exactKeys(counts, [\"notes\", \"assets\", \"bytes\"], [], \"manifest.counts\");\n if (!isRecord(truncated))\n throw new TypeError(\"manifest.truncated must be an object\");\n exactKeys(truncated, [], [\"terms\", \"text\", \"assets\"], \"manifest.truncated\");\n for (const flag of Object.values(truncated)) {\n if (flag !== true)\n throw new TypeError(\"manifest.truncated flags must be true\");\n }\n const digest = source.digest;\n if (typeof digest !== \"string\" || !/^sha256:[0-9a-f]{64}$/u.test(digest)) {\n throw new TypeError(\"manifest.source.digest must be a sha256 hex digest\");\n }\n return {\n format: WORDCELL_SITE_FORMAT_V1,\n site: Object.freeze({\n title: boundedText(site.title, WORDCELL_SITE_LIMITS_V1.titleBytes, \"site.title\"),\n basePath: boundedText(site.basePath, 1024, \"site.basePath\"),\n ...site.description === undefined ? {} : { description: boundedText(site.description, WORDCELL_SITE_LIMITS_V1.descriptionBytes, \"site.description\") }\n }),\n generated: Object.freeze({\n by: \"@hraness/wordcell\",\n version: boundedText(generated.version, 64, \"generated.version\"),\n ...generated.at === undefined ? {} : { at: boundedText(generated.at, 64, \"generated.at\") }\n }),\n source: Object.freeze({\n selection: source.selection,\n notes: boundedCount(source.notes, WORDCELL_SITE_LIMITS_V1.notes, \"source.notes\"),\n digest\n }),\n paths: Object.freeze({\n catalog: boundedText(paths.catalog, 256, \"paths.catalog\"),\n graph: boundedText(paths.graph, 256, \"paths.graph\"),\n docs: boundedText(paths.docs, 256, \"paths.docs\"),\n terms: boundedText(paths.terms, 256, \"paths.terms\"),\n postingsPrefix: boundedText(paths.postingsPrefix, 256, \"paths.postingsPrefix\"),\n notePrefix: boundedText(paths.notePrefix, 256, \"paths.notePrefix\"),\n assetPrefix: boundedText(paths.assetPrefix, 256, \"paths.assetPrefix\"),\n readerPrefix: boundedText(paths.readerPrefix, 256, \"paths.readerPrefix\")\n }),\n search: Object.freeze({\n mode: \"exact\",\n content: contentSearch(search.content),\n shards: boundedCount(search.shards, WORDCELL_SITE_LIMITS_V1.contentShards, \"search.shards\"),\n hash: \"fnv1a32-8bit\"\n }),\n counts: Object.freeze({\n notes: boundedCount(counts.notes, WORDCELL_SITE_LIMITS_V1.notes, \"counts.notes\"),\n assets: boundedCount(counts.assets, WORDCELL_SITE_LIMITS_V1.assets, \"counts.assets\"),\n bytes: boundedCount(counts.bytes, Number.MAX_SAFE_INTEGER, \"counts.bytes\")\n }),\n truncated: Object.freeze({ ...truncated })\n };\n}\nfunction parseSiteCatalogV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"catalog must be an object\");\n exactKeys(value, [\"format\", \"entries\"], [], \"catalog\");\n if (value.format !== WORDCELL_SITE_CATALOG_FORMAT_V1) {\n throw new TypeError(`catalog format must be ${WORDCELL_SITE_CATALOG_FORMAT_V1}`);\n }\n if (!Array.isArray(value.entries) || value.entries.length > WORDCELL_SITE_LIMITS_V1.notes) {\n throw new TypeError(\"catalog.entries must be a bounded array\");\n }\n const entries = value.entries.map((entry, index) => {\n if (!isRecord(entry))\n throw new TypeError(`catalog entry ${index} must be an object`);\n exactKeys(entry, [\"i\", \"s\", \"t\"], [\"type\", \"g\"], `catalog entry ${index}`);\n return {\n i: boundedCount(entry.i, WORDCELL_SITE_LIMITS_V1.notes, `catalog entry ${index}.i`),\n s: boundedText(entry.s, 1024, `catalog entry ${index}.s`),\n t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `catalog entry ${index}.t`),\n ...entry.type === undefined ? {} : { type: boundedText(entry.type, 256, `catalog entry ${index}.type`) },\n ...entry.g === undefined ? {} : { g: stringArray(entry.g, 128, `catalog entry ${index}.g`) }\n };\n });\n return { format: WORDCELL_SITE_CATALOG_FORMAT_V1, entries };\n}\nfunction docFields(value, field) {\n if (!isRecord(value))\n throw new TypeError(`${field} must be an object`);\n exactKeys(value, [\"t\", \"a\", \"p\", \"g\", \"m\"], [], field);\n return {\n t: boundedText(value.t, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.t`),\n a: boundedText(value.a, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.a`),\n p: boundedText(value.p, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.p`),\n g: boundedText(value.g, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.g`),\n m: boundedText(value.m, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.m`)\n };\n}\nfunction parseSiteDocsV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"docs must be an object\");\n exactKeys(value, [\"format\", \"content\", \"docs\"], [], \"docs\");\n if (value.format !== WORDCELL_SITE_DOCS_FORMAT_V1) {\n throw new TypeError(`docs format must be ${WORDCELL_SITE_DOCS_FORMAT_V1}`);\n }\n const content = contentSearch(value.content);\n if (!Array.isArray(value.docs) || value.docs.length > WORDCELL_SITE_LIMITS_V1.notes) {\n throw new TypeError(\"docs.docs must be a bounded array\");\n }\n const docs = value.docs.map((entry, index) => {\n const field = `docs entry ${index}`;\n if (!isRecord(entry))\n throw new TypeError(`${field} must be an object`);\n exactKeys(entry, [\"i\", \"s\", \"t\", \"p\", \"f\"], [\"x\"], field);\n const inline = entry.x === undefined ? {} : { x: boundedText(entry.x, WORDCELL_SITE_LIMITS_V1.inlineTextBytes, `${field}.x`) };\n return {\n i: boundedCount(entry.i, WORDCELL_SITE_LIMITS_V1.notes, `${field}.i`),\n s: boundedText(entry.s, 1024, `${field}.s`),\n t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field}.t`),\n p: boundedText(entry.p, WORDCELL_SITE_LIMITS_V1.docPreviewBytes + 16, `${field}.p`),\n f: docFields(entry.f, `${field}.f`),\n ...inline\n };\n });\n return { format: WORDCELL_SITE_DOCS_FORMAT_V1, content, docs };\n}\nfunction parseSiteTermsV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"terms must be an object\");\n exactKeys(value, [\"format\", \"terms\"], [], \"terms\");\n if (value.format !== WORDCELL_SITE_TERMS_FORMAT_V1) {\n throw new TypeError(`terms format must be ${WORDCELL_SITE_TERMS_FORMAT_V1}`);\n }\n return {\n format: WORDCELL_SITE_TERMS_FORMAT_V1,\n terms: stringArray(value.terms, WORDCELL_SITE_LIMITS_V1.indexTerms, \"terms.terms\")\n };\n}\nfunction parseSitePostingsV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"postings must be an object\");\n exactKeys(value, [\"format\", \"shard\", \"postings\"], [], \"postings\");\n if (value.format !== WORDCELL_SITE_POSTINGS_FORMAT_V1) {\n throw new TypeError(`postings format must be ${WORDCELL_SITE_POSTINGS_FORMAT_V1}`);\n }\n const shard = value.shard;\n if (typeof shard !== \"string\" || !/^[0-9a-f]{2}$/u.test(shard)) {\n throw new TypeError(\"postings.shard must be a two-digit lowercase hex string\");\n }\n if (!isRecord(value.postings))\n throw new TypeError(\"postings.postings must be an object\");\n const entries = Object.entries(value.postings);\n if (entries.length > WORDCELL_SITE_LIMITS_V1.shardPairs) {\n throw new TypeError(\"postings.postings exceeds the shard pair limit\");\n }\n const postings = Object.create(null);\n for (const [term, docs] of entries) {\n if (!Array.isArray(docs) || docs.length > WORDCELL_SITE_LIMITS_V1.notes) {\n throw new TypeError(`postings for ${JSON.stringify(term)} must be a bounded array`);\n }\n postings[term] = docs.map((doc) => boundedCount(doc, WORDCELL_SITE_LIMITS_V1.notes, \"posting document id\"));\n }\n return { format: WORDCELL_SITE_POSTINGS_FORMAT_V1, shard, postings: Object.freeze(postings) };\n}\nfunction noteLinks(value, field) {\n if (!Array.isArray(value) || value.length > WORDCELL_SITE_LIMITS_V1.notes) {\n throw new TypeError(`${field} must be a bounded array`);\n }\n return value.map((entry, index) => {\n if (!isRecord(entry))\n throw new TypeError(`${field} ${index} must be an object`);\n exactKeys(entry, [\"s\", \"t\"], [], `${field} ${index}`);\n return {\n s: boundedText(entry.s, 1024, `${field} ${index}.s`),\n t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field} ${index}.t`)\n };\n });\n}\nfunction parseSiteNoteV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"note payload must be an object\");\n exactKeys(value, [\"format\", \"id\", \"slug\", \"title\", \"aliases\", \"tags\", \"summary\", \"text\", \"textTruncated\", \"links\", \"backlinks\", \"relations\", \"relationBacklinks\"], [\"type\"], \"note payload\");\n if (value.format !== WORDCELL_SITE_NOTE_FORMAT_V1) {\n throw new TypeError(`note payload format must be ${WORDCELL_SITE_NOTE_FORMAT_V1}`);\n }\n if (value.textTruncated !== true && value.textTruncated !== false) {\n throw new TypeError(\"note payload textTruncated must be boolean\");\n }\n return {\n format: WORDCELL_SITE_NOTE_FORMAT_V1,\n id: boundedText(value.id, 1024, \"note.id\"),\n slug: boundedText(value.slug, 1024, \"note.slug\"),\n title: boundedText(value.title, WORDCELL_SITE_LIMITS_V1.titleBytes, \"note.title\"),\n aliases: stringArray(value.aliases, 128, \"note.aliases\"),\n ...value.type === undefined ? {} : { type: boundedText(value.type, 256, \"note.type\") },\n tags: stringArray(value.tags, 128, \"note.tags\"),\n summary: boundedText(value.summary, WORDCELL_SITE_LIMITS_V1.descriptionBytes, \"note.summary\"),\n text: boundedText(value.text, WORDCELL_SITE_LIMITS_V1.noteTextBytes, \"note.text\"),\n textTruncated: value.textTruncated,\n links: noteLinks(value.links, \"note.links\"),\n backlinks: noteLinks(value.backlinks, \"note.backlinks\"),\n relations: noteRelations(value.relations, \"note.relations\"),\n relationBacklinks: noteRelations(value.relationBacklinks, \"note.relationBacklinks\")\n };\n}\nfunction noteRelations(value, field) {\n if (!Array.isArray(value) || value.length > WORDCELL_SITE_LIMITS_V1.notes) {\n throw new TypeError(`${field} must be a bounded array`);\n }\n return value.map((entry, index) => {\n if (!isRecord(entry))\n throw new TypeError(`${field} ${index} must be an object`);\n exactKeys(entry, [\"p\", \"s\", \"t\"], [], `${field} ${index}`);\n return {\n p: boundedText(entry.p, 256, `${field} ${index}.p`),\n s: boundedText(entry.s, 1024, `${field} ${index}.s`),\n t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field} ${index}.t`)\n };\n });\n}\nfunction parseSiteGraphV1(value) {\n if (!isRecord(value))\n throw new TypeError(\"graph must be an object\");\n exactKeys(value, [\"format\", \"edges\"], [], \"graph\");\n if (value.format !== WORDCELL_SITE_GRAPH_FORMAT_V1) {\n throw new TypeError(`graph format must be ${WORDCELL_SITE_GRAPH_FORMAT_V1}`);\n }\n if (!Array.isArray(value.edges) || value.edges.length > 1e6) {\n throw new TypeError(\"graph.edges must be a bounded array\");\n }\n const edges = value.edges.map((entry, index) => {\n if (!isRecord(entry))\n throw new TypeError(`graph edge ${index} must be an object`);\n exactKeys(entry, [\"s\", \"t\", \"k\"], [\"p\"], `graph edge ${index}`);\n if (entry.k !== \"link\" && entry.k !== \"relation\") {\n throw new TypeError(`graph edge ${index}.k must be link or relation`);\n }\n return {\n s: boundedCount(entry.s, WORDCELL_SITE_LIMITS_V1.notes, `graph edge ${index}.s`),\n t: boundedCount(entry.t, WORDCELL_SITE_LIMITS_V1.notes, `graph edge ${index}.t`),\n k: entry.k,\n ...entry.p === undefined ? {} : { p: boundedText(entry.p, 256, `graph edge ${index}.p`) }\n };\n });\n return { format: WORDCELL_SITE_GRAPH_FORMAT_V1, edges };\n}\n\n// src/publish-graph.ts\nvar WORDCELL_SITE_GRAPH_VIEW_LIMIT = 1000;\nfunction mulberry32(seed) {\n let state = seed | 0;\n return () => {\n state = state + 1831565813 | 0;\n let value = Math.imul(state ^ state >>> 15, 1 | state);\n value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;\n return ((value ^ value >>> 14) >>> 0) / 4294967296;\n };\n}\nfunction siteGraphSeed(slugs, edges) {\n let hash = 2166136261;\n const mix = (text) => {\n for (let index = 0;index < text.length; index += 1) {\n hash ^= text.charCodeAt(index);\n hash = Math.imul(hash, 16777619) >>> 0;\n }\n };\n for (const slug of slugs) {\n mix(slug);\n mix(`\n`);\n }\n mix(\"\\x00\");\n for (const edge of edges) {\n mix(`${String(edge.s)}-${String(edge.t)},`);\n }\n return hash >>> 0;\n}\nfunction layoutSiteGraph(nodeCount, edges, options) {\n if (nodeCount <= 0)\n return [];\n const width = options.width ?? 1000;\n const height = options.height ?? 700;\n const iterations = options.iterations ?? Math.max(80, Math.min(260, 24000 / nodeCount));\n const random = mulberry32(options.seed);\n const x = new Float64Array(nodeCount);\n const y = new Float64Array(nodeCount);\n const dx = new Float64Array(nodeCount);\n const dy = new Float64Array(nodeCount);\n for (let index = 0;index < nodeCount; index += 1) {\n x[index] = (random() - 0.5) * width;\n y[index] = (random() - 0.5) * height;\n }\n if (nodeCount === 1) {\n x[0] = 0;\n y[0] = 0;\n return [{ x: 0, y: 0 }];\n }\n const pairs = new Set;\n const deduped = [];\n for (const edge of edges) {\n const s = Math.min(edge.s, edge.t);\n const t = Math.max(edge.s, edge.t);\n if (s === t || s < 0 || t >= nodeCount)\n continue;\n const key = s * nodeCount + t;\n if (pairs.has(key))\n continue;\n pairs.add(key);\n deduped.push({ s, t });\n }\n const area = width * height;\n const k = Math.sqrt(area / nodeCount);\n const centerX = 0;\n const centerY = 0;\n const epsilon = 0.01;\n const gravity = 0.05;\n let temperature = k * 2;\n for (let iteration = 0;iteration < iterations; iteration += 1) {\n dx.fill(0);\n dy.fill(0);\n for (let i = 0;i < nodeCount; i += 1) {\n for (let j = i + 1;j < nodeCount; j += 1) {\n const deltaX = (x[i] ?? 0) - (x[j] ?? 0);\n const deltaY = (y[i] ?? 0) - (y[j] ?? 0);\n const distance = Math.max(Math.sqrt(deltaX * deltaX + deltaY * deltaY), epsilon);\n const force = k * k / distance;\n const ux = deltaX / distance * force;\n const uy = deltaY / distance * force;\n dx[i] = (dx[i] ?? 0) + ux;\n dy[i] = (dy[i] ?? 0) + uy;\n dx[j] = (dx[j] ?? 0) - ux;\n dy[j] = (dy[j] ?? 0) - uy;\n }\n }\n for (const edge of deduped) {\n const deltaX = (x[edge.s] ?? 0) - (x[edge.t] ?? 0);\n const deltaY = (y[edge.s] ?? 0) - (y[edge.t] ?? 0);\n const distance = Math.max(Math.sqrt(deltaX * deltaX + deltaY * deltaY), epsilon);\n const force = distance * distance / k;\n const ux = deltaX / distance * force;\n const uy = deltaY / distance * force;\n dx[edge.s] = (dx[edge.s] ?? 0) - ux;\n dy[edge.s] = (dy[edge.s] ?? 0) - uy;\n dx[edge.t] = (dx[edge.t] ?? 0) + ux;\n dy[edge.t] = (dy[edge.t] ?? 0) + uy;\n }\n for (let i = 0;i < nodeCount; i += 1) {\n dx[i] = (dx[i] ?? 0) + (centerX - (x[i] ?? 0)) * gravity;\n dy[i] = (dy[i] ?? 0) + (centerY - (y[i] ?? 0)) * gravity;\n }\n for (let i = 0;i < nodeCount; i += 1) {\n const moveX = dx[i] ?? 0;\n const moveY = dy[i] ?? 0;\n const length = Math.sqrt(moveX * moveX + moveY * moveY);\n if (length === 0)\n continue;\n const bounded = Math.min(length, temperature);\n x[i] = (x[i] ?? 0) + moveX / length * bounded;\n y[i] = (y[i] ?? 0) + moveY / length * bounded;\n }\n temperature *= 1 - (iteration + 1) / iterations;\n }\n const points = [];\n for (let index = 0;index < nodeCount; index += 1) {\n points.push({ x: x[index] ?? 0, y: y[index] ?? 0 });\n }\n return points;\n}\nfunction siteGraphDegrees(nodeCount, edges) {\n const degree = new Array(Math.max(0, nodeCount)).fill(0);\n for (const edge of edges) {\n if (edge.s === edge.t || edge.s < 0 || edge.t < 0)\n continue;\n if (edge.s >= nodeCount || edge.t >= nodeCount)\n continue;\n degree[edge.s] = (degree[edge.s] ?? 0) + 1;\n degree[edge.t] = (degree[edge.t] ?? 0) + 1;\n }\n return degree;\n}\n\n// src/publish-search.ts\nvar PUBLISH_FIELD_TITLE = 1;\nvar PUBLISH_FIELD_ALIAS = 2;\nvar PUBLISH_FIELD_PATH = 4;\nvar PUBLISH_FIELD_TAG = 8;\nvar PUBLISH_FIELD_METADATA = 16;\nvar PUBLISH_FIELD_CONTENT = 32;\nvar PUBLISH_SEARCH_WEIGHTS_V1 = Object.freeze({\n identity: Object.freeze({ title: 1000, alias: 950, path: 900 }),\n phrase: Object.freeze({\n title: 400,\n alias: 350,\n path: 300,\n tag: 250,\n content: 150,\n metadata: 100\n }),\n term: Object.freeze({\n title: 40,\n alias: 35,\n path: 30,\n tag: 30,\n metadata: 10,\n content: 5\n }),\n coverage: 100\n});\nvar MAX_PUBLISH_QUERY_BYTES = 16 * 1024;\nvar MAX_PUBLISH_QUERY_TERMS = 64;\nvar MAX_PUBLISH_PREFIX_EXPANSIONS = 32;\nvar TERM_PATTERN = /[\\p{L}\\p{N}][\\p{L}\\p{N}._/-]*/gu;\nvar textEncoder2 = new TextEncoder;\nfunction publishUtf8Bytes(value) {\n return textEncoder2.encode(value).byteLength;\n}\nfunction publishNormalize(value) {\n return value.normalize(\"NFC\").toLocaleLowerCase(\"en-US\");\n}\nvar MAX_PUBLISH_QUERY_FILTERS = 8;\nvar FILTER_PATTERN = /^(tag|type|path):(\\S+)$/u;\nfunction trimSlashes(value) {\n let start = 0;\n let end = value.length;\n while (start < end && value.charCodeAt(start) === 47)\n start += 1;\n while (end > start && value.charCodeAt(end - 1) === 47)\n end -= 1;\n return value.slice(start, end);\n}\nfunction publishQueryParts(raw) {\n const tags = [];\n const types = [];\n const paths = [];\n const rest = [];\n for (const token of raw.split(/\\s+/u)) {\n if (token === \"\")\n continue;\n const match = FILTER_PATTERN.exec(token);\n const value = match === null ? \"\" : publishNormalize(trimSlashes(match[2] ?? \"\"));\n if (match === null || value === \"\") {\n rest.push(token);\n continue;\n }\n const bucket = match[1] === \"tag\" ? tags : match[1] === \"type\" ? types : paths;\n if (bucket.length < MAX_PUBLISH_QUERY_FILTERS)\n bucket.push(value);\n else\n rest.push(token);\n }\n return { filters: { tags, types, paths }, text: rest.join(\" \") };\n}\nfunction publishDocMatchesFilters(doc, filters, type) {\n if (filters.tags.length > 0) {\n const tags = doc.f.g === \"\" ? [] : doc.f.g.split(`\n`);\n for (const tag of filters.tags) {\n if (!tags.includes(publishNormalize(tag)))\n return false;\n }\n }\n if (filters.types.length > 0) {\n const docType = publishNormalize(type === undefined || type === \"\" ? \"note\" : type);\n for (const wanted of filters.types) {\n if (docType !== publishNormalize(wanted))\n return false;\n }\n }\n if (filters.paths.length > 0) {\n const slug = publishNormalize(doc.s);\n const fields = doc.f.p === \"\" ? [] : doc.f.p.split(`\n`);\n for (const value of filters.paths) {\n const prefix = publishNormalize(trimSlashes(value));\n const boundary = `${prefix}/`;\n const inSlug = slug === prefix || slug.startsWith(boundary);\n const inFields = fields.some((line) => line === prefix || line.startsWith(boundary));\n if (!inSlug && !inFields)\n return false;\n }\n }\n return true;\n}\nvar MAX_PUBLISH_MARK_RANGES = 32;\nfunction publishMarkRanges(text, terms, maximum = MAX_PUBLISH_MARK_RANGES) {\n const display = text.normalize(\"NFC\");\n let normalized = \"\";\n const map = [];\n for (let offset = 0;offset < display.length; ) {\n const point = display.codePointAt(offset) ?? 0;\n const character = String.fromCodePoint(point);\n const lowered = character.toLocaleLowerCase(\"en-US\");\n for (let index = 0;index < lowered.length; index += 1)\n map.push(offset);\n normalized += lowered;\n offset += character.length;\n }\n const ranges = [];\n for (const term of terms) {\n const needle = publishNormalize(term);\n if (needle === \"\")\n continue;\n let from = 0;\n while (ranges.length < maximum) {\n const hit = normalized.indexOf(needle, from);\n if (hit === -1)\n break;\n const start = map[hit] ?? 0;\n const tail = hit + needle.length;\n const end = tail >= map.length ? display.length : map[tail] ?? display.length;\n if (end > start)\n ranges.push({ start, end });\n from = tail === 0 ? 1 : tail;\n }\n if (ranges.length >= maximum)\n break;\n }\n ranges.sort((left, right) => left.start - right.start || left.end - right.end);\n const merged = [];\n for (const range of ranges) {\n const last = merged[merged.length - 1];\n if (last !== undefined && range.start <= last.end) {\n merged[merged.length - 1] = { start: last.start, end: Math.max(last.end, range.end) };\n } else {\n merged.push(range);\n }\n }\n return merged;\n}\nfunction publishQuery(value) {\n if (typeof value !== \"string\") {\n throw new TypeError(\"Search query must be a string.\");\n }\n if (publishUtf8Bytes(value) > MAX_PUBLISH_QUERY_BYTES) {\n throw new RangeError(`Search query exceeds the ${MAX_PUBLISH_QUERY_BYTES}-byte limit.`);\n }\n const raw = value.trim();\n const normalized = publishNormalize(raw);\n const terms = [];\n const seen = new Set;\n for (const match of normalized.matchAll(TERM_PATTERN)) {\n if (seen.has(match[0]))\n continue;\n seen.add(match[0]);\n terms.push(match[0]);\n if (terms.length > MAX_PUBLISH_QUERY_TERMS) {\n throw new RangeError(`Search query may contain at most ${MAX_PUBLISH_QUERY_TERMS} unique normalized terms.`);\n }\n }\n return { raw, normalized, terms };\n}\nfunction publishShardName(term) {\n let hash = 2166136261;\n for (let index = 0;index < term.length; index += 1) {\n hash ^= term.charCodeAt(index);\n hash = Math.imul(hash, 16777619) >>> 0;\n }\n return (hash >>> 24).toString(16).padStart(2, \"0\");\n}\nfunction publishPrefixTerms(dictionary, prefix, maximum = MAX_PUBLISH_PREFIX_EXPANSIONS) {\n if (prefix === \"\")\n return [];\n let low = 0;\n let high = dictionary.length;\n while (low < high) {\n const middle = low + high >> 1;\n if ((dictionary[middle] ?? \"\") < prefix)\n low = middle + 1;\n else\n high = middle;\n }\n const found = [];\n for (let index = low;index < dictionary.length && found.length < maximum; index += 1) {\n const term = dictionary[index] ?? \"\";\n if (!term.startsWith(prefix))\n break;\n found.push(term);\n }\n return found;\n}\nfunction fieldMasks(term, fields) {\n let mask = 0;\n if (fields.t.includes(term))\n mask |= PUBLISH_FIELD_TITLE;\n if (fields.a.includes(term))\n mask |= PUBLISH_FIELD_ALIAS;\n if (fields.p.includes(term))\n mask |= PUBLISH_FIELD_PATH;\n if (fields.g.includes(term))\n mask |= PUBLISH_FIELD_TAG;\n if (fields.m.includes(term))\n mask |= PUBLISH_FIELD_METADATA;\n return mask;\n}\nfunction scorePublishDocument(input, query) {\n const fields = input.doc.f;\n const phrase = query.normalized;\n const weights = PUBLISH_SEARCH_WEIGHTS_V1;\n let score = 0;\n let identity = false;\n let phraseMatched = false;\n if (phrase !== \"\") {\n if (fields.t === phrase) {\n identity = true;\n score += weights.identity.title;\n }\n if (fields.a.split(`\n`).includes(phrase)) {\n identity = true;\n score += weights.identity.alias;\n }\n if (fields.p.split(`\n`).includes(phrase)) {\n identity = true;\n score += weights.identity.path;\n }\n const phraseFields = [\n [PUBLISH_FIELD_TITLE, fields.t, weights.phrase.title],\n [PUBLISH_FIELD_ALIAS, fields.a, weights.phrase.alias],\n [PUBLISH_FIELD_PATH, fields.p, weights.phrase.path],\n [PUBLISH_FIELD_TAG, fields.g, weights.phrase.tag],\n [PUBLISH_FIELD_METADATA, fields.m, weights.phrase.metadata]\n ];\n for (const [, value, weight] of phraseFields) {\n if (value.includes(phrase)) {\n phraseMatched = true;\n score += weight;\n }\n }\n if (input.contentText !== undefined && input.contentText.includes(phrase)) {\n phraseMatched = true;\n score += weights.phrase.content;\n }\n }\n let matchedTerms = 0;\n for (const term of query.terms) {\n const mask = fieldMasks(term, fields) | (input.contentTerms.has(term) ? PUBLISH_FIELD_CONTENT : 0);\n if (mask === 0)\n continue;\n matchedTerms += 1;\n if (mask & PUBLISH_FIELD_TITLE)\n score += weights.term.title;\n if (mask & PUBLISH_FIELD_ALIAS)\n score += weights.term.alias;\n if (mask & PUBLISH_FIELD_PATH)\n score += weights.term.path;\n if (mask & PUBLISH_FIELD_TAG)\n score += weights.term.tag;\n if (mask & PUBLISH_FIELD_METADATA)\n score += weights.term.metadata;\n if (mask & PUBLISH_FIELD_CONTENT)\n score += weights.term.content;\n }\n const requiredTerms = query.terms.length <= 1 ? query.terms.length : Math.min(3, Math.ceil(query.terms.length / 2));\n if (!identity && !phraseMatched && matchedTerms < requiredTerms)\n return null;\n if (query.terms.length > 0) {\n score += Math.round(matchedTerms / query.terms.length * weights.coverage);\n }\n if (score === 0)\n return null;\n return { score, identity, phraseMatched, matchedTerms };\n}\nfunction comparePublishScores(left, right) {\n return Number(right.identity) - Number(left.identity) || right.score - left.score || left.i - right.i;\n}\nfunction publishSnippet(text, query, fallback, windowBytes = 160) {\n if (!Number.isSafeInteger(windowBytes) || windowBytes < 0)\n throw new RangeError(\"Snippet window must be a nonnegative safe byte count.\");\n const display = text.normalize(\"NFC\");\n const normalized = publishNormalize(display);\n let match = query.normalized === \"\" ? -1 : normalized.indexOf(query.normalized);\n if (match < 0) {\n for (const term of query.terms) {\n match = normalized.indexOf(term);\n if (match >= 0)\n break;\n }\n }\n if (match < 0)\n return fallback;\n let offset = 0, normalizedOffset = 0;\n for (const character of display) {\n const width = character.toLocaleLowerCase(\"en-US\").length;\n if (normalizedOffset + width > match)\n break;\n normalizedOffset += width;\n offset += character.length;\n }\n const edge = windowBytes >= 6 ? \"…\" : \"\";\n const budget = windowBytes - 2 * publishUtf8Bytes(edge);\n const previous = (end2) => {\n const last = display.charCodeAt(end2 - 1);\n return end2 > 1 && last >= 56320 && last <= 57343 && display.charCodeAt(end2 - 2) >= 55296 && display.charCodeAt(end2 - 2) <= 56319 ? end2 - 2 : end2 - 1;\n };\n let start = offset, end = offset, used = 0;\n while (start > 0) {\n const before = previous(start), bytes = publishUtf8Bytes(display.slice(before, start));\n if (used + bytes > Math.floor(budget / 2))\n break;\n start = before;\n used += bytes;\n }\n while (end < display.length) {\n const character = String.fromCodePoint(display.codePointAt(end) ?? 0), bytes = publishUtf8Bytes(character);\n if (used + bytes > budget)\n break;\n end += character.length;\n used += bytes;\n }\n while (start > 0) {\n const before = previous(start), bytes = publishUtf8Bytes(display.slice(before, start));\n if (used + bytes > budget)\n break;\n start = before;\n used += bytes;\n }\n const snippet = display.slice(start, end).replace(/\\s+/gu, \" \").trim();\n return `${start > 0 ? edge : \"\"}${snippet}${end < display.length ? edge : \"\"}`;\n}\n\n// src/publish-theme.ts\nvar wordcellPalettes = [\n \"wordcell\",\n \"catppuccin\",\n \"gruvbox\",\n \"rose-pine\",\n \"tokyo-night\"\n];\nvar wordcellPaletteLabels = {\n wordcell: \"Wordcell\",\n catppuccin: \"Catppuccin\",\n gruvbox: \"Gruvbox\",\n \"rose-pine\": \"Rosé Pine\",\n \"tokyo-night\": \"Tokyo Night\"\n};\nvar wordcellAppearanceModes = [\"system\", \"light\", \"dark\"];\nvar wordcellAppearanceModeLabels = {\n system: \"System\",\n light: \"Light\",\n dark: \"Dark\"\n};\nfunction wordcellAppearanceBridge() {\n return globalThis.wordcellAppearance;\n}\n\n// src/publish-reader/reader.ts\nvar dom = globalThis;\nasync function fetchJson(base, path) {\n const response = await dom.fetch(`${base}${path}`);\n if (!response.ok)\n return;\n return response.json();\n}\nasync function loadIndex(base) {\n const manifestRaw = await fetchJson(base, \"manifest.json\");\n if (manifestRaw === undefined)\n return;\n const manifest = parseSiteManifestV1(manifestRaw);\n const docsRaw = await fetchJson(base, manifest.paths.docs);\n const termsRaw = await fetchJson(base, manifest.paths.terms);\n if (docsRaw === undefined || termsRaw === undefined)\n return;\n const docs = parseSiteDocsV1(docsRaw);\n const terms = parseSiteTermsV1(termsRaw);\n return {\n manifest,\n docs: docs.docs,\n terms: terms.terms,\n postings: new Map,\n notes: new Map,\n catalog: undefined\n };\n}\nasync function loadShard(index, base, shard) {\n const cached = index.postings.get(shard);\n if (cached !== undefined)\n return cached;\n const raw = await fetchJson(base, `${index.manifest.paths.postingsPrefix}${shard}.json`);\n const postings = raw === undefined ? new Map : new Map(Object.entries(parseSitePostingsV1(raw).postings));\n index.postings.set(shard, postings);\n return postings;\n}\nasync function loadCatalog(index, base) {\n if (index.catalog !== undefined)\n return index.catalog;\n const raw = await fetchJson(base, index.manifest.paths.catalog);\n if (raw === undefined)\n return;\n index.catalog = parseSiteCatalogV1(raw);\n return index.catalog;\n}\nasync function hydrate(index, base, slug) {\n const cached = index.notes.get(slug);\n if (cached !== undefined)\n return cached;\n const path = slug === \"\" ? \"index.json\" : `${index.manifest.paths.notePrefix}${slug}.json`;\n const raw = await fetchJson(base, path);\n if (raw === undefined)\n return;\n const note = parseSiteNoteV1(raw);\n index.notes.set(slug, note);\n return note;\n}\nvar FILTER_ONLY_SCORE = Object.freeze({\n score: 0,\n identity: false,\n phraseMatched: false,\n matchedTerms: 0\n});\nfunction hasFilters(filters) {\n return filters.tags.length + filters.types.length + filters.paths.length > 0;\n}\nasync function search(index, base, raw) {\n const { filters, text } = publishQueryParts(raw);\n const filtered = hasFilters(filters);\n const query = publishQuery(text);\n if (!filtered && query.terms.length === 0 && query.normalized === \"\")\n return [];\n let typeByIndex;\n if (filters.types.length > 0) {\n const catalog = await loadCatalog(index, base);\n typeByIndex = new Map((catalog?.entries ?? []).map((entry) => [entry.i, entry.type ?? \"note\"]));\n }\n const postingsHits = new Map;\n if (index.manifest.search.content === \"shards\" && query.terms.length > 0) {\n const expandedByTerm = new Map;\n const shards = new Set;\n for (const term of query.terms) {\n const expanded = publishPrefixTerms(index.terms, term, MAX_PUBLISH_PREFIX_EXPANSIONS);\n expandedByTerm.set(term, expanded);\n for (const candidate of expanded)\n shards.add(publishShardName(candidate));\n }\n const tables = new Map;\n for (const shard of shards)\n tables.set(shard, await loadShard(index, base, shard));\n for (const doc of index.docs) {\n const hits2 = new Set;\n for (const term of query.terms) {\n const matched = (expandedByTerm.get(term) ?? []).some((candidate) => tables.get(publishShardName(candidate))?.get(candidate)?.includes(doc.i) === true);\n if (matched)\n hits2.add(term);\n }\n postingsHits.set(doc.i, hits2);\n }\n }\n const hits = [];\n for (const doc of index.docs) {\n if (!publishDocMatchesFilters(doc, filters, typeByIndex?.get(doc.i)))\n continue;\n if (query.terms.length === 0 && query.normalized === \"\") {\n hits.push({ doc, score: FILTER_ONLY_SCORE });\n continue;\n }\n const inline = index.manifest.search.content === \"inline\" ? doc.x : undefined;\n const contentTerms = inline !== undefined ? new Set(query.terms.filter((term) => inline.includes(term))) : postingsHits.get(doc.i) ?? new Set;\n const scored = scorePublishDocument({ doc, contentTerms, ...inline === undefined ? {} : { contentText: inline } }, query);\n if (scored === null)\n continue;\n hits.push({ doc, score: scored });\n }\n hits.sort((left, right) => comparePublishScores({ i: left.doc.i, ...left.score }, { i: right.doc.i, ...right.score }));\n return hits.slice(0, WORDCELL_SITE_LIMITS_V1.searchResults);\n}\nfunction resultHref(base, slug) {\n return slug === \"\" ? base : `${base}n/${encodeURI(slug)}/`;\n}\nfunction snippetFor(index, doc, query) {\n const hydrated = index.notes.get(doc.s);\n const text = hydrated?.text;\n return text === undefined ? doc.p : publishSnippet(text, query, doc.p);\n}\nfunction appendMarked(parent, text, terms) {\n const display = text.normalize(\"NFC\");\n const ranges = publishMarkRanges(display, terms);\n if (ranges.length === 0) {\n parent.appendChild(dom.document.createTextNode(display));\n return;\n }\n let cursor = 0;\n for (const range of ranges) {\n if (range.start > cursor) {\n parent.appendChild(dom.document.createTextNode(display.slice(cursor, range.start)));\n }\n const mark = dom.document.createElement(\"mark\");\n mark.textContent = display.slice(range.start, range.end);\n parent.appendChild(mark);\n cursor = range.end;\n }\n if (cursor < display.length) {\n parent.appendChild(dom.document.createTextNode(display.slice(cursor)));\n }\n}\nfunction buildOverlay(base) {\n const overlay = dom.document.createElement(\"div\");\n overlay.className = \"wordcell-search-overlay\";\n overlay.setAttribute(\"role\", \"dialog\");\n overlay.setAttribute(\"aria-modal\", \"true\");\n overlay.setAttribute(\"aria-label\", \"Search\");\n const panel = dom.document.createElement(\"div\");\n panel.className = \"wordcell-search-panel\";\n const input = dom.document.createElement(\"input\");\n input.type = \"search\";\n input.placeholder = \"Search this site… (tag:, type:, path:)\";\n input.setAttribute(\"aria-label\", \"Search this site\");\n input.setAttribute(\"autocomplete\", \"off\");\n input.setAttribute(\"spellcheck\", \"false\");\n const status = dom.document.createElement(\"div\");\n status.className = \"wordcell-search-status\";\n status.setAttribute(\"role\", \"status\");\n const list = dom.document.createElement(\"ul\");\n list.className = \"wordcell-search-results\";\n panel.appendChild(input);\n panel.appendChild(status);\n panel.appendChild(list);\n overlay.appendChild(panel);\n let index;\n let loading;\n let hits = [];\n let active = -1;\n let debounce = 0;\n const render = () => {\n list.innerHTML = \"\";\n let query;\n try {\n query = publishQuery(publishQueryParts(input.value).text);\n } catch {\n query = undefined;\n }\n const terms = query?.terms ?? [];\n for (const [position, hit] of hits.entries()) {\n const item = dom.document.createElement(\"li\");\n const link = dom.document.createElement(\"a\");\n link.href = resultHref(base, hit.doc.s);\n link.className = position === active ? \"active\" : \"\";\n const title = dom.document.createElement(\"span\");\n title.className = \"title\";\n appendMarked(title, hit.doc.t, terms);\n const snippet = dom.document.createElement(\"span\");\n snippet.className = \"snippet\";\n const snippetText = index === undefined || query === undefined || query.terms.length === 0 ? hit.doc.p : snippetFor(index, hit.doc, query);\n appendMarked(snippet, snippetText, terms);\n link.appendChild(title);\n link.appendChild(snippet);\n item.appendChild(link);\n list.appendChild(item);\n }\n status.textContent = hits.length === 0 ? input.value.trim() === \"\" ? \"Type to search. Filter with tag:, type:, path:.\" : \"No results.\" : `${hits.length} result${hits.length === 1 ? \"\" : \"s\"}`;\n };\n const run = () => {\n (async () => {\n if (index === undefined) {\n loading ??= loadIndex(base);\n index = await loading;\n if (index === undefined) {\n status.textContent = \"Search is unavailable for this site.\";\n return;\n }\n }\n try {\n hits = await search(index, base, input.value);\n } catch {\n hits = [];\n }\n active = hits.length === 0 ? -1 : 0;\n render();\n const current = index;\n Promise.all(hits.slice(0, WORDCELL_SITE_LIMITS_V1.searchHydration).map((hit) => hydrate(current, base, hit.doc.s))).then(() => render());\n })();\n };\n input.addEventListener(\"input\", () => {\n dom.clearTimeout(debounce);\n debounce = dom.setTimeout(run, 80);\n });\n input.addEventListener(\"keydown\", (event) => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n if (hits.length > 0) {\n active = Math.min(hits.length - 1, active + 1);\n render();\n }\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n if (hits.length > 0) {\n active = Math.max(0, active - 1);\n render();\n }\n } else if (event.key === \"Enter\") {\n const hit = hits[active];\n if (hit !== undefined) {\n event.preventDefault();\n dom.location.assign(resultHref(base, hit.doc.s));\n }\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n overlay.remove();\n }\n });\n overlay.addEventListener(\"click\", (event) => {\n if (event.target === overlay)\n overlay.remove();\n });\n return {\n open() {\n dom.document.body.appendChild(overlay);\n hits = [];\n active = -1;\n render();\n input.focus();\n if (index === undefined || input.value.trim() !== \"\")\n run();\n },\n close() {\n overlay.remove();\n }\n };\n}\nvar GRAPH_MIN_SCALE = 0.05;\nvar GRAPH_MAX_SCALE = 8;\nfunction cssVar(shell, name, fallback) {\n const value = dom.getComputedStyle?.(shell).getPropertyValue(name).trim();\n return value === undefined || value === \"\" ? fallback : value;\n}\nfunction graphTheme(shell) {\n return {\n bg: cssVar(shell, \"--wordcell-bg\", \"#ffffff\"),\n fg: cssVar(shell, \"--wordcell-fg\", \"#1a1a1a\"),\n muted: cssVar(shell, \"--wordcell-muted\", \"#5f6368\"),\n border: cssVar(shell, \"--wordcell-border\", \"#e1e4e8\"),\n accent: cssVar(shell, \"--wordcell-accent\", \"#0b5bd3\")\n };\n}\nfunction fitView(points, width, height) {\n let minX = 0;\n let minY = 0;\n let maxX = 0;\n let maxY = 0;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n maxX = Math.max(maxX, point.x);\n minY = Math.min(minY, point.y);\n maxY = Math.max(maxY, point.y);\n }\n const spanX = Math.max(maxX - minX, 40);\n const spanY = Math.max(maxY - minY, 40);\n const margin = 48;\n const scale = Math.min(GRAPH_MAX_SCALE, Math.max(GRAPH_MIN_SCALE, Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY)));\n return {\n scale,\n tx: width / 2 - (minX + maxX) / 2 * scale,\n ty: height / 2 - (minY + maxY) / 2 * scale,\n hover: -1,\n focus: -1\n };\n}\nfunction drawGraph(context, ratio, theme, view, points, titles, degrees, edges, neighborFocus) {\n const { scale, tx, ty } = view;\n context.setTransform(scale * ratio, 0, 0, scale * ratio, tx * ratio, ty * ratio);\n const inv = 1 / scale;\n for (const edge of edges) {\n const a = points[edge.s];\n const b = points[edge.t];\n if (a === undefined || b === undefined)\n continue;\n const focused = view.focus >= 0 && (edge.s === view.focus || edge.t === view.focus);\n const hovered = view.hover >= 0 && (edge.s === view.hover || edge.t === view.hover);\n context.beginPath();\n context.moveTo(a.x, a.y);\n context.lineTo(b.x, b.y);\n context.lineWidth = (focused || hovered ? 1.8 : 1) * inv;\n context.strokeStyle = focused || hovered ? theme.accent : theme.border;\n context.globalAlpha = focused || hovered ? 1 : 0.7;\n context.setLineDash(edge.kind === \"relation\" ? [4 * inv, 3 * inv] : []);\n context.stroke();\n }\n context.setLineDash([]);\n context.globalAlpha = 1;\n const labelEverywhere = points.length <= 48;\n for (const [index, point] of points.entries()) {\n const degree = degrees[index] ?? 0;\n const radius = Math.min(9, 3 + degree * 0.8) * inv;\n const active = index === view.hover || index === view.focus || neighborFocus.has(index);\n context.beginPath();\n context.arc(point.x, point.y, radius, 0, Math.PI * 2);\n context.fillStyle = active ? theme.accent : theme.muted;\n context.fill();\n context.lineWidth = 1.2 * inv;\n context.strokeStyle = theme.bg;\n context.stroke();\n }\n context.font = `${11 / scale}px ui-sans-serif, system-ui, sans-serif`;\n context.textAlign = \"center\";\n context.textBaseline = \"top\";\n for (const [index, point] of points.entries()) {\n const degree = degrees[index] ?? 0;\n const active = index === view.hover || index === view.focus || neighborFocus.has(index);\n if (!labelEverywhere && !active && degree < 3)\n continue;\n context.fillStyle = active ? theme.fg : theme.muted;\n context.fillText(titles[index] ?? \"\", point.x, point.y + (Math.min(9, 3 + degree * 0.8) + 4) * inv);\n }\n}\nfunction initGraph(base) {\n const shell = dom.document.querySelector(\"[data-wordcell-graph]\");\n if (shell === null)\n return;\n const canvas = shell.querySelector(\"[data-wordcell-graph-canvas]\");\n const status = shell.querySelector(\"[data-wordcell-graph-status]\");\n const reset = shell.querySelector(\"[data-wordcell-graph-reset]\");\n if (canvas === null)\n return;\n const context = canvas.getContext(\"2d\");\n if (context === null) {\n if (status !== null)\n status.textContent = \"Canvas is unavailable; every note is listed below.\";\n return;\n }\n (async () => {\n const manifestRaw = await fetchJson(base, \"manifest.json\");\n if (manifestRaw === undefined)\n throw new Error(\"manifest fetch failed\");\n const manifest = parseSiteManifestV1(manifestRaw);\n const [catalogRaw, graphRaw] = await Promise.all([\n fetchJson(base, manifest.paths.catalog),\n fetchJson(base, manifest.paths.graph)\n ]);\n if (catalogRaw === undefined || graphRaw === undefined)\n throw new Error(\"graph data fetch failed\");\n const catalog = parseSiteCatalogV1(catalogRaw);\n const graph = parseSiteGraphV1(graphRaw);\n const nodes = catalog.entries;\n if (nodes.length > WORDCELL_SITE_GRAPH_VIEW_LIMIT) {\n if (status !== null) {\n status.textContent = `This site has ${nodes.length} notes — over the ${WORDCELL_SITE_GRAPH_VIEW_LIMIT}-node interactive limit. Every note is listed below.`;\n }\n return;\n }\n const edges = [];\n const drawn = [];\n for (const edge of graph.edges) {\n if (edge.s === edge.t || edge.s < 0 || edge.t < 0)\n continue;\n if (edge.s >= nodes.length || edge.t >= nodes.length)\n continue;\n edges.push({ s: edge.s, t: edge.t });\n drawn.push({ s: edge.s, t: edge.t, kind: edge.k });\n }\n const slugs = nodes.map((entry) => entry.s);\n const titles = nodes.map((entry) => entry.t);\n const seed = siteGraphSeed(slugs, edges);\n const points = layoutSiteGraph(nodes.length, edges, { seed });\n const degrees = siteGraphDegrees(nodes.length, edges);\n const hrefFor = (index) => resultHref(base, slugs[index] ?? \"\");\n const rect = canvas.getBoundingClientRect();\n const ratio = dom.devicePixelRatio ?? 1;\n const cssWidth = Math.max(320, rect.width || 960);\n const cssHeight = Math.max(280, rect.height || 560);\n canvas.width = Math.round(cssWidth * ratio);\n canvas.height = Math.round(cssHeight * ratio);\n let view = fitView(points, cssWidth, cssHeight);\n const theme = { current: graphTheme(shell) };\n let focusNeighbors = new Set;\n const neighborsOf = (index) => {\n const neighbors = new Set;\n for (const edge of drawn) {\n if (edge.s === index)\n neighbors.add(edge.t);\n if (edge.t === index)\n neighbors.add(edge.s);\n }\n return neighbors;\n };\n const draw = () => {\n context.setTransform(1, 0, 0, 1, 0, 0);\n context.clearRect(0, 0, canvas.width, canvas.height);\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n context.fillStyle = theme.current.bg;\n context.fillRect(0, 0, cssWidth, cssHeight);\n drawGraph(context, ratio, theme.current, view, points, titles, degrees, drawn, focusNeighbors);\n };\n let scheduled = false;\n const redraw = () => {\n if (dom.requestAnimationFrame === undefined) {\n draw();\n return;\n }\n if (scheduled)\n return;\n scheduled = true;\n dom.requestAnimationFrame(() => {\n scheduled = false;\n draw();\n });\n };\n const nodeAt = (clientX, clientY) => {\n const bounds = canvas.getBoundingClientRect();\n const px = clientX - bounds.left;\n const py = clientY - bounds.top;\n let best = -1;\n let bestDistance = 14;\n for (const [index, point] of points.entries()) {\n const sx = point.x * view.scale + view.tx;\n const sy = point.y * view.scale + view.ty;\n const distance = Math.sqrt((sx - px) * (sx - px) + (sy - py) * (sy - py));\n if (distance < bestDistance) {\n bestDistance = distance;\n best = index;\n }\n }\n return best;\n };\n const focusSlug = () => {\n const hash = dom.location.hash ?? \"\";\n const match = /^#n=(.+)$/u.exec(hash);\n if (match === null)\n return;\n let slug = match[1] ?? \"\";\n try {\n slug = decodeURIComponent(slug);\n } catch {\n return;\n }\n const index = slugs.indexOf(slug);\n if (index === -1)\n return;\n view.focus = index;\n focusNeighbors = neighborsOf(index);\n const point = points[index];\n if (point !== undefined) {\n view.tx = cssWidth / 2 - point.x * view.scale;\n view.ty = cssHeight / 2 - point.y * view.scale;\n }\n redraw();\n };\n let dragFrom;\n let dragged = false;\n canvas.addEventListener(\"pointerdown\", (event) => {\n dragFrom = { x: event.clientX ?? 0, y: event.clientY ?? 0 };\n dragged = false;\n canvas.style.cursor = \"grabbing\";\n });\n canvas.addEventListener(\"pointermove\", (event) => {\n const clientX = event.clientX ?? 0;\n const clientY = event.clientY ?? 0;\n if (dragFrom !== undefined) {\n const deltaX = clientX - dragFrom.x;\n const deltaY = clientY - dragFrom.y;\n if (Math.abs(deltaX) + Math.abs(deltaY) > 4)\n dragged = true;\n if (dragged) {\n view.tx += deltaX;\n view.ty += deltaY;\n dragFrom = { x: clientX, y: clientY };\n redraw();\n }\n return;\n }\n const hit = nodeAt(clientX, clientY);\n if (hit !== view.hover) {\n view.hover = hit;\n canvas.style.cursor = hit >= 0 ? \"pointer\" : \"grab\";\n redraw();\n }\n });\n canvas.addEventListener(\"pointerup\", (event) => {\n canvas.style.cursor = \"grab\";\n const wasDrag = dragged;\n dragFrom = undefined;\n dragged = false;\n if (wasDrag)\n return;\n const hit = nodeAt(event.clientX ?? 0, event.clientY ?? 0);\n if (hit >= 0)\n dom.location.assign(hrefFor(hit));\n });\n canvas.addEventListener(\"pointerleave\", () => {\n dragFrom = undefined;\n dragged = false;\n if (view.hover !== -1) {\n view.hover = -1;\n redraw();\n }\n });\n canvas.addEventListener(\"wheel\", (event) => {\n event.preventDefault();\n const bounds = canvas.getBoundingClientRect();\n const px = (event.clientX ?? 0) - bounds.left;\n const py = (event.clientY ?? 0) - bounds.top;\n const factor = Math.exp(-(event.deltaY ?? 0) * 0.0015);\n const next = Math.min(GRAPH_MAX_SCALE, Math.max(GRAPH_MIN_SCALE, view.scale * factor));\n const applied = next / view.scale;\n view.tx = px - (px - view.tx) * applied;\n view.ty = py - (py - view.ty) * applied;\n view.scale = next;\n redraw();\n });\n canvas.addEventListener(\"keydown\", (event) => {\n const panStep = 48;\n if (event.key === \"ArrowLeft\")\n view.tx += panStep;\n else if (event.key === \"ArrowRight\")\n view.tx -= panStep;\n else if (event.key === \"ArrowUp\")\n view.ty += panStep;\n else if (event.key === \"ArrowDown\")\n view.ty -= panStep;\n else if (event.key === \"+\" || event.key === \"=\") {\n view.scale = Math.min(GRAPH_MAX_SCALE, view.scale * 1.25);\n } else if (event.key === \"-\" || event.key === \"_\") {\n view.scale = Math.max(GRAPH_MIN_SCALE, view.scale / 1.25);\n } else if (event.key === \"0\" || event.key === \"Escape\") {\n view = fitView(points, cssWidth, cssHeight);\n } else if (event.key === \"Enter\") {\n const target = view.hover >= 0 ? view.hover : view.focus;\n if (target < 0)\n return;\n dom.location.assign(hrefFor(target));\n } else {\n return;\n }\n event.preventDefault();\n redraw();\n });\n reset?.addEventListener(\"click\", () => {\n view = fitView(points, cssWidth, cssHeight);\n redraw();\n });\n const repaintTheme = () => {\n theme.current = graphTheme(shell);\n redraw();\n };\n wordcellAppearanceBridge()?.subscribe(repaintTheme);\n dom.matchMedia?.(\"(prefers-color-scheme: dark)\").addEventListener(\"change\", repaintTheme);\n if (status !== null)\n status.hidden = true;\n dom.addEventListener?.(\"hashchange\", focusSlug);\n focusSlug();\n draw();\n })().catch(() => {\n if (status !== null) {\n status.hidden = false;\n status.textContent = \"The graph data could not be loaded; every note is listed below.\";\n }\n });\n}\nfunction initAppearance() {\n const bridge = wordcellAppearanceBridge();\n const actions = dom.document.querySelector(\".site-actions\");\n if (bridge === undefined || actions === null)\n return;\n const wrap = dom.document.createElement(\"div\");\n wrap.className = \"appearance\";\n const trigger = dom.document.createElement(\"button\");\n trigger.type = \"button\";\n trigger.className = \"appearance-trigger\";\n trigger.setAttribute(\"aria-haspopup\", \"dialog\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n trigger.setAttribute(\"aria-label\", \"Appearance\");\n trigger.innerHTML = '' + '' + '';\n const menu = dom.document.createElement(\"div\");\n menu.className = \"appearance-menu\";\n menu.hidden = true;\n menu.setAttribute(\"role\", \"dialog\");\n menu.setAttribute(\"aria-label\", \"Appearance\");\n const paletteInputs = new Map;\n const modeInputs = new Map;\n const modeGroup = dom.document.createElement(\"div\");\n modeGroup.className = \"appearance-group\";\n const modeLabel = dom.document.createElement(\"div\");\n modeLabel.className = \"appearance-label\";\n modeLabel.textContent = \"Appearance\";\n modeGroup.appendChild(modeLabel);\n for (const mode of wordcellAppearanceModes) {\n const label = dom.document.createElement(\"label\");\n label.className = \"appearance-option\";\n const input = dom.document.createElement(\"input\");\n input.type = \"radio\";\n input.name = \"wordcell-appearance-mode\";\n input.value = mode;\n input.addEventListener(\"change\", () => {\n bridge.set({ palette: bridge.get().palette, mode });\n });\n modeInputs.set(mode, input);\n const text = dom.document.createElement(\"span\");\n text.textContent = wordcellAppearanceModeLabels[mode];\n label.appendChild(input);\n label.appendChild(text);\n modeGroup.appendChild(label);\n }\n const paletteGroup = dom.document.createElement(\"div\");\n paletteGroup.className = \"appearance-group\";\n const paletteLabel = dom.document.createElement(\"div\");\n paletteLabel.className = \"appearance-label\";\n paletteLabel.textContent = \"Palette\";\n paletteGroup.appendChild(paletteLabel);\n for (const palette of wordcellPalettes) {\n const label = dom.document.createElement(\"label\");\n label.className = \"appearance-option\";\n const input = dom.document.createElement(\"input\");\n input.type = \"radio\";\n input.name = \"wordcell-appearance-palette\";\n input.value = palette;\n input.addEventListener(\"change\", () => {\n bridge.set({ palette, mode: bridge.get().mode });\n });\n paletteInputs.set(palette, input);\n const swatch = dom.document.createElement(\"span\");\n swatch.className = `swatch swatch-${palette}`;\n const text = dom.document.createElement(\"span\");\n text.textContent = wordcellPaletteLabels[palette];\n label.appendChild(input);\n label.appendChild(swatch);\n label.appendChild(text);\n paletteGroup.appendChild(label);\n }\n menu.appendChild(modeGroup);\n menu.appendChild(paletteGroup);\n wrap.appendChild(trigger);\n wrap.appendChild(menu);\n actions.appendChild(wrap);\n const sync = () => {\n const preference = bridge.get();\n for (const [mode, input] of modeInputs)\n input.checked = mode === preference.mode;\n for (const [palette, input] of paletteInputs)\n input.checked = palette === preference.palette;\n };\n const close = () => {\n menu.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n };\n const open = () => {\n sync();\n menu.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n paletteInputs.get(bridge.get().palette)?.focus();\n };\n trigger.addEventListener(\"click\", () => {\n if (menu.hidden)\n open();\n else\n close();\n });\n dom.document.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Escape\" && !menu.hidden) {\n close();\n trigger.focus();\n }\n });\n dom.document.addEventListener(\"click\", (event) => {\n if (!menu.hidden && event.target?.closest?.(\".appearance\") == null)\n close();\n });\n bridge.subscribe(sync);\n sync();\n}\nfunction start() {\n const content = dom.document.querySelector('meta[name=\"wordcell:base\"]')?.getAttribute(\"content\") ?? \"./\";\n const base = content.endsWith(\"/\") ? content : `${content}/`;\n initAppearance();\n const overlay = buildOverlay(base);\n for (const button of Array.from(dom.document.querySelectorAll(\"[data-wordcell-search]\"))) {\n button.addEventListener(\"click\", () => overlay.open());\n }\n dom.document.addEventListener(\"keydown\", (event) => {\n if (event.key === \"/\" && !/INPUT|TEXTAREA|SELECT/u.test(event.target?.tagName ?? \"\")) {\n event.preventDefault();\n overlay.open();\n }\n if (event.key === \"Escape\")\n overlay.close();\n });\n initGraph(base);\n}\nif (dom.document.querySelector(\"[data-wordcell-search]\") !== null) {\n start();\n}\n", "reader.css": "/* Wordcell reference reader — zero-dependency, system-font, light/dark.\n *\n * Palette values are the resolved semantic colors of the shared\n * `hraness-design-palette-v1` contract (Catppuccin, Gruvbox, Rosé Pine,\n * Tokyo Night; light + dark), adapted onto the reader's --wordcell-*\n * tokens. `data-palette`/`data-theme` are applied by reader/theme.js\n * before first paint; pages without it fall back to the neutral\n * \"wordcell\" palette via prefers-color-scheme below.\n */\n\n:root {\n --wordcell-bg: #ffffff;\n --wordcell-fg: #1a1a1a;\n --wordcell-muted: #5f6368;\n --wordcell-border: #e1e4e8;\n --wordcell-border-strong: #a8aeb6;\n --wordcell-accent: #0b5bd3;\n --wordcell-accent-soft: #e9f1fc;\n --wordcell-warning: #9a6700;\n --wordcell-code-bg: #f6f8fa;\n --wordcell-mark: #fff3bf;\n --wordcell-overlay: rgba(15, 15, 15, 0.45);\n --wordcell-panel: #ffffff;\n --wordcell-nav-w: 14.5rem;\n --wordcell-toc-w: 11rem;\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme]) {\n --wordcell-bg: #14151a;\n --wordcell-fg: #e8e8e8;\n --wordcell-muted: #9aa0a6;\n --wordcell-border: #2d3138;\n --wordcell-border-strong: #4a505a;\n --wordcell-accent: #7aa5f8;\n --wordcell-accent-soft: #1b2a44;\n --wordcell-warning: #e3b341;\n --wordcell-code-bg: #1d2025;\n --wordcell-mark: #4d4200;\n --wordcell-overlay: rgba(0, 0, 0, 0.6);\n --wordcell-panel: #1d2025;\n }\n}\n\n/* Shared palettes — explicit palette/theme pairs override the media-query\n * defaults above. `border` is the subtle hairline (grid role); interactive\n * boundaries use `border-strong` (control role). */\n\n:root[data-palette=\"wordcell\"][data-theme=\"light\"] {\n --wordcell-bg: #ffffff;\n --wordcell-fg: #1a1a1a;\n --wordcell-muted: #5f6368;\n --wordcell-border: #e1e4e8;\n --wordcell-border-strong: #a8aeb6;\n --wordcell-accent: #0b5bd3;\n --wordcell-accent-soft: #e9f1fc;\n --wordcell-warning: #9a6700;\n --wordcell-code-bg: #f6f8fa;\n --wordcell-mark: #fff3bf;\n --wordcell-overlay: rgba(15, 15, 15, 0.45);\n --wordcell-panel: #ffffff;\n}\n\n:root[data-palette=\"wordcell\"][data-theme=\"dark\"] {\n --wordcell-bg: #14151a;\n --wordcell-fg: #e8e8e8;\n --wordcell-muted: #9aa0a6;\n --wordcell-border: #2d3138;\n --wordcell-border-strong: #4a505a;\n --wordcell-accent: #7aa5f8;\n --wordcell-accent-soft: #1b2a44;\n --wordcell-warning: #e3b341;\n --wordcell-code-bg: #1d2025;\n --wordcell-mark: #4d4200;\n --wordcell-overlay: rgba(0, 0, 0, 0.6);\n --wordcell-panel: #1d2025;\n}\n\n:root[data-palette=\"catppuccin\"][data-theme=\"light\"] {\n --wordcell-bg: #eff1f5;\n --wordcell-fg: #3a3c50;\n --wordcell-muted: #545768;\n --wordcell-border: #dce0e8;\n --wordcell-border-strong: #6e7183;\n --wordcell-accent: #1750bf;\n --wordcell-accent-soft: #d6e0f5;\n --wordcell-warning: #7b4e10;\n --wordcell-code-bg: #e6e9ef;\n --wordcell-mark: #ede5db;\n --wordcell-overlay: #00000070;\n --wordcell-panel: #dce0e8;\n}\n\n:root[data-palette=\"catppuccin\"][data-theme=\"dark\"] {\n --wordcell-bg: #1e1e2e;\n --wordcell-fg: #dbe1f7;\n --wordcell-muted: #b2b8cf;\n --wordcell-border: #313244;\n --wordcell-border-strong: #9296ab;\n --wordcell-accent: #92bafa;\n --wordcell-accent-soft: #2b3046;\n --wordcell-warning: #f9e2af;\n --wordcell-code-bg: #181825;\n --wordcell-mark: #38363d;\n --wordcell-overlay: #000000b8;\n --wordcell-panel: #313244;\n}\n\n:root[data-palette=\"gruvbox\"][data-theme=\"light\"] {\n --wordcell-bg: #fbf1c7;\n --wordcell-fg: #393533;\n --wordcell-muted: #584f48;\n --wordcell-border: #ebdbb2;\n --wordcell-border-strong: #75685e;\n --wordcell-accent: #065968;\n --wordcell-accent-soft: #dee0be;\n --wordcell-warning: #70490c;\n --wordcell-code-bg: #f9f5d7;\n --wordcell-mark: #f3e2b2;\n --wordcell-overlay: #00000070;\n --wordcell-panel: #ebdbb2;\n}\n\n:root[data-palette=\"gruvbox\"][data-theme=\"dark\"] {\n --wordcell-bg: #282828;\n --wordcell-fg: #f0e5c7;\n --wordcell-muted: #c7baa3;\n --wordcell-border: #3c3836;\n --wordcell-border-strong: #a5988c;\n --wordcell-accent: #a9c1b8;\n --wordcell-accent-soft: #333735;\n --wordcell-warning: #fabd2f;\n --wordcell-code-bg: #1d2021;\n --wordcell-mark: #413a29;\n --wordcell-overlay: #000000b8;\n --wordcell-panel: #3c3836;\n}\n\n:root[data-palette=\"rose-pine\"][data-theme=\"light\"] {\n --wordcell-bg: #faf4ed;\n --wordcell-fg: #44405e;\n --wordcell-muted: #605c74;\n --wordcell-border: #f2e9e1;\n --wordcell-border-strong: #797593;\n --wordcell-accent: #68587a;\n --wordcell-accent-soft: #ede5e5;\n --wordcell-warning: #81561d;\n --wordcell-code-bg: #fffaf3;\n --wordcell-mark: #f8ead7;\n --wordcell-overlay: #00000070;\n --wordcell-panel: #f2e9e1;\n}\n\n:root[data-palette=\"rose-pine\"][data-theme=\"dark\"] {\n --wordcell-bg: #191724;\n --wordcell-fg: #e0def4;\n --wordcell-muted: #adaac0;\n --wordcell-border: #26233a;\n --wordcell-border-strong: #908caa;\n --wordcell-accent: #c4a7e7;\n --wordcell-accent-soft: #2e283b;\n --wordcell-warning: #f6c177;\n --wordcell-code-bg: #1f1d2e;\n --wordcell-mark: #342b2e;\n --wordcell-overlay: #000000b8;\n --wordcell-panel: #26233a;\n}\n\n:root[data-palette=\"tokyo-night\"][data-theme=\"light\"] {\n --wordcell-bg: #e1e2e7;\n --wordcell-fg: #1c3161;\n --wordcell-muted: #414c76;\n --wordcell-border: #c4c8da;\n --wordcell-border-strong: #56659d;\n --wordcell-accent: #1d4e90;\n --wordcell-accent-soft: #ccd6e7;\n --wordcell-warning: #614b2b;\n --wordcell-code-bg: #d0d5e3;\n --wordcell-mark: #d7d4d3;\n --wordcell-overlay: #00000070;\n --wordcell-panel: #c4c8da;\n}\n\n:root[data-palette=\"tokyo-night\"][data-theme=\"dark\"] {\n --wordcell-bg: #1a1b26;\n --wordcell-fg: #c0caf5;\n --wordcell-muted: #a9b1d6;\n --wordcell-border: #24283b;\n --wordcell-border-strong: #737aa2;\n --wordcell-accent: #7aa2f7;\n --wordcell-accent-soft: #262b3f;\n --wordcell-warning: #e0af68;\n --wordcell-code-bg: #16161e;\n --wordcell-mark: #322d2e;\n --wordcell-overlay: #000000b8;\n --wordcell-panel: #24283b;\n}\n\nhtml {\n color-scheme: light dark;\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n}\n\n:root[data-theme=\"dark\"] {\n color-scheme: dark;\n}\n\nbody {\n margin: 0;\n background: var(--wordcell-bg);\n color: var(--wordcell-fg);\n font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n}\n\n.skip-link {\n position: absolute;\n left: -9999px;\n top: 0;\n padding: 0.4rem 0.8rem;\n background: var(--wordcell-panel);\n color: var(--wordcell-accent);\n border: 1px solid var(--wordcell-border);\n border-radius: 0 0 0.5rem 0;\n z-index: 60;\n}\n\n.skip-link:focus {\n left: 0;\n}\n\n.site-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n max-width: 76rem;\n margin: 0 auto;\n padding: 0.65rem 1.25rem;\n border-bottom: 1px solid var(--wordcell-border);\n}\n\n.site-title {\n color: var(--wordcell-fg);\n font-weight: 650;\n font-size: 0.95rem;\n text-decoration: none;\n}\n\n.site-actions {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\n\n.graph-link {\n color: var(--wordcell-muted);\n text-decoration: none;\n font-size: 0.9rem;\n padding: 0.3rem 0.6rem;\n border-radius: 0.5rem;\n}\n\n.graph-link:hover {\n color: var(--wordcell-fg);\n background: var(--wordcell-code-bg);\n}\n\n.graph-link[aria-current=\"page\"] {\n color: var(--wordcell-accent);\n font-weight: 600;\n}\n\n.search-button {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.3rem 0.7rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 0.5rem;\n background: transparent;\n color: var(--wordcell-muted);\n font: inherit;\n font-size: 0.9rem;\n cursor: pointer;\n}\n\n.search-button:hover {\n color: var(--wordcell-fg);\n background: var(--wordcell-code-bg);\n}\n\n.search-button kbd {\n padding: 0 0.35rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 0.25rem;\n font: 0.75em ui-monospace, monospace;\n}\n\n/* Appearance menu — the single icon control at the rightmost header\n * action, injected by the reader bundle (absent without JavaScript). */\n\n.appearance {\n position: relative;\n}\n\n.appearance-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.9rem;\n height: 1.9rem;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 0.5rem;\n background: transparent;\n color: var(--wordcell-muted);\n cursor: pointer;\n}\n\n.appearance-trigger:hover {\n color: var(--wordcell-fg);\n background: var(--wordcell-code-bg);\n}\n\n.appearance-trigger[aria-expanded=\"true\"] {\n color: var(--wordcell-accent);\n background: var(--wordcell-accent-soft);\n}\n\n.appearance-menu {\n position: absolute;\n top: calc(100% + 0.4rem);\n right: 0;\n z-index: 40;\n min-width: 11.5rem;\n margin: 0;\n padding: 0.45rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 0.65rem;\n background: var(--wordcell-panel);\n box-shadow: 0 10px 32px rgba(0, 0, 0, 0.18);\n font-size: 0.85rem;\n}\n\n.appearance-menu[hidden] {\n display: none;\n}\n\n.appearance-group {\n padding: 0.15rem 0;\n}\n\n.appearance-group + .appearance-group {\n border-top: 1px solid var(--wordcell-border);\n margin-top: 0.3rem;\n padding-top: 0.45rem;\n}\n\n.appearance-label {\n padding: 0.1rem 0.55rem 0.3rem;\n font-size: 0.7rem;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--wordcell-muted);\n}\n\n.appearance-option {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n padding: 0.3rem 0.55rem;\n border-radius: 0.4rem;\n color: var(--wordcell-fg);\n cursor: pointer;\n line-height: 1.35;\n}\n\n.appearance-option:hover {\n background: var(--wordcell-code-bg);\n}\n\n.appearance-option input {\n appearance: none;\n width: 0.85rem;\n height: 0.85rem;\n margin: 0;\n flex: none;\n border: 1.5px solid var(--wordcell-border-strong);\n border-radius: 50%;\n background: transparent;\n}\n\n.appearance-option input:checked {\n border-color: var(--wordcell-accent);\n background: var(--wordcell-accent);\n box-shadow: inset 0 0 0 2.5px var(--wordcell-panel);\n}\n\n.appearance-option input:focus-visible {\n outline: 2px solid var(--wordcell-accent);\n outline-offset: 2px;\n}\n\n.appearance-option .swatch {\n width: 0.85rem;\n height: 0.85rem;\n flex: none;\n border-radius: 50%;\n border: 1px solid var(--wordcell-border-strong);\n}\n\n/* Swatches show each palette's light/dark page backgrounds, split. */\n.swatch-wordcell { background: linear-gradient(135deg, #ffffff 50%, #14151a 50%); }\n.swatch-catppuccin { background: linear-gradient(135deg, #eff1f5 50%, #1e1e2e 50%); }\n.swatch-gruvbox { background: linear-gradient(135deg, #fbf1c7 50%, #282828 50%); }\n.swatch-rose-pine { background: linear-gradient(135deg, #faf4ed 50%, #191724 50%); }\n.swatch-tokyo-night { background: linear-gradient(135deg, #e1e2e7 50%, #1a1b26 50%); }\n\na:focus-visible,\nbutton:focus-visible,\nsummary:focus-visible {\n outline: 2px solid var(--wordcell-accent);\n outline-offset: 2px;\n border-radius: 0.3rem;\n}\n\n::selection {\n background: var(--wordcell-accent-soft);\n}\n\n.site-layout {\n display: grid;\n grid-template-columns: var(--wordcell-nav-w) minmax(0, 1fr);\n gap: 0 2rem;\n max-width: 76rem;\n margin: 0 auto;\n padding: 0 1.25rem;\n align-items: start;\n}\n\n.site-layout-flat {\n grid-template-columns: minmax(0, 1fr);\n}\n\n/* Sidebar tree */\n\n.site-nav {\n position: sticky;\n top: 0;\n max-height: 100vh;\n overflow-y: auto;\n padding: 1.25rem 0.5rem 2rem 0;\n font-size: 0.9rem;\n scrollbar-width: thin;\n}\n\n.site-tree,\n.site-tree ul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.site-tree ul {\n padding-left: 0.9rem;\n border-left: 1px solid var(--wordcell-border);\n margin-left: 0.55rem;\n}\n\n.site-tree-leaf,\n.site-tree-self,\n.site-tree-more {\n margin: 0.1rem 0;\n}\n\n.site-tree-link {\n display: block;\n padding: 0.15rem 0.5rem;\n border-radius: 0.4rem;\n color: var(--wordcell-fg);\n text-decoration: none;\n line-height: 1.4;\n}\n\n.site-tree-link:hover {\n background: var(--wordcell-code-bg);\n}\n\n.site-tree-link.site-tree-current {\n color: var(--wordcell-accent);\n font-weight: 600;\n background: var(--wordcell-accent-soft);\n}\n\n.site-tree-empty {\n display: block;\n padding: 0.15rem 0.5rem;\n color: var(--wordcell-muted);\n}\n\n.site-tree-group > details > summary {\n cursor: pointer;\n padding: 0.15rem 0.5rem;\n border-radius: 0.4rem;\n color: var(--wordcell-muted);\n font-weight: 550;\n line-height: 1.4;\n list-style-position: outside;\n}\n\n.site-tree-group > details > summary:hover {\n background: var(--wordcell-code-bg);\n color: var(--wordcell-fg);\n}\n\n.site-tree-self > .site-tree-link {\n font-style: italic;\n}\n\n.site-tree-more a {\n display: block;\n padding: 0.15rem 0.5rem;\n color: var(--wordcell-muted);\n font-size: 0.82rem;\n text-decoration: none;\n}\n\n.site-tree-more a:hover {\n color: var(--wordcell-accent);\n}\n\n/* Main column */\n\nmain {\n min-width: 0;\n padding: 1.5rem 0 4rem;\n}\n\n.breadcrumbs {\n font-size: 0.82rem;\n color: var(--wordcell-muted);\n margin: 0 0 1rem;\n}\n\n.breadcrumbs ol {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.15rem;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.breadcrumbs li + li::before {\n content: \"/\";\n margin: 0 0.45rem;\n color: var(--wordcell-border);\n}\n\n.breadcrumbs a {\n color: var(--wordcell-muted);\n text-decoration: none;\n}\n\n.breadcrumbs a:hover {\n color: var(--wordcell-accent);\n text-decoration: underline;\n}\n\n.breadcrumbs [aria-current=\"page\"] {\n color: var(--wordcell-fg);\n font-weight: 550;\n}\n\n.note-columns {\n display: grid;\n grid-template-columns: minmax(0, 1fr) var(--wordcell-toc-w);\n gap: 1.5rem;\n align-items: start;\n}\n\n.note-toc {\n position: sticky;\n top: 1.25rem;\n font-size: 0.82rem;\n border-left: 1px solid var(--wordcell-border);\n padding-left: 1rem;\n max-height: calc(100vh - 2.5rem);\n overflow-y: auto;\n}\n\n.note-toc h2 {\n margin: 0 0 0.5rem;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--wordcell-muted);\n}\n\n.note-toc ol {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.note-toc li {\n margin: 0.3rem 0;\n}\n\n.note-toc a {\n color: var(--wordcell-muted);\n text-decoration: none;\n display: block;\n line-height: 1.35;\n}\n\n.note-toc a:hover {\n color: var(--wordcell-accent);\n}\n\n.note-toc .toc-level-2 { padding-left: 0.8rem; }\n.note-toc .toc-level-3,\n.note-toc .toc-level-4 { padding-left: 1.6rem; }\n\n.note {\n max-width: 42rem;\n}\n\n.note h1 {\n margin: 0 0 0.5rem;\n font-size: 1.8rem;\n line-height: 1.25;\n letter-spacing: -0.01em;\n text-wrap: balance;\n}\n\n.note-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n margin: 0.25rem 0 1rem;\n}\n\n.note-type,\n.note-tag {\n padding: 0.08rem 0.55rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 999px;\n color: var(--wordcell-muted);\n font-size: 0.78rem;\n}\n\n.note-aliases {\n color: var(--wordcell-muted);\n font-size: 0.9rem;\n margin: 0 0 1rem;\n}\n\n.note-body {\n line-height: 1.7;\n}\n\n.note-body p,\n.note-body li {\n text-wrap: pretty;\n}\n\n.note-body h2,\n.note-body h3,\n.note-body h4 {\n text-wrap: balance;\n letter-spacing: -0.005em;\n}\n\n.note-body a {\n color: var(--wordcell-accent);\n text-decoration-thickness: 0.06em;\n text-underline-offset: 0.14em;\n}\n\n.note-body hr {\n border: 0;\n border-top: 1px solid var(--wordcell-border);\n margin: 2rem 0;\n}\n\n.note-body img {\n max-width: 100%;\n height: auto;\n}\n\n.note-body pre {\n overflow-x: auto;\n padding: 0.9rem 1rem;\n border-radius: 0.5rem;\n background: var(--wordcell-code-bg);\n}\n\n.note-body code {\n font: 0.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n\n.note-body p code,\n.note-body li code,\n.note-body td code {\n padding: 0.1rem 0.3rem;\n border-radius: 0.3rem;\n background: var(--wordcell-code-bg);\n}\n\n.note-body blockquote {\n margin: 1rem 0;\n padding-left: 1rem;\n border-left: 3px solid var(--wordcell-border);\n color: var(--wordcell-muted);\n}\n\n.note-body table {\n border-collapse: collapse;\n width: 100%;\n margin: 1rem 0;\n}\n\n.note-body th,\n.note-body td {\n padding: 0.45rem 0.7rem;\n border: 1px solid var(--wordcell-border);\n text-align: left;\n}\n\n.note-body mark,\n.wordcell-search-results mark {\n background: var(--wordcell-mark);\n color: inherit;\n border-radius: 0.15rem;\n padding: 0 0.05rem;\n}\n\n.note-body .unresolved {\n color: var(--wordcell-muted);\n border-bottom: 1px dashed var(--wordcell-border);\n}\n\n.note-body input[type=\"checkbox\"] {\n margin-right: 0.35rem;\n}\n\n.note-aside {\n margin-top: 2.5rem;\n padding-top: 1rem;\n border-top: 1px solid var(--wordcell-border);\n display: grid;\n gap: 1.25rem;\n}\n\n.note-aside h2 {\n margin: 0 0 0.5rem;\n font-size: 0.95rem;\n color: var(--wordcell-muted);\n font-weight: 600;\n text-transform: none;\n}\n\n.note-aside ul {\n margin: 0;\n padding-left: 1.2rem;\n}\n\n.note-aside a {\n color: var(--wordcell-accent);\n text-decoration: none;\n}\n\n.note-aside .predicate {\n color: var(--wordcell-muted);\n font-size: 0.85rem;\n}\n\n.catalog-group h2 {\n margin: 1.5rem 0 0.5rem;\n font-size: 1.05rem;\n color: var(--wordcell-muted);\n font-weight: 600;\n}\n\n.catalog-group ul {\n margin: 0;\n padding-left: 1.2rem;\n list-style: none;\n}\n\n.catalog-group a {\n color: var(--wordcell-accent);\n text-decoration: none;\n}\n\n.catalog-group a:hover {\n text-decoration: underline;\n}\n\n/* Graph page */\n\n.graph-lede {\n color: var(--wordcell-muted);\n font-size: 0.9rem;\n}\n\n.graph-shell {\n position: relative;\n border: 1px solid var(--wordcell-border);\n border-radius: 0.75rem;\n overflow: hidden;\n background: var(--wordcell-code-bg);\n}\n\n.graph-canvas {\n display: block;\n width: 100%;\n height: 62vh;\n min-height: 22rem;\n cursor: grab;\n touch-action: none;\n outline: none;\n}\n\n.graph-canvas:focus-visible {\n box-shadow: inset 0 0 0 2px var(--wordcell-accent);\n}\n\n.graph-status {\n position: absolute;\n inset-inline: 0;\n top: 0.75rem;\n margin: 0 auto;\n width: fit-content;\n max-width: 90%;\n padding: 0.4rem 0.9rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 999px;\n background: var(--wordcell-panel);\n color: var(--wordcell-muted);\n font-size: 0.8rem;\n text-align: center;\n}\n\n.graph-toolbar {\n position: absolute;\n inset-inline: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n background: color-mix(in srgb, var(--wordcell-panel) 82%, transparent);\n border-top: 1px solid var(--wordcell-border);\n font-size: 0.78rem;\n color: var(--wordcell-muted);\n}\n\n.graph-reset {\n border: 1px solid var(--wordcell-border-strong);\n border-radius: 0.45rem;\n background: var(--wordcell-panel);\n color: var(--wordcell-fg);\n font: inherit;\n font-size: 0.78rem;\n padding: 0.25rem 0.7rem;\n cursor: pointer;\n}\n\n.graph-reset:hover {\n background: var(--wordcell-code-bg);\n}\n\n.graph-legend {\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n}\n\n.graph-edge {\n display: inline-block;\n width: 1.4rem;\n border-top: 2px solid var(--wordcell-border);\n}\n\n.graph-edge-relation {\n border-top-style: dashed;\n}\n\n.graph-index {\n columns: 2;\n list-style: none;\n padding: 0;\n margin: 0.5rem 0 0;\n}\n\n.graph-index li {\n break-inside: avoid;\n margin: 0.15rem 0;\n}\n\n.graph-index a {\n color: var(--wordcell-accent);\n text-decoration: none;\n}\n\n.graph-index a:hover {\n text-decoration: underline;\n}\n\n.site-footer {\n max-width: 76rem;\n margin: 0 auto;\n padding: 1rem 1.25rem 2rem;\n border-top: 1px solid var(--wordcell-border);\n color: var(--wordcell-muted);\n font-size: 0.85rem;\n}\n\n.site-footer a {\n color: inherit;\n}\n\n/* Search overlay */\n\n.wordcell-search-overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 12vh 1rem 1rem;\n background: var(--wordcell-overlay);\n}\n\n.wordcell-search-panel {\n width: 100%;\n max-width: 36rem;\n border: 1px solid var(--wordcell-border);\n border-radius: 0.75rem;\n background: var(--wordcell-panel);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);\n overflow: hidden;\n}\n\n.wordcell-search-panel input {\n width: 100%;\n box-sizing: border-box;\n padding: 0.9rem 1rem;\n border: 0;\n border-bottom: 1px solid var(--wordcell-border);\n background: transparent;\n color: var(--wordcell-fg);\n font: inherit;\n outline: none;\n}\n\n.wordcell-search-status {\n padding: 0.35rem 1rem;\n color: var(--wordcell-muted);\n font-size: 0.8rem;\n}\n\n.wordcell-search-results {\n margin: 0;\n padding: 0.25rem;\n list-style: none;\n max-height: 55vh;\n overflow-y: auto;\n}\n\n.wordcell-search-results a {\n display: block;\n padding: 0.55rem 0.75rem;\n border-radius: 0.5rem;\n color: var(--wordcell-fg);\n text-decoration: none;\n}\n\n.wordcell-search-results a.active,\n.wordcell-search-results a:hover {\n background: var(--wordcell-accent-soft);\n}\n\n.wordcell-search-results .title {\n display: block;\n font-weight: 600;\n}\n\n.wordcell-search-results .snippet {\n display: block;\n color: var(--wordcell-muted);\n font-size: 0.85rem;\n}\n\n@media (max-width: 68rem) {\n .note-columns {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .note-toc {\n display: none;\n }\n}\n\n@media (max-width: 56rem) {\n .site-layout {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .site-nav {\n position: static;\n max-height: 18rem;\n border-bottom: 1px solid var(--wordcell-border);\n margin-bottom: 0.5rem;\n padding: 0.75rem 0;\n }\n\n .graph-index {\n columns: 1;\n }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .wordcell-search-overlay {\n animation: wordcell-fade-in 120ms ease-out;\n }\n\n @keyframes wordcell-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n}\n", diff --git a/site/package.json b/site/package.json index 84c8d0a..91d2251 100644 --- a/site/package.json +++ b/site/package.json @@ -28,7 +28,7 @@ "@hraness/site-footer": "github:hraness/site-footer#v0.19.2", "@hraness/ui": "github:hraness/ui#v0.5.18", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", - "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.22.4/hraness-wordcell-0.22.4.tgz", + "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.23.0/hraness-wordcell-0.23.0.tgz", "next": "16.3.3", "react": "19.2.6", "react-dom": "19.2.6"