From 6bb9a127a051848c7af914bfa302dab59e12b4fe Mon Sep 17 00:00:00 2001
From: Alexey
Date: Thu, 23 Apr 2026 23:34:39 +0500
Subject: [PATCH] mantra v0.1: book-grade reader for design-dna corpus + margin
drawer (notes + claude-ask)
---
.gitignore | 4 +
Cargo.lock | 3529 +++++++++++++++++++
Cargo.toml | 58 +
crates/mantra-server/Cargo.toml | 28 +
crates/mantra-server/src/corpus_loader.rs | 331 ++
crates/mantra-server/src/main.rs | 119 +
crates/mantra-ui/Cargo.toml | 51 +
crates/mantra-ui/src/api/mod.rs | 460 +++
crates/mantra-ui/src/api/types.rs | 46 +
crates/mantra-ui/src/app.rs | 47 +
crates/mantra-ui/src/corpus.rs | 125 +
crates/mantra-ui/src/lib.rs | 22 +
crates/mantra-ui/src/pages/landing.rs | 146 +
crates/mantra-ui/src/pages/margin.rs | 355 ++
crates/mantra-ui/src/pages/mod.rs | 5 +
crates/mantra-ui/src/pages/shared.rs | 37 +
crates/mantra-ui/src/pages/source.rs | 194 +
crates/mantra-ui/src/pages/theme.rs | 117 +
sass/main.scss | 756 ++++
static/fonts/fraunces-italic-variable.woff2 | Bin 0 -> 45656 bytes
static/fonts/fraunces-variable.woff2 | Bin 0 -> 36620 bytes
static/fonts/plex-mono-variable.woff2 | Bin 0 -> 14708 bytes
static/fonts/plex-sans-variable.woff2 | Bin 0 -> 45712 bytes
23 files changed, 6430 insertions(+)
create mode 100644 .gitignore
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 crates/mantra-server/Cargo.toml
create mode 100644 crates/mantra-server/src/corpus_loader.rs
create mode 100644 crates/mantra-server/src/main.rs
create mode 100644 crates/mantra-ui/Cargo.toml
create mode 100644 crates/mantra-ui/src/api/mod.rs
create mode 100644 crates/mantra-ui/src/api/types.rs
create mode 100644 crates/mantra-ui/src/app.rs
create mode 100644 crates/mantra-ui/src/corpus.rs
create mode 100644 crates/mantra-ui/src/lib.rs
create mode 100644 crates/mantra-ui/src/pages/landing.rs
create mode 100644 crates/mantra-ui/src/pages/margin.rs
create mode 100644 crates/mantra-ui/src/pages/mod.rs
create mode 100644 crates/mantra-ui/src/pages/shared.rs
create mode 100644 crates/mantra-ui/src/pages/source.rs
create mode 100644 crates/mantra-ui/src/pages/theme.rs
create mode 100644 sass/main.scss
create mode 100644 static/fonts/fraunces-italic-variable.woff2
create mode 100644 static/fonts/fraunces-variable.woff2
create mode 100644 static/fonts/plex-mono-variable.woff2
create mode 100644 static/fonts/plex-sans-variable.woff2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..11b99a8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/target
+/content
+.DS_Store
+.env
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..20b9281
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3529 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "any_spawner"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
+dependencies = [
+ "futures",
+ "thiserror 2.0.18",
+ "tokio",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-once-cell"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "attribute-derive"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
+dependencies = [
+ "attribute-derive-macro",
+ "derive-where",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "base64",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "multer",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base16"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+
+[[package]]
+name = "cc"
+version = "1.2.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "codee"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "collection_literals"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config"
+version = "0.15.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
+dependencies = [
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde_core",
+ "toml 1.1.2+spec-1.1.0",
+ "winnow",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "const-str"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
+
+[[package]]
+name = "const_format"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
+dependencies = [
+ "const_format_proc_macros",
+ "konst",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "const_str_slice_concat"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "convert_case_extras"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb"
+dependencies = [
+ "convert_case 0.11.0",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+
+[[package]]
+name = "derive-where"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "drain_filter_polyfill"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "either_of"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd"
+dependencies = [
+ "paste",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gray_matter"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8666976c40b8633f918783969b6681a3ddb205f29150348617de425d85a3e3bd"
+dependencies = [
+ "serde",
+ "serde_json",
+ "toml 0.5.11",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "guardian"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hydration_context"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283"
+dependencies = [
+ "futures",
+ "js-sys",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
+[[package]]
+name = "inventory"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "konst"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
+dependencies = [
+ "konst_macro_rules",
+]
+
+[[package]]
+name = "konst_macro_rules"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "leptos"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efa3982e7fe36c1de68f91f3c9083124f389a975523881f3d7e3363362feda41"
+dependencies = [
+ "any_spawner",
+ "base64",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "getrandom 0.4.2",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "rand",
+ "reactive_graph",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.18",
+ "throw_error",
+ "typed-builder",
+ "typed-builder-macro",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm_split_helpers",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ac7734eed700b0170dffbfc93b03491ed1f306622d79625323a21ed0eedac0"
+dependencies = [
+ "any_spawner",
+ "axum",
+ "futures",
+ "hydration_context",
+ "leptos",
+ "leptos_integration_utils",
+ "leptos_macro",
+ "leptos_meta",
+ "leptos_router",
+ "or_poisoned",
+ "server_fn",
+ "tachys",
+ "tokio",
+ "tower",
+ "tower-http",
+]
+
+[[package]]
+name = "leptos_config"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c06f751315bccc0d193fab302ac01d25bcfcd97474d4676440e7e3250dc3fc3"
+dependencies = [
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.18",
+ "typed-builder",
+]
+
+[[package]]
+name = "leptos_dom"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884"
+dependencies = [
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_hot_reload"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1"
+dependencies = [
+ "anyhow",
+ "camino",
+ "indexmap",
+ "or_poisoned",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "leptos_integration_utils"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c097f89cd9aa606297672f56fa5bdda09f01609a9f4eefaccdbb5ab5afea4279"
+dependencies = [
+ "futures",
+ "hydration_context",
+ "leptos",
+ "leptos_config",
+ "leptos_meta",
+ "leptos_router",
+ "reactive_graph",
+]
+
+[[package]]
+name = "leptos_macro"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9360df573fb57582384a8b7640a3de94ce6501d49be3b69f637cf11a42da484b"
+dependencies = [
+ "attribute-derive",
+ "cfg-if",
+ "convert_case 0.11.0",
+ "convert_case_extras",
+ "html-escape",
+ "itertools",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "rustc_version",
+ "server_fn_macro",
+ "syn",
+ "uuid",
+]
+
+[[package]]
+name = "leptos_meta"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c3efe657b4c55ed2e078922786ffe20acfb71767c3dd913767b09a35c75c890"
+dependencies = [
+ "futures",
+ "indexmap",
+ "leptos",
+ "or_poisoned",
+ "send_wrapper",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_router"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c15158449162e099e2273442f7fd9b924f5cefd935d52af5755ec62aa819fa52"
+dependencies = [
+ "any_spawner",
+ "either_of",
+ "futures",
+ "gloo-net",
+ "js-sys",
+ "leptos",
+ "leptos_router_macro",
+ "or_poisoned",
+ "percent-encoding",
+ "reactive_graph",
+ "rustc_version",
+ "send_wrapper",
+ "tachys",
+ "thiserror 2.0.18",
+ "url",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_router_macro"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "leptos_server"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae"
+dependencies = [
+ "any_spawner",
+ "base64",
+ "codee",
+ "futures",
+ "hydration_context",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "server_fn",
+ "tachys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.185"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "mantra-server"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "gray_matter",
+ "leptos",
+ "leptos_axum",
+ "leptos_meta",
+ "leptos_router",
+ "mantra-ui",
+ "pulldown-cmark",
+ "serde",
+ "tokio",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "mantra-ui"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "console_error_panic_hook",
+ "leptos",
+ "leptos_meta",
+ "leptos_router",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "manyhow"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "manyhow-macros"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
+[[package]]
+name = "next_tuple"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "oco_ref"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
+dependencies = [
+ "serde",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "or_poisoned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project"
+version = "1.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-utils"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "smallvec",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
+dependencies = [
+ "bitflags",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quote-use"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "reactive_graph"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00c5a025366836190c7030e883cc2bcd9e384ff555336e3c7954741ca411b177"
+dependencies = [
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "indexmap",
+ "or_poisoned",
+ "paste",
+ "pin-project-lite",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.18",
+ "web-sys",
+]
+
+[[package]]
+name = "reactive_stores"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c30fd35b7d299c591293bb69fed47a703eb2703b1cff0493e78b16ed007e5382"
+dependencies = [
+ "guardian",
+ "indexmap",
+ "itertools",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+ "send_wrapper",
+]
+
+[[package]]
+name = "reactive_stores_macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d8e790a5ae5ddf9b7fa380c728375b06858e0cca7d063a73b3408320c523e1"
+dependencies = [
+ "convert_case 0.11.0",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rstml"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
+dependencies = [
+ "derive-where",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+ "syn_derive",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "server_fn"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d60e4c1dfccd91fe0990141f69f1d5cf5679797ad53aa1b45e5bd658eb119f0"
+dependencies = [
+ "axum",
+ "base64",
+ "bytes",
+ "const-str",
+ "const_format",
+ "futures",
+ "gloo-net",
+ "http",
+ "http-body-util",
+ "hyper",
+ "inventory",
+ "js-sys",
+ "or_poisoned",
+ "pin-project-lite",
+ "rustc_version",
+ "rustversion",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.18",
+ "throw_error",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f"
+dependencies = [
+ "const_format",
+ "convert_case 0.11.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00"
+dependencies = [
+ "server_fn_macro",
+ "syn",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tachys"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2989c94c59db8497727875aa561d4d0daa3cc79b5774d5ced48263f7091beff1"
+dependencies = [
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "either_of",
+ "erased",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools",
+ "js-sys",
+ "next_tuple",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "slotmap",
+ "throw_error",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "throw_error"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "iri-string",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "sha1",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "typed-builder"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm_split_helpers"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0cb6d1008be3c4c5abc31a407bfb8c8449ae14efc8561c1db821f79b9614b0a"
+dependencies = [
+ "async-once-cell",
+ "wasm_split_macros",
+]
+
+[[package]]
+name = "wasm_split_macros"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a659ffe5c7f4538aa6357c07e3d73221cc61eba03bd9a081e14bc91ed09b8c"
+dependencies = [
+ "base16",
+ "quote",
+ "sha2",
+ "syn",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0fcbd14
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,58 @@
+[workspace]
+resolver = "2"
+members = ["crates/mantra-server", "crates/mantra-ui"]
+
+[workspace.dependencies]
+leptos = { version = "0.8" }
+leptos_axum = "0.8"
+leptos_router = { version = "0.8" }
+leptos_meta = "0.8"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["full"] }
+axum = "0.8"
+tower-http = { version = "0.6", features = ["trace", "fs"] }
+tracing = "0.1"
+tracing-subscriber = "0.3"
+anyhow = "1"
+pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
+gray_matter = "0.2"
+reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
+chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
+
+[profile.release]
+opt-level = "z"
+lto = "fat"
+codegen-units = 1
+
+[profile.dev.package.leptos_macro]
+opt-level = 3
+
+# cargo-leptos workspace config
+[[workspace.metadata.leptos]]
+name = "mantra"
+bin-package = "mantra-server"
+bin-exe-name = "mantra-server"
+bin-features = ["ssr"]
+bin-default-features = false
+lib-package = "mantra-ui"
+lib-features = ["hydrate"]
+lib-default-features = false
+output-name = "mantra"
+site-root = "target/site"
+site-pkg-dir = "pkg"
+style-file = "sass/main.scss"
+assets-dir = "static"
+site-addr = "127.0.0.1:9920"
+reload-port = 3040
+env = "DEV"
+browserquery = "defaults"
+lib-profile-release = "wasm-release"
+
+[profile.wasm-release]
+inherits = "release"
+opt-level = "z"
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = "symbols"
diff --git a/crates/mantra-server/Cargo.toml b/crates/mantra-server/Cargo.toml
new file mode 100644
index 0000000..e229972
--- /dev/null
+++ b/crates/mantra-server/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "mantra-server"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "mantra-server"
+path = "src/main.rs"
+
+[dependencies]
+mantra-ui = { path = "../mantra-ui", features = ["ssr"] }
+leptos = { workspace = true, features = ["ssr"] }
+leptos_axum = { workspace = true }
+leptos_router = { workspace = true, features = ["ssr"] }
+leptos_meta = { workspace = true, features = ["ssr"] }
+axum = { workspace = true }
+tokio = { workspace = true }
+tower-http = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true, features = ["env-filter"] }
+anyhow = { workspace = true }
+pulldown-cmark = { workspace = true }
+gray_matter = { workspace = true }
+serde = { workspace = true }
+
+[features]
+default = []
+ssr = []
diff --git a/crates/mantra-server/src/corpus_loader.rs b/crates/mantra-server/src/corpus_loader.rs
new file mode 100644
index 0000000..0b35c93
--- /dev/null
+++ b/crates/mantra-server/src/corpus_loader.rs
@@ -0,0 +1,331 @@
+//! Loads the design-dna corpus from a directory of markdown files.
+//!
+//! Layout expected (relative to MANTRA_CONTENT_DIR):
+//! cycle-1-philosophy/
+//! _index.md ← corpus-level index with themes
+//! .md × N ← one distillation per source
+//!
+//! The loader returns an in-memory `Corpus` ready for SSR renders.
+
+use std::collections::HashMap;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use anyhow::{anyhow, Context, Result};
+use gray_matter::engine::YAML;
+use gray_matter::Matter;
+use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd};
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
+use serde::Deserialize;
+
+use mantra_ui::corpus::{Corpus, Source, Theme};
+
+#[derive(Deserialize, Debug, Default)]
+struct SourceFrontmatter {
+ #[serde(default)]
+ source: String,
+ #[serde(default)]
+ tags: Vec,
+ #[serde(default)]
+ confidence: String,
+ #[serde(default, rename = "type")]
+ _kind: String,
+}
+
+pub fn load_corpus(root: &Path) -> Result {
+ load_corpus_alt_cycle_dir(root, "cycle-1-philosophy")
+}
+
+pub fn load_corpus_alt_cycle_dir(root: &Path, cycle_subdir: &str) -> Result {
+ let cycle_dir = root.join(cycle_subdir);
+ if !cycle_dir.is_dir() {
+ return Err(anyhow!(
+ "content dir missing {} subdir: {}",
+ cycle_subdir,
+ cycle_dir.display()
+ ));
+ }
+
+ // First pass: gather all source files (exclude _index.md).
+ let mut source_paths: Vec = Vec::new();
+ for entry in fs::read_dir(&cycle_dir)? {
+ let path = entry?.path();
+ let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
+ continue;
+ };
+ if !name.ends_with(".md") {
+ continue;
+ }
+ if name.starts_with('_') {
+ continue;
+ }
+ source_paths.push(path);
+ }
+ source_paths.sort();
+
+ let mut sources: HashMap = HashMap::new();
+ let mut order: Vec = Vec::new();
+ for path in &source_paths {
+ let src = parse_source(path).with_context(|| {
+ format!("parsing source {}", path.display())
+ })?;
+ order.push(src.slug.clone());
+ sources.insert(src.slug.clone(), src);
+ }
+
+ // Themes come from the _index.md if present; skip gracefully otherwise.
+ let themes = parse_themes(&cycle_dir.join("_index.md"))
+ .unwrap_or_else(|e| {
+ tracing::warn!("no themes loaded: {e}");
+ Vec::new()
+ });
+
+ Ok(Corpus { order, sources, themes })
+}
+
+fn parse_source(path: &Path) -> Result {
+ let raw = fs::read_to_string(path)?;
+ let matter = Matter::::new();
+ let parsed = matter.parse(&raw);
+ let fm: SourceFrontmatter = parsed
+ .data
+ .as_ref()
+ .and_then(|p| p.deserialize().ok())
+ .unwrap_or_default();
+
+ let body_md = parsed.content;
+ let slug = path
+ .file_stem()
+ .and_then(|s| s.to_str())
+ .ok_or_else(|| anyhow!("bad filename: {}", path.display()))?
+ .to_string();
+
+ // Extract H1 (### replaces some files — just grab the first #-line).
+ let title = body_md
+ .lines()
+ .find(|l| l.starts_with("# "))
+ .map(|l| l.trim_start_matches("# ").trim().to_string())
+ .unwrap_or_else(|| slug.clone());
+
+ let core_claim = extract_section(&body_md, "## Core claim")
+ .unwrap_or_default()
+ .trim()
+ .replace('\n', " ");
+
+ let author = fm
+ .source
+ .split(',')
+ .next()
+ .unwrap_or(&slug)
+ .trim()
+ .to_string();
+
+ let body_html = md_to_html(&body_md);
+
+ Ok(Source {
+ slug,
+ title,
+ author,
+ core_claim,
+ tags: fm.tags,
+ confidence: if fm.confidence.is_empty() {
+ "medium".into()
+ } else {
+ fm.confidence
+ },
+ body_html,
+ })
+}
+
+/// Extract text content between a given heading and the next `## ` heading.
+fn extract_section(md: &str, heading: &str) -> Option {
+ let mut out = String::new();
+ let mut capturing = false;
+ for line in md.lines() {
+ if line.trim_start().starts_with(heading) {
+ capturing = true;
+ continue;
+ }
+ if capturing && line.trim_start().starts_with("## ") {
+ break;
+ }
+ if capturing {
+ out.push_str(line);
+ out.push('\n');
+ }
+ }
+ if out.trim().is_empty() { None } else { Some(out) }
+}
+
+/// Render markdown to HTML and inject stable `data-para-id` attributes
+/// on every `` element. The id is a short content-hash: stable
+/// across minor edits of the surrounding text, unique enough within a
+/// single source document. The margin-layer UI uses these ids to
+/// anchor notes and Claude-asks to specific paragraphs.
+fn md_to_html(md: &str) -> String {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_SMART_PUNCTUATION);
+
+ // First pass: render plain HTML.
+ let parser = Parser::new_ext(md, opts);
+ let mut raw = String::new();
+ html::push_html(&mut raw, parser);
+
+ // Second pass: re-parse to capture paragraph text for hashing, and
+ // inject data-para-id. pulldown-cmark emits events in order, so we
+ // can walk the source again, track paragraphs, compute hashes, and
+ // then do a simple textual rewrite on the output.
+ let parser2 = Parser::new_ext(md, opts);
+ let mut current_para_text = String::new();
+ let mut in_para = false;
+ let mut para_hashes: Vec = Vec::new();
+ for ev in parser2 {
+ match ev {
+ Event::Start(Tag::Paragraph) => {
+ in_para = true;
+ current_para_text.clear();
+ }
+ Event::End(TagEnd::Paragraph) => {
+ let id = short_hash(current_para_text.trim());
+ para_hashes.push(id);
+ in_para = false;
+ }
+ Event::Text(t) if in_para => current_para_text.push_str(&t),
+ Event::Code(t) if in_para => current_para_text.push_str(&t),
+ _ => {}
+ }
+ }
+
+ // Inject attributes: replace each `` (in order) with
+ // `
`. We walk the byte buffer to find the
+ // ASCII literal `
`, but copy UTF-8 slices (not byte-by-byte)
+ // to preserve non-ASCII correctly — the earlier `push(u8 as char)`
+ // approach silently decoded multibyte UTF-8 as Latin-1 and turned
+ // em-dashes / smart-quotes / Cyrillic / Greek into mojibake.
+ let mut out = String::with_capacity(raw.len() + para_hashes.len() * 24);
+ let mut pending = para_hashes.into_iter();
+ let bytes = raw.as_bytes();
+ let mut last_copied = 0usize;
+ let mut i = 0usize;
+ while i + 3 <= bytes.len() {
+ if &bytes[i..i + 3] == b"
" {
+ // flush everything from last_copied..i as a proper str slice
+ out.push_str(&raw[last_copied..i]);
+ if let Some(id) = pending.next() {
+ out.push_str(&format!(r#"
"#));
+ } else {
+ out.push_str("
");
+ }
+ i += 3;
+ last_copied = i;
+ } else {
+ i += 1;
+ }
+ }
+ out.push_str(&raw[last_copied..]);
+ out
+}
+
+fn short_hash(s: &str) -> String {
+ let mut h = DefaultHasher::new();
+ s.hash(&mut h);
+ format!("{:08x}", h.finish() & 0xFFFF_FFFF)
+}
+
+/// Parse `_index.md` for the 12 running themes + their contributing sources.
+///
+/// Format assumption: each theme is introduced by `### {n}. {title}`
+/// with the following paragraph carrying a comma-separated list of
+/// author names. We map author names back to slugs by substring-match
+/// on the source files' author field. Fallback if pattern doesn't
+/// match: empty themes list, landing shows empty themes column.
+fn parse_themes(index_path: &Path) -> Result> {
+ let raw = fs::read_to_string(index_path)?;
+ let mut themes: Vec = Vec::new();
+ let mut current: Option<(String, String, String)> = None; // (slug, title, desc_md)
+
+ for line in raw.lines() {
+ if let Some(rest) = line.strip_prefix("### ") {
+ if let Some((slug, title, desc)) = current.take() {
+ themes.push(finalize_theme(slug, title, desc));
+ }
+ // e.g. "### 1. Emptiness / absence как носитель формы"
+ let rest = rest.trim();
+ let after_num = rest
+ .split_once('.')
+ .map(|(_, r)| r.trim().to_string())
+ .unwrap_or_else(|| rest.to_string());
+ let slug = slugify(&after_num);
+ current = Some((slug, after_num, String::new()));
+ } else if line.starts_with("## ") {
+ // exit "running themes" section — fallthrough to push last theme
+ if let Some((slug, title, desc)) = current.take() {
+ themes.push(finalize_theme(slug, title, desc));
+ }
+ } else if let Some((_, _, desc)) = current.as_mut() {
+ desc.push_str(line);
+ desc.push('\n');
+ }
+ }
+ if let Some((slug, title, desc)) = current.take() {
+ themes.push(finalize_theme(slug, title, desc));
+ }
+ Ok(themes)
+}
+
+fn finalize_theme(slug: String, title: String, desc_md: String) -> Theme {
+ let contributing = parse_theme_contributors(&desc_md);
+ let description_html = md_to_html(desc_md.trim());
+ Theme {
+ slug,
+ title,
+ description_html,
+ contributing,
+ }
+}
+
+/// Very loose extraction: pull parenthesis-grouped author names out of
+/// the theme description and attempt to match them to known sources.
+/// Since we don't yet have the corpus at this point, we just return a
+/// best-guess list of tokens — the landing component can filter by
+/// membership later if desired.
+fn parse_theme_contributors(desc: &str) -> Vec {
+ // Heuristic: grab author names from the first parenthetical group.
+ let Some(start) = desc.find('(') else {
+ return Vec::new();
+ };
+ let Some(end) = desc[start..].find(')') else {
+ return Vec::new();
+ };
+ let inside = &desc[start + 1..start + end];
+ inside
+ .split([',', '·'])
+ .map(|s| s.trim())
+ .filter(|s| !s.is_empty())
+ .map(|s| {
+ // Strip parenthetical hints like "Laozi (ch. 11)" → "Laozi"
+ s.split_whitespace()
+ .next()
+ .unwrap_or(s)
+ .to_string()
+ })
+ .map(|s| slugify(&s))
+ .filter(|s| !s.is_empty())
+ .collect()
+}
+
+fn slugify(s: &str) -> String {
+ s.to_lowercase()
+ .chars()
+ .map(|c| if c.is_alphanumeric() { c } else { '-' })
+ .collect::()
+ .split('-')
+ .filter(|p| !p.is_empty())
+ .collect::>()
+ .join("-")
+}
diff --git a/crates/mantra-server/src/main.rs b/crates/mantra-server/src/main.rs
new file mode 100644
index 0000000..c8c3585
--- /dev/null
+++ b/crates/mantra-server/src/main.rs
@@ -0,0 +1,119 @@
+//! mantra server — axum + leptos_axum SSR.
+//!
+//! Loads the corpus from disk once at startup (env `MANTRA_CONTENT_DIR`),
+//! hands a shared `Arc` to every render pass via Leptos context.
+
+#![recursion_limit = "256"]
+
+mod corpus_loader;
+
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::Result;
+use axum::routing::any;
+use axum::Router;
+use leptos::config::get_configuration;
+use leptos::prelude::provide_context;
+use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes};
+use tower_http::services::ServeDir;
+use tower_http::trace::TraceLayer;
+use tracing::info;
+
+use mantra_ui::app::{shell, App};
+use mantra_ui::corpus::{BilingualCorpus, BilingualHandle};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ tracing_subscriber::fmt()
+ .with_env_filter(
+ tracing_subscriber::EnvFilter::try_from_env("MANTRA_LOG")
+ .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
+ )
+ .without_time()
+ .init();
+
+ let content_dir = PathBuf::from(
+ std::env::var("MANTRA_CONTENT_DIR")
+ .unwrap_or_else(|_| "content".to_string()),
+ );
+ info!("loading bilingual corpus from {}", content_dir.display());
+
+ // The loader expects a language-specific subdir; we call it twice
+ // against the two sibling directories.
+ let en = corpus_loader::load_corpus(&content_dir)?;
+ // ru lives under ../-ru relative path — or env-overridden.
+ let ru_dir = std::env::var("MANTRA_CONTENT_DIR_RU")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| {
+ // derive from content_dir by replacing trailing component
+ let mut p = content_dir.clone();
+ let last = p
+ .file_name()
+ .map(|n| n.to_string_lossy().into_owned())
+ .unwrap_or_else(|| String::from("content"));
+ p.pop();
+ p.push(format!("{last}-ru"));
+ p
+ });
+ // Hack for symlinked content: our Mac dev uses `content` symlink to
+ // design-dna/, so content-ru doesn't exist at peer — fall back to the
+ // sibling `cycle-1-philosophy-ru` under the DESIGN-DNA root instead.
+ let ru = if ru_dir.is_dir() {
+ corpus_loader::load_corpus(&ru_dir)?
+ } else {
+ // if MANTRA_CONTENT_DIR is design-dna root, then ru lives under
+ // /cycle-1-philosophy-ru — but that's a file layout
+ // difference, handled inside load_corpus_ru.
+ corpus_loader::load_corpus_alt_cycle_dir(&content_dir, "cycle-1-philosophy-ru")?
+ };
+
+ let bilingual: BilingualHandle = Arc::new(BilingualCorpus {
+ ru: Arc::new(ru),
+ en: Arc::new(en),
+ });
+ info!(
+ "loaded: ru={} sources / en={} sources, themes={}",
+ bilingual.ru.sources.len(),
+ bilingual.en.sources.len(),
+ bilingual.ru.themes.len()
+ );
+
+ let conf = get_configuration(None)
+ .map_err(|e| anyhow::anyhow!("leptos config: {e}"))?;
+ let leptos_options = conf.leptos_options;
+ let addr: SocketAddr = leptos_options.site_addr;
+ let routes = generate_route_list(App);
+
+ let bilingual_ctx = bilingual.clone();
+ let bilingual_for_sfn = bilingual.clone();
+ let app = Router::new()
+ .route(
+ "/api/{*fn_name}",
+ any(move |req| {
+ let ctx = bilingual_for_sfn.clone();
+ handle_server_fns_with_context(
+ move || provide_context(ctx.clone()),
+ req,
+ )
+ }),
+ )
+ .leptos_routes_with_context(
+ &leptos_options,
+ routes,
+ move || provide_context(bilingual_ctx.clone()),
+ {
+ let opts = leptos_options.clone();
+ move || shell(opts.clone())
+ },
+ )
+ .fallback_service(ServeDir::new("target/site"))
+ .layer(TraceLayer::new_for_http())
+ .with_state(leptos_options);
+
+ info!("mantra listening on {addr}");
+ let listener = tokio::net::TcpListener::bind(&addr).await?;
+ axum::serve(listener, app.into_make_service()).await?;
+ Ok(())
+}
diff --git a/crates/mantra-ui/Cargo.toml b/crates/mantra-ui/Cargo.toml
new file mode 100644
index 0000000..fa80baa
--- /dev/null
+++ b/crates/mantra-ui/Cargo.toml
@@ -0,0 +1,51 @@
+[package]
+name = "mantra-ui"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+leptos.workspace = true
+leptos_router.workspace = true
+leptos_meta.workspace = true
+serde.workspace = true
+chrono = { workspace = true }
+console_error_panic_hook = { version = "0.1", optional = true }
+wasm-bindgen = { version = "0.2.118", optional = true }
+web-sys = { version = "0.3", optional = true, features = [
+ "Window",
+ "Storage",
+ "Document",
+ "Element",
+ "HtmlElement",
+ "HtmlTextAreaElement",
+ "HtmlCollection",
+ "NodeList",
+ "DomTokenList",
+ "Event",
+ "EventTarget",
+ "console",
+] }
+# Server-only — pulled in under ssr feature for notes I/O + Claude HTTP.
+tokio = { workspace = true, optional = true }
+reqwest = { workspace = true, optional = true }
+serde_json = { workspace = true, optional = true }
+
+[features]
+default = []
+hydrate = [
+ "leptos/hydrate",
+ "dep:console_error_panic_hook",
+ "dep:wasm-bindgen",
+ "dep:web-sys",
+]
+ssr = [
+ "leptos/ssr",
+ "leptos_router/ssr",
+ "leptos_meta/ssr",
+ "dep:tokio",
+ "dep:reqwest",
+ "dep:serde_json",
+]
diff --git a/crates/mantra-ui/src/api/mod.rs b/crates/mantra-ui/src/api/mod.rs
new file mode 100644
index 0000000..766228f
--- /dev/null
+++ b/crates/mantra-ui/src/api/mod.rs
@@ -0,0 +1,460 @@
+//! Server functions for the margin layer: notes and Claude-ask.
+//!
+//! Notes live as markdown files in `$MANTRA_NOTES_DIR/.md`.
+//! Each file is append-oriented: a note is a dated entry anchored to
+//! a paragraph-id. Subsequent reads parse the file and return
+//! structured entries.
+
+pub mod types;
+
+use leptos::prelude::*;
+
+pub use types::{LandingData, NoteEntry, NoteKind, SourcePageData, ThemePageData};
+
+// --- fetch_source_page -------------------------------------------------
+//
+// Snapshot for `/source/:slug`. Moves corpus access into a server fn
+// so SSR and hydrate render from the same Resource payload and the
+// client doesn't need the full `Arc` in context.
+
+#[server(endpoint = "source_page")]
+pub async fn fetch_source_page(
+ slug: String,
+ lang: String,
+) -> Result, ServerFnError> {
+ #[cfg(feature = "ssr")]
+ {
+ use crate::corpus::{BilingualHandle, Lang};
+ let bilingual = use_context::()
+ .ok_or_else(|| ServerFnError::new("no corpus in context"))?;
+ let lang = Lang::from_query(Some(&lang));
+ let corpus = bilingual.for_lang(lang);
+ let Some(src) = corpus.get(&slug).cloned() else {
+ return Ok(None);
+ };
+ let (prev, next) = corpus.neighbors(&slug);
+ Ok(Some(SourcePageData {
+ source: src,
+ prev: prev.map(|s| s.to_string()),
+ next: next.map(|s| s.to_string()),
+ }))
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ let _ = (slug, lang);
+ unreachable!()
+ }
+}
+
+// --- fetch_landing -----------------------------------------------------
+
+#[server(endpoint = "landing")]
+pub async fn fetch_landing(lang: String) -> Result {
+ #[cfg(feature = "ssr")]
+ {
+ use crate::corpus::{BilingualHandle, Lang};
+ let bilingual = use_context::()
+ .ok_or_else(|| ServerFnError::new("no corpus in context"))?;
+ let lang = Lang::from_query(Some(&lang));
+ let corpus = bilingual.for_lang(lang);
+ let sources = corpus
+ .order
+ .iter()
+ .filter_map(|slug| corpus.get(slug).cloned())
+ .collect();
+ Ok(LandingData {
+ order: corpus.order.clone(),
+ sources,
+ themes: corpus.themes.clone(),
+ })
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ let _ = lang;
+ unreachable!()
+ }
+}
+
+// --- fetch_theme_page --------------------------------------------------
+
+#[server(endpoint = "theme_page")]
+pub async fn fetch_theme_page(
+ slug: String,
+ lang: String,
+) -> Result, ServerFnError> {
+ #[cfg(feature = "ssr")]
+ {
+ use crate::corpus::{BilingualHandle, Lang};
+ let bilingual = use_context::()
+ .ok_or_else(|| ServerFnError::new("no corpus in context"))?;
+ let lang = Lang::from_query(Some(&lang));
+ let corpus = bilingual.for_lang(lang);
+ let Some(theme) = corpus.themes.iter().find(|t| t.slug == slug).cloned() else {
+ return Ok(None);
+ };
+ let contributing = theme
+ .contributing
+ .iter()
+ .filter_map(|s| corpus.get(s).cloned())
+ .collect();
+ Ok(Some(ThemePageData { theme, contributing }))
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ let _ = (slug, lang);
+ unreachable!()
+ }
+}
+
+// --- fetch_notes -------------------------------------------------------
+
+#[server(endpoint = "notes_fetch")]
+pub async fn fetch_notes(source_slug: String) -> Result, ServerFnError> {
+ #[cfg(feature = "ssr")]
+ {
+ let dir = std::env::var("MANTRA_NOTES_DIR").map_err(|_| {
+ ServerFnError::new("MANTRA_NOTES_DIR not set on server")
+ })?;
+ let path = std::path::PathBuf::from(&dir).join(format!("{source_slug}.md"));
+ if !path.exists() {
+ return Ok(Vec::new());
+ }
+ let raw = tokio::fs::read_to_string(&path)
+ .await
+ .map_err(|e| ServerFnError::new(format!("read notes: {e}")))?;
+ Ok(parse_marginalia(&raw))
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ unreachable!()
+ }
+}
+
+// --- save_note ---------------------------------------------------------
+
+#[server(endpoint = "notes_save")]
+pub async fn save_note(
+ source_slug: String,
+ para_id: String,
+ para_excerpt: String,
+ note_text: String,
+ author: String,
+) -> Result<(), ServerFnError> {
+ #[cfg(feature = "ssr")]
+ {
+ if note_text.trim().is_empty() {
+ return Err(ServerFnError::new("note text is empty"));
+ }
+ append_marginalia_entry(
+ &source_slug,
+ ¶_id,
+ ¶_excerpt,
+ MarginaliaPayload::Note(note_text),
+ &author,
+ )
+ .await?;
+ spawn_git_autocommit(&source_slug, ¶_id).await;
+ Ok(())
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ unreachable!()
+ }
+}
+
+// --- ask_claude --------------------------------------------------------
+
+#[server(endpoint = "claude_ask")]
+pub async fn ask_claude(
+ source_slug: String,
+ para_id: String,
+ para_excerpt: String,
+ question: String,
+ author: String,
+) -> Result {
+ #[cfg(feature = "ssr")]
+ {
+ let q = question.trim();
+ if q.is_empty() {
+ return Err(ServerFnError::new("question is empty"));
+ }
+ let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| {
+ ServerFnError::new("ANTHROPIC_API_KEY not set on server")
+ })?;
+
+ let system = format!(
+ "You are a reading companion for a philosophical substance called \
+ design-dna. A reader is studying the distillation of {source_slug} \
+ and has highlighted a passage. Answer their question grounded in the \
+ passage and its tradition. Be concise (≤300 words), direct, \
+ substance over summary. Same register as the distillation itself: \
+ living note, not academic report. Answer in the same language the \
+ user asked in. If you don't know, say so."
+ );
+ let user_msg = format!(
+ "Context passage:\n\n{para_excerpt}\n\n---\n\nQuestion: {q}"
+ );
+
+ let answer = call_anthropic(&api_key, &system, &user_msg)
+ .await
+ .map_err(|e| ServerFnError::new(format!("claude: {e}")))?;
+
+ // Persist Q+A to marginalia too — reading is a dialogue, and the
+ // dialogue is the record.
+ append_marginalia_entry(
+ &source_slug,
+ ¶_id,
+ ¶_excerpt,
+ MarginaliaPayload::Ask {
+ question: q.to_string(),
+ answer: answer.clone(),
+ },
+ &author,
+ )
+ .await?;
+ spawn_git_autocommit(&source_slug, ¶_id).await;
+ Ok(answer)
+ }
+ #[cfg(not(feature = "ssr"))]
+ {
+ unreachable!()
+ }
+}
+
+// =====================================================================
+// server-only implementations
+// =====================================================================
+
+#[cfg(feature = "ssr")]
+enum MarginaliaPayload {
+ Note(String),
+ Ask { question: String, answer: String },
+}
+
+#[cfg(feature = "ssr")]
+async fn append_marginalia_entry(
+ source_slug: &str,
+ para_id: &str,
+ para_excerpt: &str,
+ payload: MarginaliaPayload,
+ author: &str,
+) -> Result<(), ServerFnError> {
+ let dir = std::env::var("MANTRA_NOTES_DIR").map_err(|_| {
+ ServerFnError::new("MANTRA_NOTES_DIR not set on server")
+ })?;
+ let dir = std::path::PathBuf::from(&dir);
+ tokio::fs::create_dir_all(&dir)
+ .await
+ .map_err(|e| ServerFnError::new(format!("mkdir: {e}")))?;
+ let path = dir.join(format!("{source_slug}.md"));
+
+ let existed = path.exists();
+ let header_needed = !existed;
+ let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M UTC").to_string();
+
+ let mut out = String::new();
+ if header_needed {
+ out.push_str(&format!(
+ "---\ntype: marginalia\nsource: {source_slug}\nproject: design-dna\n---\n\n# Marginalia · {source_slug}\n\n"
+ ));
+ }
+
+ // We always append a fresh block. Simple, no merge logic v0.1.
+ out.push_str(&format!("## {{#{para_id}}}\n\n"));
+ let excerpt = para_excerpt.trim();
+ if !excerpt.is_empty() {
+ // Store the first 180 chars of excerpt as a blockquote anchor.
+ let cut: String = excerpt.chars().take(180).collect();
+ out.push_str("> ");
+ out.push_str(&cut.replace('\n', " "));
+ if excerpt.chars().count() > 180 {
+ out.push_str(" …");
+ }
+ out.push_str("\n\n");
+ }
+
+ match payload {
+ MarginaliaPayload::Note(text) => {
+ out.push_str(&format!("### {author} · {ts}\n\n{text}\n\n"));
+ }
+ MarginaliaPayload::Ask { question, answer } => {
+ out.push_str(&format!(
+ "### {author} · {ts} (asked)\n\n**Q:** {question}\n\n**A (Claude):** {answer}\n\n"
+ ));
+ }
+ }
+
+ use tokio::io::AsyncWriteExt;
+ let mut f = tokio::fs::OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&path)
+ .await
+ .map_err(|e| ServerFnError::new(format!("open notes: {e}")))?;
+ f.write_all(out.as_bytes())
+ .await
+ .map_err(|e| ServerFnError::new(format!("write notes: {e}")))?;
+ Ok(())
+}
+
+#[cfg(feature = "ssr")]
+fn parse_marginalia(raw: &str) -> Vec {
+ // Structure we parse:
+ // ## {#}
+ // > excerpt
+ //
+ // ### Alexey · 2026-04-23 12:34
+ // Note body.
+ //
+ // ### Alexey · 2026-04-23 13:00 (asked)
+ // **Q:** ...
+ //
+ // **A (Claude):** ...
+
+ let mut entries: Vec = Vec::new();
+ let mut current_para: Option = None;
+ let mut current_excerpt: String = String::new();
+ let mut current_author_ts: Option = None;
+ let mut current_kind: NoteKind = NoteKind::Note;
+ let mut current_body: String = String::new();
+
+ fn flush(
+ entries: &mut Vec,
+ current_para: &Option,
+ current_excerpt: &str,
+ current_author_ts: &Option,
+ current_kind: NoteKind,
+ current_body: &str,
+ ) {
+ if let (Some(para), Some(ats)) = (current_para.clone(), current_author_ts.clone()) {
+ let body = current_body.trim();
+ if !body.is_empty() {
+ entries.push(NoteEntry {
+ para_id: para,
+ excerpt: current_excerpt.trim().to_string(),
+ author_ts: ats,
+ kind: current_kind,
+ body: body.to_string(),
+ });
+ }
+ }
+ }
+
+ for line in raw.lines() {
+ if let Some(rest) = line.strip_prefix("## {#") {
+ // Flush previous entry.
+ flush(
+ &mut entries,
+ ¤t_para,
+ ¤t_excerpt,
+ ¤t_author_ts,
+ current_kind,
+ ¤t_body,
+ );
+ current_body.clear();
+ current_author_ts = None;
+ current_kind = NoteKind::Note;
+ current_excerpt.clear();
+ let id = rest.trim_end_matches('}').trim();
+ current_para = Some(id.to_string());
+ } else if line.starts_with("> ") {
+ if !current_excerpt.is_empty() {
+ current_excerpt.push(' ');
+ }
+ current_excerpt.push_str(line.trim_start_matches("> ").trim());
+ } else if let Some(rest) = line.strip_prefix("### ") {
+ flush(
+ &mut entries,
+ ¤t_para,
+ ¤t_excerpt,
+ ¤t_author_ts,
+ current_kind,
+ ¤t_body,
+ );
+ current_body.clear();
+ let asked = rest.ends_with("(asked)");
+ current_kind = if asked { NoteKind::Ask } else { NoteKind::Note };
+ let head = rest.trim_end_matches("(asked)").trim();
+ current_author_ts = Some(head.to_string());
+ } else {
+ current_body.push_str(line);
+ current_body.push('\n');
+ }
+ }
+ flush(
+ &mut entries,
+ ¤t_para,
+ ¤t_excerpt,
+ ¤t_author_ts,
+ current_kind,
+ ¤t_body,
+ );
+
+ entries
+}
+
+#[cfg(feature = "ssr")]
+async fn call_anthropic(
+ api_key: &str,
+ system: &str,
+ user: &str,
+) -> Result {
+ let body = serde_json::json!({
+ "model": "claude-sonnet-4-6",
+ "max_tokens": 1024,
+ "system": system,
+ "messages": [{ "role": "user", "content": user }],
+ });
+ let resp = reqwest::Client::new()
+ .post("https://api.anthropic.com/v1/messages")
+ .header("x-api-key", api_key)
+ .header("anthropic-version", "2023-06-01")
+ .header("content-type", "application/json")
+ .json(&body)
+ .send()
+ .await
+ .map_err(|e| format!("request: {e}"))?;
+ if !resp.status().is_success() {
+ let st = resp.status();
+ let body = resp.text().await.unwrap_or_default();
+ return Err(format!("anthropic {st}: {body}"));
+ }
+ let v: serde_json::Value = resp.json().await.map_err(|e| format!("decode: {e}"))?;
+ let text = v["content"][0]["text"]
+ .as_str()
+ .ok_or_else(|| "anthropic response missing content[0].text".to_string())?;
+ Ok(text.to_string())
+}
+
+#[cfg(feature = "ssr")]
+async fn spawn_git_autocommit(source_slug: &str, para_id: &str) {
+ // Fire-and-forget: we don't want to block the user on git push.
+ // Only runs if MANTRA_NOTES_GIT_AUTO_PUSH is set (opt-in).
+ if std::env::var("MANTRA_NOTES_GIT_AUTO_PUSH").is_err() {
+ return;
+ }
+ let dir = match std::env::var("MANTRA_NOTES_DIR") {
+ Ok(d) => d,
+ Err(_) => return,
+ };
+ let slug = source_slug.to_string();
+ let pid = para_id.to_string();
+ tokio::spawn(async move {
+ let _ = tokio::process::Command::new("sh")
+ .arg("-c")
+ .arg(format!(
+ "cd {dir} && git add -A && git commit -m 'note: {slug} {pid}' && git push 2>&1",
+ dir = shell_escape(&dir),
+ slug = shell_escape(&slug),
+ pid = shell_escape(&pid),
+ ))
+ .output()
+ .await;
+ });
+}
+
+#[cfg(feature = "ssr")]
+fn shell_escape(s: &str) -> String {
+ // Good enough for slugs (alphanumeric + dash) and ordinary paths.
+ s.replace('\'', "'\\''")
+}
diff --git a/crates/mantra-ui/src/api/types.rs b/crates/mantra-ui/src/api/types.rs
new file mode 100644
index 0000000..a4344e5
--- /dev/null
+++ b/crates/mantra-ui/src/api/types.rs
@@ -0,0 +1,46 @@
+//! Wire types for the margin-layer server fns.
+
+use serde::{Deserialize, Serialize};
+
+use crate::corpus::{Source, Theme};
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum NoteKind {
+ Note,
+ Ask,
+}
+
+/// One note OR Q+A entry attached to a specific paragraph.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct NoteEntry {
+ pub para_id: String,
+ pub excerpt: String,
+ pub author_ts: String,
+ pub kind: NoteKind,
+ pub body: String,
+}
+
+/// Snapshot of a single source + its neighbors for the source page.
+/// Carries everything the page needs so SSR and hydrate render
+/// identically from the same payload.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct SourcePageData {
+ pub source: Source,
+ pub prev: Option,
+ pub next: Option,
+}
+
+/// Snapshot for the landing page — works in reading order + themes.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct LandingData {
+ pub order: Vec,
+ pub sources: Vec,
+ pub themes: Vec,
+}
+
+/// Snapshot for the theme page — one theme + contributing sources.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ThemePageData {
+ pub theme: Theme,
+ pub contributing: Vec,
+}
diff --git a/crates/mantra-ui/src/app.rs b/crates/mantra-ui/src/app.rs
new file mode 100644
index 0000000..7a6e324
--- /dev/null
+++ b/crates/mantra-ui/src/app.rs
@@ -0,0 +1,47 @@
+//! Root Leptos component and HTML shell.
+
+use leptos::config::LeptosOptions;
+use leptos::prelude::*;
+use leptos_meta::{provide_meta_context, MetaTags, Title};
+use leptos_router::components::*;
+use leptos_router::*;
+
+use crate::pages;
+
+/// HTML skeleton wrapping the root app. Called by
+/// `leptos_axum::LeptosRoutes` to generate the full page SSR.
+pub fn shell(options: LeptosOptions) -> impl IntoView {
+ view! {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
+
+/// Application root — provides meta context, declares routes.
+#[component]
+pub fn App() -> impl IntoView {
+ provide_meta_context();
+
+ view! {
+
+
+ "404"
}>
+
+
+
+
+
+ }
+}
diff --git a/crates/mantra-ui/src/corpus.rs b/crates/mantra-ui/src/corpus.rs
new file mode 100644
index 0000000..c8f1d4d
--- /dev/null
+++ b/crates/mantra-ui/src/corpus.rs
@@ -0,0 +1,125 @@
+//! Corpus data types — shared between SSR render pass and UI components.
+//!
+//! The server crate owns `load_corpus` and populates the structures
+//! from disk; the UI crate only reads them. Everything here is
+//! `Serialize + Deserialize + Clone` so it survives Leptos Resources
+//! and possible future hydration round-trips.
+
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::sync::Arc;
+
+/// The entire loaded corpus. Shared via Leptos context as `Arc`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Corpus {
+ /// Ordered list of source slugs (reading order — chronological or
+ /// thematic; driven by `_index.md`'s table of works).
+ pub order: Vec,
+ /// Slug → Source lookup.
+ pub sources: HashMap,
+ /// Running themes extracted from `_index.md`.
+ pub themes: Vec,
+}
+
+impl Corpus {
+ pub fn get(&self, slug: &str) -> Option<&Source> {
+ self.sources.get(slug)
+ }
+
+ /// Index-in-reading-order of `slug`, if present.
+ pub fn index_of(&self, slug: &str) -> Option {
+ self.order.iter().position(|s| s == slug)
+ }
+
+ /// Neighbor (prev, next) by reading order — wrap-around.
+ pub fn neighbors(&self, slug: &str) -> (Option<&str>, Option<&str>) {
+ let Some(i) = self.index_of(slug) else { return (None, None) };
+ let n = self.order.len();
+ let prev = if n > 1 {
+ Some(self.order[(i + n - 1) % n].as_str())
+ } else {
+ None
+ };
+ let next = if n > 1 {
+ Some(self.order[(i + 1) % n].as_str())
+ } else {
+ None
+ };
+ (prev, next)
+ }
+}
+
+/// One distillation. Body is pre-rendered HTML (pulldown-cmark output).
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Source {
+ pub slug: String,
+ /// Display title — "Symposium (Plato)" etc.
+ pub title: String,
+ /// Short author label — "Plato", "Ibn ʿArabī" etc.
+ pub author: String,
+ /// The core-claim one-liner (extracted from `## Core claim` section).
+ pub core_claim: String,
+ /// Tags from YAML frontmatter.
+ pub tags: Vec,
+ /// Confidence level (high / medium / low) from frontmatter.
+ pub confidence: String,
+ /// The full markdown body rendered to HTML.
+ pub body_html: String,
+}
+
+/// A running theme — pattern that proustep across multiple sources.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Theme {
+ pub slug: String,
+ pub title: String,
+ /// Short pitch (one paragraph).
+ pub description_html: String,
+ /// Sources that contribute to this theme (by slug).
+ pub contributing: Vec,
+}
+
+/// Convenience alias — we always thread the corpus through context as
+/// an `Arc` so multiple concurrent requests share one in-memory copy.
+pub type CorpusHandle = Arc;
+
+/// Reading language.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Lang {
+ Ru,
+ En,
+}
+
+impl Lang {
+ pub fn as_str(&self) -> &'static str {
+ match self { Lang::Ru => "ru", Lang::En => "en" }
+ }
+ pub fn from_query(s: Option<&str>) -> Self {
+ match s {
+ Some("en") => Lang::En,
+ _ => Lang::Ru,
+ }
+ }
+ pub fn other(&self) -> Lang {
+ match self { Lang::Ru => Lang::En, Lang::En => Lang::Ru }
+ }
+}
+
+/// Bilingual container — same slugs in both languages. Each language
+/// is held as an `Arc` so `for_lang` is cheap and callers can
+/// keep their own handle without deep-cloning a 60kw corpus.
+#[derive(Debug, Clone)]
+pub struct BilingualCorpus {
+ pub ru: Arc,
+ pub en: Arc,
+}
+
+impl BilingualCorpus {
+ pub fn for_lang(&self, lang: Lang) -> Arc {
+ match lang {
+ Lang::Ru => self.ru.clone(),
+ Lang::En => self.en.clone(),
+ }
+ }
+}
+
+pub type BilingualHandle = Arc;
diff --git a/crates/mantra-ui/src/lib.rs b/crates/mantra-ui/src/lib.rs
new file mode 100644
index 0000000..43679f1
--- /dev/null
+++ b/crates/mantra-ui/src/lib.rs
@@ -0,0 +1,22 @@
+//! mantra — Leptos UI.
+//!
+//! Reader for dense markdown distillate corpora. First consumer: the
+//! design-dna philosophy corpus (20 sources, ~61k words). The server
+//! crate loads the corpus into `Arc` at startup and provides
+//! it via context; UI components read from that context.
+
+#![recursion_limit = "256"]
+
+pub mod api;
+pub mod app;
+pub mod corpus;
+pub mod pages;
+
+/// WASM entry point. Called by the cargo-leptos-injected bootstrap
+/// script once the bundle has downloaded in the browser.
+#[cfg(feature = "hydrate")]
+#[wasm_bindgen::prelude::wasm_bindgen]
+pub fn hydrate() {
+ console_error_panic_hook::set_once();
+ leptos::mount::hydrate_body(app::App);
+}
diff --git a/crates/mantra-ui/src/pages/landing.rs b/crates/mantra-ui/src/pages/landing.rs
new file mode 100644
index 0000000..c85679c
--- /dev/null
+++ b/crates/mantra-ui/src/pages/landing.rs
@@ -0,0 +1,146 @@
+//! `/` — two-door entry into the corpus: works (20) and themes.
+
+use leptos::prelude::*;
+use leptos_router::hooks::use_query_map;
+
+use crate::api::{fetch_landing, LandingData};
+use crate::corpus::{Lang, Source, Theme};
+use crate::pages::shared::LangToggle;
+
+#[component]
+pub fn Landing() -> impl IntoView {
+ let query = use_query_map();
+ let lang = Memo::new(move |_| {
+ Lang::from_query(query.read().get("lang").as_deref())
+ });
+
+ let data = Resource::new(
+ move || lang.get().as_str().to_string(),
+ |l| fetch_landing(l),
+ );
+
+ let hero_title = move || match lang.get() {
+ Lang::Ru => "дизайн днк",
+ Lang::En => "design dna",
+ };
+ let hero_subtitle = move || match lang.get() {
+ Lang::Ru => "цикл 1 · философия · 20 источников",
+ Lang::En => "cycle 1 · philosophy · 20 sources",
+ };
+ let hero_question = move || match lang.get() {
+ Lang::Ru => "почему форма трогает человеческое сердце?",
+ Lang::En => "why does form move the human heart?",
+ };
+ let works_label = move || match lang.get() {
+ Lang::Ru => "работы",
+ Lang::En => "works",
+ };
+ let themes_label = move || match lang.get() {
+ Lang::Ru => "темы",
+ Lang::En => "themes",
+ };
+ let sources_suffix = move |n: usize| match lang.get() {
+ Lang::Ru => format!(" · {n} источников"),
+ Lang::En => format!(" · {n} sources"),
+ };
+
+ view! {
+
+
+
+ {hero_title}
+ {hero_subtitle}
+ {hero_question}
+
+
+ "…"
}>
+ {move || {
+ data.get().map(|res| {
+ let lang_val = lang.get();
+ let suffix_for_themes = sources_suffix.clone();
+ match res {
+ Ok(LandingData { order, sources, themes }) => {
+ // Build slug -> Source map for cheap lookup in the For body
+ let mut by_slug: std::collections::HashMap =
+ std::collections::HashMap::with_capacity(sources.len());
+ for s in sources { by_slug.insert(s.slug.clone(), s); }
+ view! {
+
+ }.into_any()
+ }
+ Err(e) => view! { {format!("{e}")}
}.into_any(),
+ }
+ })
+ }}
+
+
+ }
+}
+
+#[component]
+fn WorkLine(source: Option, slug: String, lang: Lang) -> impl IntoView {
+ let href = format!("/source/{slug}?lang={}", lang.as_str());
+ match source {
+ None => view! { }.into_any(),
+ Some(s) => view! {
+
+
+ {s.author}
+ " · "
+ {s.title}
+ {s.core_claim}
+
+
+ }.into_any(),
+ }
+}
+
+#[component]
+fn ThemeLine(theme: Theme, lang: Lang, suffix: F) -> impl IntoView
+where
+ F: Fn(usize) -> String + 'static + Send + Sync + Clone,
+{
+ let href = format!("/theme/{}?lang={}", theme.slug, lang.as_str());
+ let count = theme.contributing.len();
+ view! {
+
+
+ {theme.title}
+ {suffix(count)}
+
+
+ }
+}
diff --git a/crates/mantra-ui/src/pages/margin.rs b/crates/mantra-ui/src/pages/margin.rs
new file mode 100644
index 0000000..9f889d2
--- /dev/null
+++ b/crates/mantra-ui/src/pages/margin.rs
@@ -0,0 +1,355 @@
+//! Margin layer — the drawer that opens when a paragraph is clicked.
+//!
+//! Holds: a Notes tab (write a thought, permanent record in vault) and
+//! an Ask tab (ask Claude about the passage; Q+A is also saved to the
+//! vault — the dialogue IS the record).
+//!
+//! Author identity for v0.1: an inline input pinned at the bottom of
+//! the drawer, prefilled from `localStorage` under `mantra.author`.
+//! No modal prompts.
+
+use leptos::html;
+use leptos::prelude::*;
+use leptos::task::spawn_local;
+
+use crate::api::{ask_claude, fetch_notes, save_note, NoteEntry, NoteKind};
+use crate::corpus::Lang;
+
+/// A paragraph the user opened the drawer on.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ActivePara {
+ pub id: String,
+ pub excerpt: String,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Tab {
+ Note,
+ Ask,
+}
+
+#[component]
+pub fn MarginDrawer(
+ slug: Signal,
+ lang: Signal,
+ active: ReadSignal>,
+ set_active: WriteSignal >,
+ /// Bumped whenever the set of notes for this slug changes, so the
+ /// source page can re-annotate paragraph dots.
+ notes_tick: RwSignal,
+) -> impl IntoView {
+ let (notes, set_notes) = signal::>(Vec::new());
+ let (tab, set_tab) = signal(Tab::Note);
+ let (draft, set_draft) = signal(String::new());
+ let (pending, set_pending) = signal(false);
+ let (error, set_error) = signal::>(None);
+ let (author, set_author) = signal(initial_author());
+
+ let textarea_ref: NodeRef = NodeRef::new();
+
+ // Refetch all notes for the current slug. Happens on: mount,
+ // slug change, after save, after ask.
+ let reload = move |slug_s: String| {
+ spawn_local(async move {
+ match fetch_notes(slug_s).await {
+ Ok(v) => {
+ set_notes.set(v);
+ notes_tick.update(|n| *n = n.wrapping_add(1));
+ }
+ Err(e) => {
+ log(&format!("fetch_notes error: {e}"));
+ }
+ }
+ });
+ };
+
+ // Track slug changes — reload notes.
+ Effect::new(move |_| {
+ let s = slug.get();
+ if !s.is_empty() {
+ reload(s);
+ }
+ });
+
+ // Clear draft/error/tab when switching paragraphs.
+ Effect::new(move |_| {
+ let _ = active.get();
+ set_draft.set(String::new());
+ set_error.set(None);
+ set_tab.set(Tab::Note);
+ clear_textarea(&textarea_ref);
+ });
+
+ let filtered = Memo::new(move |_| {
+ let Some(a) = active.get() else { return Vec::new() };
+ notes.with(|all| {
+ all.iter().filter(|e| e.para_id == a.id).cloned().collect::>()
+ })
+ });
+
+ let close = move |_| set_active.set(None);
+
+ let submit = move || {
+ log("submit clicked");
+ let Some(a) = active.get_untracked() else {
+ log("submit: no active paragraph");
+ return;
+ };
+ let text = draft.get_untracked().trim().to_string();
+ if text.is_empty() {
+ log("submit: empty draft");
+ return;
+ }
+ let author_name = {
+ let a = author.get_untracked();
+ if a.trim().is_empty() { "anon".to_string() } else { a }
+ };
+ let slug_s = slug.get_untracked();
+ let para_id = a.id.clone();
+ let excerpt = a.excerpt.clone();
+ let current_tab = tab.get_untracked();
+ log(&format!(
+ "submit: tab={:?} slug={} para_id={} text_len={} author={}",
+ current_tab, slug_s, para_id, text.len(), author_name,
+ ));
+ set_pending.set(true);
+ set_error.set(None);
+ spawn_local(async move {
+ let res = match current_tab {
+ Tab::Note => save_note(
+ slug_s.clone(),
+ para_id,
+ excerpt,
+ text,
+ author_name,
+ ).await.map(|_| ()),
+ Tab::Ask => ask_claude(
+ slug_s.clone(),
+ para_id,
+ excerpt,
+ text,
+ author_name,
+ ).await.map(|_| ()),
+ };
+ set_pending.set(false);
+ match res {
+ Ok(()) => {
+ log("submit: ok");
+ set_draft.set(String::new());
+ clear_textarea(&textarea_ref);
+ reload(slug_s);
+ }
+ Err(e) => {
+ log(&format!("submit: error {e}"));
+ set_error.set(Some(format!("{e}")));
+ }
+ }
+ });
+ };
+
+ let label_close = move || match lang.get() { Lang::Ru => "закрыть", Lang::En => "close" };
+ let label_note = move || match lang.get() { Lang::Ru => "заметка", Lang::En => "note" };
+ let label_ask = move || match lang.get() { Lang::Ru => "спросить", Lang::En => "ask" };
+ let placeholder_note = move || match lang.get() {
+ Lang::Ru => "запишите мысль…",
+ Lang::En => "write a thought…",
+ };
+ let placeholder_ask = move || match lang.get() {
+ Lang::Ru => "спросите Клода о пассаже…",
+ Lang::En => "ask Claude about the passage…",
+ };
+ let label_save = move || match lang.get() { Lang::Ru => "сохранить", Lang::En => "save" };
+ let label_ask_go = move || match lang.get() { Lang::Ru => "спросить", Lang::En => "ask" };
+ let label_pending_note = move || match lang.get() {
+ Lang::Ru => "сохраняю…",
+ Lang::En => "saving…",
+ };
+ let label_pending_ask = move || match lang.get() {
+ Lang::Ru => "думаю…",
+ Lang::En => "thinking…",
+ };
+ let label_you = move || match lang.get() { Lang::Ru => "вы:", Lang::En => "you:" };
+
+ view! {
+
+
+
+
+
+ {move || active.get().map(|a| a.excerpt)}
+
+
+
+
+
+ {move || {
+ let list = filtered.get();
+ list.into_iter().map(|e| view! {
+
+ }).collect_view()
+ }}
+
+
+
+ }
+}
+
+#[component]
+fn NoteCard(entry: NoteEntry) -> impl IntoView {
+ let is_ask = entry.kind == NoteKind::Ask;
+ let cls = if is_ask { "note-card note-card-ask" } else { "note-card" };
+
+ // For Ask entries the body is `**Q:** ...\n\n**A (Claude):** ...`;
+ // split so we can style them distinctly.
+ let (q, a) = if is_ask {
+ split_ask_body(&entry.body)
+ } else {
+ (None, Some(entry.body.clone()))
+ };
+
+ view! {
+
+
+ {q.map(|s| view! { {s}
})}
+ {a.map(|s| view! { {s}
})}
+
+ }
+}
+
+fn split_ask_body(body: &str) -> (Option, Option) {
+ let mut q = None;
+ let mut a = None;
+ if let Some(q_rest) = body.strip_prefix("**Q:**") {
+ if let Some((q_part, a_part)) = q_rest.split_once("**A (Claude):**") {
+ q = Some(q_part.trim().to_string());
+ a = Some(a_part.trim().to_string());
+ } else {
+ q = Some(q_rest.trim().to_string());
+ }
+ } else {
+ a = Some(body.trim().to_string());
+ }
+ (q, a)
+}
+
+// --- Author (inline, no modal prompt) ---------------------------------
+
+fn initial_author() -> String {
+ #[cfg(target_arch = "wasm32")]
+ {
+ if let Some(win) = web_sys::window() {
+ if let Ok(Some(storage)) = win.local_storage() {
+ if let Ok(Some(name)) = storage.get_item("mantra.author") {
+ if !name.trim().is_empty() {
+ return name;
+ }
+ }
+ }
+ }
+ }
+ String::new()
+}
+
+fn persist_author(name: &str) {
+ #[cfg(target_arch = "wasm32")]
+ {
+ if let Some(win) = web_sys::window() {
+ if let Ok(Some(storage)) = win.local_storage() {
+ let _ = storage.set_item("mantra.author", name);
+ }
+ }
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ { let _ = name; }
+}
+
+// --- Textarea helpers -------------------------------------------------
+
+fn clear_textarea(node: &NodeRef) {
+ #[cfg(target_arch = "wasm32")]
+ {
+ if let Some(el) = node.get_untracked() {
+ el.set_value("");
+ }
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ { let _ = node; }
+}
+
+// --- Debug log --------------------------------------------------------
+
+fn log(msg: &str) {
+ #[cfg(target_arch = "wasm32")]
+ {
+ web_sys::console::log_1(&format!("[mantra] {msg}").into());
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ { let _ = msg; }
+}
diff --git a/crates/mantra-ui/src/pages/mod.rs b/crates/mantra-ui/src/pages/mod.rs
new file mode 100644
index 0000000..026aad8
--- /dev/null
+++ b/crates/mantra-ui/src/pages/mod.rs
@@ -0,0 +1,5 @@
+pub mod landing;
+pub mod margin;
+pub mod shared;
+pub mod source;
+pub mod theme;
diff --git a/crates/mantra-ui/src/pages/shared.rs b/crates/mantra-ui/src/pages/shared.rs
new file mode 100644
index 0000000..c70c85f
--- /dev/null
+++ b/crates/mantra-ui/src/pages/shared.rs
@@ -0,0 +1,37 @@
+//! Shared UI atoms.
+
+use leptos::prelude::*;
+use leptos_router::hooks::{use_location, use_query_map};
+
+use crate::corpus::Lang;
+
+/// Minimal top-right language toggle. Two links; current is dimmed,
+/// the other is hot. Preserves the rest of the URL (path + remaining
+/// query params) so toggling on a deep page keeps you there.
+#[component]
+pub fn LangToggle(current: Memo) -> impl IntoView {
+ let location = use_location();
+ let query = use_query_map();
+
+ let make_href = move |target: Lang| {
+ let path = location.pathname.get();
+ let mut params = query.get();
+ params.replace("lang", target.as_str().to_string());
+ let qs = params.to_query_string();
+ if qs.is_empty() { path } else { format!("{path}{qs}") }
+ };
+
+ view! {
+
+ "ru"
+ "·"
+ "en"
+
+ }
+}
diff --git a/crates/mantra-ui/src/pages/source.rs b/crates/mantra-ui/src/pages/source.rs
new file mode 100644
index 0000000..3b04a82
--- /dev/null
+++ b/crates/mantra-ui/src/pages/source.rs
@@ -0,0 +1,194 @@
+//! `/source/:slug` — one distillation, book-page typography.
+//!
+//! Reads its data via the `source_page` server fn wrapped in a
+//! `Resource` so SSR and hydrate render from the same payload.
+//! This avoids needing the corpus `Arc` context on the
+//! client (where it isn't available).
+
+use leptos::prelude::*;
+use leptos_router::hooks::{use_params, use_query_map};
+use leptos_router::params::Params;
+
+use crate::api::{fetch_source_page, SourcePageData};
+use crate::corpus::Lang;
+use crate::pages::margin::{ActivePara, MarginDrawer};
+use crate::pages::shared::LangToggle;
+
+#[derive(Params, PartialEq, Clone, Debug)]
+struct SlugParam {
+ slug: Option,
+}
+
+#[component]
+pub fn SourcePage() -> impl IntoView {
+ let params = use_params::();
+ let slug = Memo::new(move |_| {
+ params.get().ok().and_then(|p| p.slug).unwrap_or_default()
+ });
+
+ let query = use_query_map();
+ let lang = Memo::new(move |_| {
+ Lang::from_query(query.read().get("lang").as_deref())
+ });
+
+ let (active, set_active) = signal::>(None);
+ let notes_tick = RwSignal::new(0u32);
+
+ let slug_sig: Signal = Signal::derive(move || slug.get());
+ let lang_sig: Signal = Signal::derive(move || lang.get());
+
+ // Resource key = (slug, lang). Re-fetches when either changes.
+ let data = Resource::new(
+ move || (slug.get(), lang.get().as_str().to_string()),
+ |(s, l)| fetch_source_page(s, l),
+ );
+
+ // After hydration + notes load: annotate paragraphs that have
+ // marginalia so a dot appears in the left gutter.
+ #[cfg(feature = "hydrate")]
+ {
+ let slug_for_fx = slug_sig;
+ Effect::new(move |_| {
+ let _ = notes_tick.get();
+ let current_slug = slug_for_fx.get();
+ if current_slug.is_empty() { return; }
+ leptos::task::spawn_local(async move {
+ if let Ok(entries) = crate::api::fetch_notes(current_slug).await {
+ let ids: std::collections::HashSet =
+ entries.into_iter().map(|e| e.para_id).collect();
+ annotate_notes(&ids);
+ }
+ });
+ });
+ }
+
+ view! {
+
+ "…"
}>
+ {move || {
+ data.get().map(|res| {
+ let lang_val = lang.get();
+ let home_href = format!("/?lang={}", lang_val.as_str());
+ let breadcrumb_cycle = match lang_val {
+ Lang::Ru => "цикл 1",
+ Lang::En => "cycle 1",
+ };
+ let label_all = match lang_val {
+ Lang::Ru => "все источники",
+ Lang::En => "all sources",
+ };
+ let label_prev = match lang_val {
+ Lang::Ru => "← предыдущий",
+ Lang::En => "← prev",
+ };
+ let label_next = match lang_val {
+ Lang::Ru => "следующий →",
+ Lang::En => "next →",
+ };
+
+ match res {
+ Ok(Some(d)) => {
+ let SourcePageData { source: src, prev, next } = d;
+ let prev_href = prev.map(|p| format!("/source/{p}?lang={}", lang_val.as_str()));
+ let next_href = next.map(|n| format!("/source/{n}?lang={}", lang_val.as_str()));
+ view! {
+
+
+ "design dna"
+ " · "
+ {breadcrumb_cycle}
+ " · "
+ {src.author}
+
+
+
+
+
+
+ }.into_any()
+ }
+ Ok(None) => view! {
+
+ "source not found"
+ "← back"
+
+ }.into_any(),
+ Err(e) => view! {
+
+ {format!("{e}")}
+
+ }.into_any(),
+ }
+ })
+ }}
+
+
+
+ }
+}
+
+/// Climb up from the click target to find the nearest `[data-para-id]`
+/// element; return its id + plain-text content to seed the drawer.
+#[cfg(feature = "hydrate")]
+fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option {
+ use wasm_bindgen::JsCast;
+ let target = ev.target()?;
+ let mut el = target.dyn_into::().ok()?;
+ loop {
+ if el.has_attribute("data-para-id") {
+ let id = el.get_attribute("data-para-id")?;
+ let text = el.text_content().unwrap_or_default();
+ let excerpt = text.trim().to_string();
+ return Some(ActivePara { id, excerpt });
+ }
+ el = el.parent_element()?;
+ }
+}
+
+#[cfg(not(feature = "hydrate"))]
+fn paragraph_from_event(_ev: &leptos::ev::MouseEvent) -> Option {
+ None
+}
+
+/// Add `class="has-notes"` to every paragraph that has at least one
+/// marginalia entry. Called after notes fetch.
+#[cfg(feature = "hydrate")]
+fn annotate_notes(ids: &std::collections::HashSet) {
+ use wasm_bindgen::JsCast;
+ let Some(win) = web_sys::window() else { return };
+ let Some(doc) = win.document() else { return };
+ let Ok(nodes) = doc.query_selector_all("[data-para-id]") else { return };
+ for i in 0..nodes.length() {
+ let Some(node) = nodes.item(i) else { continue };
+ let Ok(el) = node.dyn_into::() else { continue };
+ let id = el.get_attribute("data-para-id").unwrap_or_default();
+ let classes = el.class_list();
+ if ids.contains(&id) {
+ let _ = classes.add_1("has-notes");
+ } else {
+ let _ = classes.remove_1("has-notes");
+ }
+ }
+}
diff --git a/crates/mantra-ui/src/pages/theme.rs b/crates/mantra-ui/src/pages/theme.rs
new file mode 100644
index 0000000..4c53e80
--- /dev/null
+++ b/crates/mantra-ui/src/pages/theme.rs
@@ -0,0 +1,117 @@
+//! `/theme/:slug` — one running theme, with contributing sources.
+
+use leptos::prelude::*;
+use leptos_router::hooks::{use_params, use_query_map};
+use leptos_router::params::Params;
+
+use crate::api::{fetch_theme_page, ThemePageData};
+use crate::corpus::{Lang, Source};
+use crate::pages::shared::LangToggle;
+
+#[derive(Params, PartialEq, Clone, Debug)]
+struct SlugParam {
+ slug: Option,
+}
+
+#[component]
+pub fn ThemePage() -> impl IntoView {
+ let params = use_params::();
+ let slug = Memo::new(move |_| {
+ params.get().ok().and_then(|p| p.slug).unwrap_or_default()
+ });
+
+ let query = use_query_map();
+ let lang = Memo::new(move |_| {
+ Lang::from_query(query.read().get("lang").as_deref())
+ });
+
+ let data = Resource::new(
+ move || (slug.get(), lang.get().as_str().to_string()),
+ |(s, l)| fetch_theme_page(s, l),
+ );
+
+ view! {
+
+ "…" }>
+ {move || {
+ data.get().map(|res| {
+ let lang_val = lang.get();
+ let home_href = format!("/?lang={}", lang_val.as_str());
+ let label_back = match lang_val {
+ Lang::Ru => "← все темы",
+ Lang::En => "← all themes",
+ };
+ let label_sources = match lang_val {
+ Lang::Ru => "источники",
+ Lang::En => "sources",
+ };
+ let theme_label = match lang_val {
+ Lang::Ru => "тема",
+ Lang::En => "theme",
+ };
+
+ match res {
+ Ok(Some(ThemePageData { theme, contributing })) => view! {
+
+
+ "design dna"
+ " · "
+ {theme_label}
+
+ {theme.title}
+
+
+
+
+ }.into_any(),
+ Ok(None) => view! {
+
+ "theme not found"
+ "← back"
+
+ }.into_any(),
+ Err(e) => view! {
+
+ {format!("{e}")}
+
+ }.into_any(),
+ }
+ })
+ }}
+
+ }
+}
+
+#[component]
+fn ContributingLine(source: Source, lang: Lang) -> impl IntoView {
+ let href = format!("/source/{}?lang={}", source.slug, lang.as_str());
+ view! {
+
+
+ {source.author}
+ " · "
+ {source.title}
+ {source.core_claim}
+
+
+ }
+}
diff --git a/sass/main.scss b/sass/main.scss
new file mode 100644
index 0000000..9edb808
--- /dev/null
+++ b/sass/main.scss
@@ -0,0 +1,756 @@
+// mantra · book-grade reader typography
+// Fraunces (variable) body + IBM Plex Sans (labels) + IBM Plex Mono (non-Latin)
+// Pergament-ink light · deep-ink dark · 62ch measure
+// Headings H2 as margin-labels on wide, inline-before on mobile
+
+// --- Fonts (self-hosted) ---------------------------------------------
+
+@font-face {
+ font-family: "Fraunces";
+ src: url("/fonts/fraunces-variable.woff2") format("woff2-variations");
+ font-weight: 100 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Fraunces";
+ src: url("/fonts/fraunces-italic-variable.woff2") format("woff2-variations");
+ font-weight: 100 900;
+ font-style: italic;
+ font-display: swap;
+}
+@font-face {
+ font-family: "IBM Plex Sans";
+ src: url("/fonts/plex-sans-variable.woff2") format("woff2-variations");
+ font-weight: 100 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: "IBM Plex Mono";
+ src: url("/fonts/plex-mono-variable.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+// --- Color tokens ----------------------------------------------------
+
+:root {
+ // Pergament (light)
+ --mantra-bg: #fdfbf5;
+ --mantra-fg: #1c1917;
+ --mantra-muted: #78716c;
+ --mantra-faint: #d6d3d0;
+ --mantra-accent: #5e4b3a;
+ --mantra-hairline: rgba(28, 25, 23, 0.08);
+
+ --mantra-measure: 62ch;
+ --mantra-serif: "Fraunces", Georgia, "Times New Roman", serif;
+ --mantra-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
+ --mantra-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ --mantra-bg: #0f1012;
+ --mantra-fg: #e7e3dd;
+ --mantra-muted: #8a857d;
+ --mantra-faint: #2a2826;
+ --mantra-accent: #c9b999;
+ --mantra-hairline: rgba(231, 227, 221, 0.10);
+ }
+}
+
+// --- Reset + base ----------------------------------------------------
+
+* { box-sizing: border-box; }
+
+::selection {
+ background: var(--mantra-accent);
+ color: var(--mantra-bg);
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--mantra-bg);
+ color: var(--mantra-fg);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+}
+
+body {
+ font-family: var(--mantra-serif);
+ font-size: 19px;
+ line-height: 1.68;
+ letter-spacing: 0.002em;
+ // Variable-axis tuning: a warmer serif without eccentricity
+ font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 400;
+ font-feature-settings: "kern" 1, "liga" 1, "calt" 1, "onum" 1;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+ border-bottom: 1px solid var(--mantra-hairline);
+ transition: border-color 0.2s, color 0.2s;
+}
+a:hover { border-bottom-color: var(--mantra-accent); color: var(--mantra-accent); }
+
+main {
+ max-width: var(--mantra-measure);
+ margin: 0 auto;
+ padding: 6rem 1.5rem 8rem;
+}
+@media (max-width: 640px) {
+ main { padding: 3rem 1.25rem 5rem; }
+ body { font-size: 18px; line-height: 1.65; }
+}
+
+// --- Landing --------------------------------------------------------
+
+.landing {
+ max-width: 72ch;
+}
+
+.landing-head { margin-bottom: 4rem; }
+
+.landing-title {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 144, "SOFT" 50, "WONK" 0, "wght" 350;
+ font-size: clamp(3rem, 7vw, 4.5rem);
+ font-weight: 350;
+ line-height: 1.02;
+ letter-spacing: -0.02em;
+ margin: 0;
+ color: var(--mantra-fg);
+}
+
+.landing-subtitle {
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 400;
+ font-size: 0.78rem;
+ color: var(--mantra-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.22em;
+ margin: 1rem 0 3rem;
+}
+
+.landing-question {
+ font-family: var(--mantra-serif);
+ font-style: italic;
+ font-variation-settings: "opsz" 36, "SOFT" 50, "WONK" 0, "wght" 380;
+ font-size: 1.4rem;
+ line-height: 1.4;
+ color: var(--mantra-fg);
+ margin: 0 0 4rem;
+ max-width: 34ch;
+}
+
+.landing-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 3.5rem;
+}
+@media (max-width: 760px) {
+ .landing-grid { grid-template-columns: 1fr; gap: 3rem; }
+}
+
+.landing-col-label {
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 500;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.28em;
+ color: var(--mantra-muted);
+ margin: 0 0 1.75rem;
+}
+
+.works-list, .themes-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.work-line {
+ a {
+ display: block;
+ border-bottom: none;
+ line-height: 1.35;
+ }
+ a:hover { color: var(--mantra-fg); }
+ a:hover .work-author { color: var(--mantra-accent); }
+}
+.work-author {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 500;
+ color: var(--mantra-fg);
+}
+.work-sep { color: var(--mantra-faint); }
+.work-title {
+ font-style: italic;
+ color: var(--mantra-muted);
+}
+.work-claim {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 14, "SOFT" 30, "WONK" 0, "wght" 380;
+ font-size: 0.88rem;
+ color: var(--mantra-muted);
+ line-height: 1.5;
+ margin-top: 0.3rem;
+}
+
+.theme-line a {
+ display: block;
+ border-bottom: none;
+}
+.theme-title {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 500;
+}
+.theme-count {
+ font-family: var(--mantra-sans);
+ color: var(--mantra-muted);
+ font-size: 0.82rem;
+ letter-spacing: 0.02em;
+}
+
+// --- Source page ----------------------------------------------------
+
+.source-breadcrumb {
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 450;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.22em;
+ color: var(--mantra-muted);
+ margin-bottom: 3rem;
+ .sep { margin: 0 0.5em; color: var(--mantra-faint); }
+ a { color: var(--mantra-muted); border-bottom: none; }
+ a:hover { color: var(--mantra-fg); }
+}
+
+.source-body {
+ // Positioning anchor for H2 margin-labels below.
+ position: relative;
+
+ // H1 is the work title
+ h1 {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 96, "SOFT" 50, "WONK" 0, "wght" 380;
+ font-size: clamp(2rem, 5vw, 2.75rem);
+ line-height: 1.1;
+ letter-spacing: -0.012em;
+ margin: 0 0 2.5rem;
+ color: var(--mantra-fg);
+ }
+
+ // H2 as margin-label on wide screens (pulled into left margin)
+ h2 {
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 500;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.24em;
+ color: var(--mantra-muted);
+ font-weight: 500;
+ margin: 3.5rem 0 1.25rem;
+ position: relative;
+ }
+ @media (min-width: 1100px) {
+ h2 {
+ position: absolute;
+ // Pull the label into the left gutter, outside the article box.
+ // 16ch for the label + 2.5rem breathing room before the text
+ // column (which starts at the article's padding-left, i.e. 0).
+ left: calc(-16ch - 2.5rem);
+ margin: 0;
+ padding-top: 0.5rem;
+ width: 16ch;
+ text-align: right;
+ }
+ // Insert spacing where H2 would have been inline
+ h2 + * { margin-top: 3rem; }
+ }
+
+ h3 {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 24, "SOFT" 40, "WONK" 0, "wght" 520;
+ font-size: 1.12rem;
+ letter-spacing: -0.005em;
+ margin: 2.2rem 0 0.8rem;
+ color: var(--mantra-fg);
+ }
+
+ p {
+ margin: 0 0 1.35rem;
+ hyphens: auto;
+ -webkit-hyphens: auto;
+ }
+
+ em, i {
+ font-style: italic;
+ font-variation-settings: "opsz" 18, "SOFT" 40, "WONK" 0, "wght" 400;
+ }
+ strong, b {
+ font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 600;
+ }
+
+ a {
+ border-bottom: 1px solid var(--mantra-muted);
+ }
+ a:hover { border-bottom-color: var(--mantra-accent); }
+
+ blockquote {
+ border-left: 2px solid var(--mantra-accent);
+ margin: 2rem 0 2rem -0.5rem;
+ padding: 0.3rem 0 0.3rem 1.5rem;
+ font-style: italic;
+ font-variation-settings: "opsz" 17, "SOFT" 50, "WONK" 0, "wght" 400;
+ color: var(--mantra-fg);
+ font-size: 0.98rem;
+ p { margin-bottom: 0.6rem; &:last-child { margin-bottom: 0; } }
+ }
+
+ code {
+ font-family: var(--mantra-mono);
+ font-size: 0.85em;
+ background: var(--mantra-faint);
+ padding: 0.1rem 0.35rem;
+ border-radius: 3px;
+ }
+
+ pre {
+ font-family: var(--mantra-mono);
+ font-size: 0.82rem;
+ background: var(--mantra-faint);
+ padding: 1rem 1.25rem;
+ border-radius: 6px;
+ overflow-x: auto;
+ margin: 1.75rem 0;
+ line-height: 1.5;
+ code { background: none; padding: 0; font-size: inherit; }
+ }
+
+ ul, ol {
+ margin: 0 0 1.35rem;
+ padding-left: 1.5rem;
+ }
+ li { margin: 0.3rem 0; }
+
+ hr {
+ border: none;
+ border-top: 1px solid var(--mantra-hairline);
+ margin: 3rem auto;
+ width: 12ch;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.9rem;
+ margin: 1.5rem 0;
+ th, td {
+ padding: 0.5rem 0.8rem;
+ border-bottom: 1px solid var(--mantra-hairline);
+ text-align: left;
+ vertical-align: top;
+ }
+ th {
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 500;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: var(--mantra-muted);
+ }
+ }
+
+ // Paragraph click-target + margin dot (M.3 margin-layer).
+ // - Empty paragraphs: faint dot appears only on hover (affordance).
+ // - Annotated paragraphs (.has-notes): dot is always visible in the
+ // accent colour, so readers see at a glance where the marginalia is.
+ p[data-para-id] {
+ position: relative;
+ cursor: pointer;
+ }
+ @media (min-width: 900px) {
+ p[data-para-id]::before {
+ content: "";
+ position: absolute;
+ left: -1.25rem;
+ top: 0.75em;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--mantra-faint);
+ opacity: 0;
+ transition: opacity 0.2s, background 0.2s, transform 0.2s;
+ }
+ p[data-para-id]:hover::before {
+ opacity: 1;
+ background: var(--mantra-muted);
+ }
+ p[data-para-id].has-notes::before {
+ opacity: 1;
+ background: var(--mantra-accent);
+ }
+ p[data-para-id].has-notes:hover::before {
+ transform: scale(1.3);
+ }
+ }
+}
+
+.source-foot {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 5rem;
+ padding-top: 2rem;
+ border-top: 1px solid var(--mantra-hairline);
+ font-family: var(--mantra-sans);
+ font-size: 0.78rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--mantra-muted);
+
+ a { border-bottom: none; color: var(--mantra-muted); }
+ a:hover { color: var(--mantra-fg); }
+
+ .nav-prev { justify-self: start; }
+ .nav-home { justify-self: center; }
+ .nav-next { justify-self: end; }
+}
+
+// --- Theme page -----------------------------------------------------
+
+.theme-h1 {
+ font-family: var(--mantra-serif);
+ font-variation-settings: "opsz" 72, "SOFT" 50, "WONK" 0, "wght" 380;
+ font-size: clamp(2rem, 4.5vw, 2.75rem);
+ line-height: 1.1;
+ letter-spacing: -0.01em;
+ margin: 0 0 2rem;
+ color: var(--mantra-fg);
+}
+
+.theme-desc {
+ margin-bottom: 3rem;
+ p { color: var(--mantra-fg); margin-bottom: 1.2rem; }
+}
+
+.theme-contributing { margin-top: 3rem; }
+
+// --- Misc -----------------------------------------------------------
+
+// --- Language toggle (top-right) ---
+.lang-toggle {
+ position: fixed;
+ top: 1.25rem;
+ right: 1.5rem;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-family: var(--mantra-sans);
+ font-variation-settings: "wght" 450;
+ font-size: 0.78rem;
+ letter-spacing: 0.1em;
+
+ a {
+ color: var(--mantra-faint);
+ text-decoration: none;
+ border-bottom: none;
+ padding: 0.1rem 0.1rem;
+ transition: color 0.15s;
+ }
+ a:hover { color: var(--mantra-fg); }
+ a.lang-active {
+ color: var(--mantra-fg);
+ font-variation-settings: "wght" 600;
+ }
+ .lang-dot {
+ color: var(--mantra-faint);
+ }
+}
+@media (max-width: 640px) {
+ .lang-toggle { top: 1rem; right: 1rem; font-size: 0.72rem; }
+}
+
+.err {
+ color: #b45309;
+ font-family: var(--mantra-mono);
+ font-size: 0.88rem;
+}
+.not-found { padding: 2rem 0; }
+.back-link {
+ color: var(--mantra-muted);
+ font-family: var(--mantra-sans);
+ font-size: 0.85rem;
+ border-bottom: none;
+}
+
+// --- Margin drawer (M.3) --------------------------------------------
+
+.margin-drawer {
+ position: fixed;
+ top: 0;
+ right: 0;
+ max-height: 100vh;
+ width: min(420px, 90vw);
+ background: var(--mantra-bg);
+ border-left: 1px solid var(--mantra-hairline);
+ box-shadow: -12px 0 32px rgba(0, 0, 0, 0.04);
+ z-index: 20;
+ display: flex;
+ flex-direction: column;
+ font-family: var(--mantra-sans);
+ font-size: 0.92rem;
+ color: var(--mantra-fg);
+ animation: drawer-in 0.22s ease-out;
+ overflow: hidden;
+}
+
+@keyframes drawer-in {
+ from { transform: translateX(16px); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+.margin-drawer-head {
+ position: relative;
+ padding: 0.9rem 1.1rem 0.7rem;
+ border-bottom: 1px solid var(--mantra-hairline);
+ text-align: center;
+}
+
+.margin-drawer-tabs {
+ display: inline-flex;
+ gap: 0.2rem;
+
+ .tab {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ font-family: var(--mantra-sans);
+ font-size: 0.8rem;
+ font-variation-settings: "wght" 450;
+ letter-spacing: 0.08em;
+ text-transform: lowercase;
+ color: var(--mantra-muted);
+ padding: 0.3rem 0.9rem;
+ border-radius: 14px;
+ transition: color 0.15s, background 0.15s;
+
+ &:hover { color: var(--mantra-fg); }
+ &.tab-active {
+ color: var(--mantra-bg);
+ background: var(--mantra-accent);
+ font-variation-settings: "wght" 600;
+ }
+ }
+}
+
+.margin-drawer-close {
+ position: absolute;
+ top: 0.55rem;
+ right: 0.9rem;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ font-family: var(--mantra-serif);
+ font-size: 1.5rem;
+ line-height: 1;
+ color: var(--mantra-muted);
+ padding: 0.2rem 0.45rem;
+ transition: color 0.15s;
+ &:hover { color: var(--mantra-fg); }
+}
+
+.margin-drawer-excerpt {
+ padding: 0.8rem 1.1rem;
+ font-family: var(--mantra-serif);
+ font-style: italic;
+ font-size: 0.88rem;
+ line-height: 1.5;
+ color: var(--mantra-muted);
+ border-bottom: 1px solid var(--mantra-hairline);
+ max-height: 6.5em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+}
+
+// Entries sit below the form (reading flows downward: write → record).
+// They scroll within their own area when long; when empty, they add
+// minimal height so the drawer collapses to just head+excerpt+form.
+.margin-drawer-entries {
+ flex: 0 1 auto;
+ max-height: 42vh;
+ overflow-y: auto;
+ padding: 0.6rem 1.1rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+ border-top: 1px solid var(--mantra-hairline);
+
+ &:empty { display: none; }
+}
+
+.margin-empty {
+ color: var(--mantra-muted);
+ font-style: italic;
+ font-family: var(--mantra-serif);
+ font-size: 0.9rem;
+ margin: 1.2rem 0;
+ text-align: center;
+}
+
+.note-card {
+ border-left: 2px solid var(--mantra-hairline);
+ padding: 0.1rem 0 0.1rem 0.8rem;
+
+ &.note-card-ask { border-left-color: var(--mantra-accent); }
+
+ .note-card-meta {
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ color: var(--mantra-muted);
+ margin-bottom: 0.3rem;
+ font-variation-settings: "wght" 500;
+ }
+ .note-q {
+ font-family: var(--mantra-serif);
+ font-style: italic;
+ font-size: 0.94rem;
+ line-height: 1.5;
+ margin: 0 0 0.5rem;
+ color: var(--mantra-fg);
+ }
+ .note-a {
+ font-family: var(--mantra-serif);
+ font-size: 0.94rem;
+ line-height: 1.55;
+ margin: 0;
+ color: var(--mantra-fg);
+ white-space: pre-wrap;
+ }
+}
+
+.margin-drawer-form {
+ padding: 0.9rem 1.1rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+}
+
+.margin-drawer-input {
+ width: 100%;
+ resize: vertical;
+ min-height: 4.5rem;
+ background: var(--mantra-bg);
+ color: var(--mantra-fg);
+ border: 1px solid var(--mantra-hairline);
+ border-radius: 4px;
+ padding: 0.55rem 0.7rem;
+ font-family: var(--mantra-serif);
+ font-size: 0.95rem;
+ line-height: 1.45;
+ transition: border-color 0.15s;
+ &:focus {
+ outline: none;
+ border-color: var(--mantra-accent);
+ }
+}
+
+.margin-drawer-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.margin-drawer-author {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ flex: 1;
+ min-width: 0;
+
+ .margin-drawer-author-label {
+ font-size: 0.72rem;
+ color: var(--mantra-muted);
+ letter-spacing: 0.06em;
+ text-transform: lowercase;
+ font-variation-settings: "wght" 500;
+ }
+ .margin-drawer-author-input {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: none;
+ border-bottom: 1px dashed var(--mantra-faint);
+ font-family: var(--mantra-sans);
+ font-size: 0.82rem;
+ color: var(--mantra-fg);
+ padding: 0.1rem 0 0.15rem;
+ outline: none;
+ transition: border-color 0.15s;
+ &:focus { border-bottom-color: var(--mantra-accent); }
+ &::placeholder { color: var(--mantra-faint); }
+ }
+}
+
+.margin-drawer-submit {
+ background: var(--mantra-accent);
+ color: var(--mantra-bg);
+ border: none;
+ border-radius: 14px;
+ padding: 0.45rem 1.1rem;
+ font-family: var(--mantra-sans);
+ font-size: 0.82rem;
+ font-variation-settings: "wght" 600;
+ letter-spacing: 0.06em;
+ text-transform: lowercase;
+ cursor: pointer;
+ transition: opacity 0.15s, transform 0.12s;
+
+ &:hover:not(:disabled) { transform: translateY(-1px); }
+ &:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+}
+
+.margin-drawer-error {
+ color: #b45309;
+ font-family: var(--mantra-mono);
+ font-size: 0.78rem;
+ margin: 0;
+ word-break: break-word;
+}
+
+// Mobile: drawer becomes a bottom sheet
+@media (max-width: 720px) {
+ .margin-drawer {
+ top: auto;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ width: 100vw;
+ max-height: 75vh;
+ border-left: none;
+ border-top: 1px solid var(--mantra-hairline);
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
+ animation: drawer-up 0.22s ease-out;
+ }
+ @keyframes drawer-up {
+ from { transform: translateY(16px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+ }
+}
diff --git a/static/fonts/fraunces-italic-variable.woff2 b/static/fonts/fraunces-italic-variable.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..1898b02129d4f8c1e0d3f95034afec8bf8b16a51
GIT binary patch
literal 45656
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