diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f4e43e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/site/target/
+/site/dist/
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..5715dfe
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,48 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1789286504,
+ "narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1789457514,
+ "narHash": "sha256-Aggle++fTyAifBy+QBPxjM+obO5iepKW/8MDxQtgGvI=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "89e99bf0778a8f2cd18c9360c3f19c1ee47fc739",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..fb637b6
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,54 @@
+{
+ description = "English reading lessons with a Leptos site";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = { nixpkgs, rust-overlay, ... }:
+ let
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "x86_64-darwin"
+ "aarch64-darwin"
+ ];
+
+ forEachSystem = f: nixpkgs.lib.genAttrs systems f;
+ in
+ {
+ devShells = forEachSystem (system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ (import rust-overlay) ];
+ };
+
+ rustToolchain = pkgs.rust-bin.stable.latest.default.override {
+ targets = [ "wasm32-unknown-unknown" ];
+ };
+ in
+ {
+ default = pkgs.mkShell {
+ packages = [
+ rustToolchain
+ pkgs.trunk
+ pkgs.wasm-bindgen-cli
+ pkgs.binaryen
+ pkgs.pkg-config
+ pkgs.git
+ ];
+
+ RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
+
+ shellHook = ''
+ echo "Leptos shell ready. Run: trunk serve --open"
+ '';
+ };
+ });
+ };
+}
diff --git a/site/Cargo.lock b/site/Cargo.lock
new file mode 100644
index 0000000..970575b
--- /dev/null
+++ b/site/Cargo.lock
@@ -0,0 +1,1785 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "any_spawner"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b"
+dependencies = [
+ "futures",
+ "thiserror 2.0.20",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[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-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[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 2.0.119",
+]
+
+[[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 2.0.119",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "camino"
+version = "1.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[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.20",
+]
+
+[[package]]
+name = "collection_literals"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+
+[[package]]
+name = "config"
+version = "0.15.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139"
+dependencies = [
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde_core",
+ "toml",
+ "winnow",
+]
+
+[[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.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "dashmap"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[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 2.0.119",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "drain_filter_polyfill"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "either"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "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 = "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.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+]
+
+[[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 = "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"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "html-escape"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "hydration_context"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a"
+dependencies = [
+ "futures",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
+]
+
+[[package]]
+name = "iana-reading"
+version = "0.1.0"
+dependencies = [
+ "leptos",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
+[[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.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "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 = "leptos"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1"
+dependencies = [
+ "any_spawner",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "rustc-hash",
+ "send_wrapper",
+ "serde",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.20",
+ "throw_error",
+ "typed-builder",
+ "typed-builder-macro",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_config"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03"
+dependencies = [
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.20",
+ "typed-builder",
+]
+
+[[package]]
+name = "leptos_dom"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d"
+dependencies = [
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_hot_reload"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e"
+dependencies = [
+ "anyhow",
+ "camino",
+ "indexmap",
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn 2.0.119",
+ "walkdir",
+]
+
+[[package]]
+name = "leptos_macro"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd"
+dependencies = [
+ "attribute-derive",
+ "cfg-if",
+ "convert_case 0.7.1",
+ "html-escape",
+ "itertools",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "server_fn_macro",
+ "syn 2.0.119",
+ "uuid",
+]
+
+[[package]]
+name = "leptos_server"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11"
+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.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "linear-map"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[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 2.0.119",
+]
+
+[[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 = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "next_tuple"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+
+[[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.20",
+]
+
+[[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.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[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 2.0.119",
+]
+
+[[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.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+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 2.0.119",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+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 2.0.119",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "reactive_graph"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6"
+dependencies = [
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "or_poisoned",
+ "pin-project-lite",
+ "rustc-hash",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.20",
+ "web-sys",
+]
+
+[[package]]
+name = "reactive_stores"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237"
+dependencies = [
+ "guardian",
+ "itertools",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+]
+
+[[package]]
+name = "reactive_stores_macro"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d"
+dependencies = [
+ "convert_case 0.7.1",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[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.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[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 2.0.119",
+ "syn_derive",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[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 = "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.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[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 = "server_fn"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6"
+dependencies = [
+ "bytes",
+ "const_format",
+ "dashmap",
+ "futures",
+ "gloo-net",
+ "http",
+ "js-sys",
+ "once_cell",
+ "pin-project-lite",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.20",
+ "throw_error",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052"
+dependencies = [
+ "const_format",
+ "convert_case 0.6.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568"
+dependencies = [
+ "server_fn_macro",
+ "syn 2.0.119",
+]
+
+[[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.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
+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 2.0.119",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tachys"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec"
+dependencies = [
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "dyn-clone",
+ "either_of",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools",
+ "js-sys",
+ "linear-map",
+ "next_tuple",
+ "oco_ref",
+ "once_cell",
+ "or_poisoned",
+ "parking_lot",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl 2.0.20",
+]
+
+[[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 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "throw_error"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
+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.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "typed-builder"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
+dependencies = [
+ "getrandom",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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 = "wasm-bindgen"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.128"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[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 = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+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 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+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 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/site/Cargo.toml b/site/Cargo.toml
new file mode 100644
index 0000000..d1608da
--- /dev/null
+++ b/site/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "iana-reading"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.7", features = ["csr"] }
+web-sys = { version = "0.3", features = ["HtmlAudioElement", "HtmlInputElement"] }
+wasm-bindgen-futures = "0.4"
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..18f8219
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+ Читаем по правилам — 01
+
+
+
+
+
diff --git a/site/public/audio/.gitkeep b/site/public/audio/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/site/public/audio/and.mp3 b/site/public/audio/and.mp3
new file mode 100644
index 0000000..46626a4
Binary files /dev/null and b/site/public/audio/and.mp3 differ
diff --git a/site/public/audio/at.mp3 b/site/public/audio/at.mp3
new file mode 100644
index 0000000..01f4ed4
Binary files /dev/null and b/site/public/audio/at.mp3 differ
diff --git a/site/public/audio/bag.mp3 b/site/public/audio/bag.mp3
new file mode 100644
index 0000000..107d4dc
Binary files /dev/null and b/site/public/audio/bag.mp3 differ
diff --git a/site/public/audio/bags.mp3 b/site/public/audio/bags.mp3
new file mode 100644
index 0000000..32c85cf
Binary files /dev/null and b/site/public/audio/bags.mp3 differ
diff --git a/site/public/audio/ben.mp3 b/site/public/audio/ben.mp3
new file mode 100644
index 0000000..37ef84c
Binary files /dev/null and b/site/public/audio/ben.mp3 differ
diff --git a/site/public/audio/big.mp3 b/site/public/audio/big.mp3
new file mode 100644
index 0000000..4d8dd0b
Binary files /dev/null and b/site/public/audio/big.mp3 differ
diff --git a/site/public/audio/bike.mp3 b/site/public/audio/bike.mp3
new file mode 100644
index 0000000..85cb006
Binary files /dev/null and b/site/public/audio/bike.mp3 differ
diff --git a/site/public/audio/bikes.mp3 b/site/public/audio/bikes.mp3
new file mode 100644
index 0000000..755d4e9
Binary files /dev/null and b/site/public/audio/bikes.mp3 differ
diff --git a/site/public/audio/black.mp3 b/site/public/audio/black.mp3
new file mode 100644
index 0000000..74fcd46
Binary files /dev/null and b/site/public/audio/black.mp3 differ
diff --git a/site/public/audio/bright.mp3 b/site/public/audio/bright.mp3
new file mode 100644
index 0000000..acb8884
Binary files /dev/null and b/site/public/audio/bright.mp3 differ
diff --git a/site/public/audio/but.mp3 b/site/public/audio/but.mp3
new file mode 100644
index 0000000..ad6f8ce
Binary files /dev/null and b/site/public/audio/but.mp3 differ
diff --git a/site/public/audio/by.mp3 b/site/public/audio/by.mp3
new file mode 100644
index 0000000..1fbcc55
Binary files /dev/null and b/site/public/audio/by.mp3 differ
diff --git a/site/public/audio/camp.mp3 b/site/public/audio/camp.mp3
new file mode 100644
index 0000000..e5f9aa9
Binary files /dev/null and b/site/public/audio/camp.mp3 differ
diff --git a/site/public/audio/can.mp3 b/site/public/audio/can.mp3
new file mode 100644
index 0000000..3529a94
Binary files /dev/null and b/site/public/audio/can.mp3 differ
diff --git a/site/public/audio/chat.mp3 b/site/public/audio/chat.mp3
new file mode 100644
index 0000000..7eb91ae
Binary files /dev/null and b/site/public/audio/chat.mp3 differ
diff --git a/site/public/audio/chip.mp3 b/site/public/audio/chip.mp3
new file mode 100644
index 0000000..f97ccc1
Binary files /dev/null and b/site/public/audio/chip.mp3 differ
diff --git a/site/public/audio/chips.mp3 b/site/public/audio/chips.mp3
new file mode 100644
index 0000000..1d06fb0
Binary files /dev/null and b/site/public/audio/chips.mp3 differ
diff --git a/site/public/audio/chop.mp3 b/site/public/audio/chop.mp3
new file mode 100644
index 0000000..752c667
Binary files /dev/null and b/site/public/audio/chop.mp3 differ
diff --git a/site/public/audio/coins.mp3 b/site/public/audio/coins.mp3
new file mode 100644
index 0000000..d4aa907
Binary files /dev/null and b/site/public/audio/coins.mp3 differ
diff --git a/site/public/audio/day.mp3 b/site/public/audio/day.mp3
new file mode 100644
index 0000000..4c8d08f
Binary files /dev/null and b/site/public/audio/day.mp3 differ
diff --git a/site/public/audio/dims.mp3 b/site/public/audio/dims.mp3
new file mode 100644
index 0000000..35e5ab4
Binary files /dev/null and b/site/public/audio/dims.mp3 differ
diff --git a/site/public/audio/dish.mp3 b/site/public/audio/dish.mp3
new file mode 100644
index 0000000..7cb8033
Binary files /dev/null and b/site/public/audio/dish.mp3 differ
diff --git a/site/public/audio/drip.mp3 b/site/public/audio/drip.mp3
new file mode 100644
index 0000000..9d16ef6
Binary files /dev/null and b/site/public/audio/drip.mp3 differ
diff --git a/site/public/audio/dry.mp3 b/site/public/audio/dry.mp3
new file mode 100644
index 0000000..a0e1af9
Binary files /dev/null and b/site/public/audio/dry.mp3 differ
diff --git a/site/public/audio/dusk.mp3 b/site/public/audio/dusk.mp3
new file mode 100644
index 0000000..9f8e36a
Binary files /dev/null and b/site/public/audio/dusk.mp3 differ
diff --git a/site/public/audio/end.mp3 b/site/public/audio/end.mp3
new file mode 100644
index 0000000..9c03ff1
Binary files /dev/null and b/site/public/audio/end.mp3 differ
diff --git a/site/public/audio/ends.mp3 b/site/public/audio/ends.mp3
new file mode 100644
index 0000000..370b706
Binary files /dev/null and b/site/public/audio/ends.mp3 differ
diff --git a/site/public/audio/fast.mp3 b/site/public/audio/fast.mp3
new file mode 100644
index 0000000..2fc78ac
Binary files /dev/null and b/site/public/audio/fast.mp3 differ
diff --git a/site/public/audio/fills.mp3 b/site/public/audio/fills.mp3
new file mode 100644
index 0000000..52cf868
Binary files /dev/null and b/site/public/audio/fills.mp3 differ
diff --git a/site/public/audio/fish.mp3 b/site/public/audio/fish.mp3
new file mode 100644
index 0000000..84851bd
Binary files /dev/null and b/site/public/audio/fish.mp3 differ
diff --git a/site/public/audio/fix.mp3 b/site/public/audio/fix.mp3
new file mode 100644
index 0000000..1c3e43b
Binary files /dev/null and b/site/public/audio/fix.mp3 differ
diff --git a/site/public/audio/fresh.mp3 b/site/public/audio/fresh.mp3
new file mode 100644
index 0000000..b2b0488
Binary files /dev/null and b/site/public/audio/fresh.mp3 differ
diff --git a/site/public/audio/grab.mp3 b/site/public/audio/grab.mp3
new file mode 100644
index 0000000..3aafff2
Binary files /dev/null and b/site/public/audio/grab.mp3 differ
diff --git a/site/public/audio/grass.mp3 b/site/public/audio/grass.mp3
new file mode 100644
index 0000000..27c11da
Binary files /dev/null and b/site/public/audio/grass.mp3 differ
diff --git a/site/public/audio/grin.mp3 b/site/public/audio/grin.mp3
new file mode 100644
index 0000000..b340cd7
Binary files /dev/null and b/site/public/audio/grin.mp3 differ
diff --git a/site/public/audio/gus.mp3 b/site/public/audio/gus.mp3
new file mode 100644
index 0000000..a22549a
Binary files /dev/null and b/site/public/audio/gus.mp3 differ
diff --git a/site/public/audio/has.mp3 b/site/public/audio/has.mp3
new file mode 100644
index 0000000..14cde40
Binary files /dev/null and b/site/public/audio/has.mp3 differ
diff --git a/site/public/audio/help.mp3 b/site/public/audio/help.mp3
new file mode 100644
index 0000000..e4ba85e
Binary files /dev/null and b/site/public/audio/help.mp3 differ
diff --git a/site/public/audio/hike.mp3 b/site/public/audio/hike.mp3
new file mode 100644
index 0000000..ce99194
Binary files /dev/null and b/site/public/audio/hike.mp3 differ
diff --git a/site/public/audio/hills.mp3 b/site/public/audio/hills.mp3
new file mode 100644
index 0000000..b9b5728
Binary files /dev/null and b/site/public/audio/hills.mp3 differ
diff --git a/site/public/audio/his.mp3 b/site/public/audio/his.mp3
new file mode 100644
index 0000000..864766b
Binary files /dev/null and b/site/public/audio/his.mp3 differ
diff --git a/site/public/audio/hit.mp3 b/site/public/audio/hit.mp3
new file mode 100644
index 0000000..6be187b
Binary files /dev/null and b/site/public/audio/hit.mp3 differ
diff --git a/site/public/audio/hits.mp3 b/site/public/audio/hits.mp3
new file mode 100644
index 0000000..bd8044c
Binary files /dev/null and b/site/public/audio/hits.mp3 differ
diff --git a/site/public/audio/home.mp3 b/site/public/audio/home.mp3
new file mode 100644
index 0000000..6b37582
Binary files /dev/null and b/site/public/audio/home.mp3 differ
diff --git a/site/public/audio/hot.mp3 b/site/public/audio/hot.mp3
new file mode 100644
index 0000000..fca4c3f
Binary files /dev/null and b/site/public/audio/hot.mp3 differ
diff --git a/site/public/audio/in.mp3 b/site/public/audio/in.mp3
new file mode 100644
index 0000000..0d90371
Binary files /dev/null and b/site/public/audio/in.mp3 differ
diff --git a/site/public/audio/keep.mp3 b/site/public/audio/keep.mp3
new file mode 100644
index 0000000..a5e441a
Binary files /dev/null and b/site/public/audio/keep.mp3 differ
diff --git a/site/public/audio/lamps.mp3 b/site/public/audio/lamps.mp3
new file mode 100644
index 0000000..3653d12
Binary files /dev/null and b/site/public/audio/lamps.mp3 differ
diff --git a/site/public/audio/last.mp3 b/site/public/audio/last.mp3
new file mode 100644
index 0000000..7c7d23d
Binary files /dev/null and b/site/public/audio/last.mp3 differ
diff --git a/site/public/audio/light.mp3 b/site/public/audio/light.mp3
new file mode 100644
index 0000000..4dc14e5
Binary files /dev/null and b/site/public/audio/light.mp3 differ
diff --git a/site/public/audio/lights.mp3 b/site/public/audio/lights.mp3
new file mode 100644
index 0000000..9632958
Binary files /dev/null and b/site/public/audio/lights.mp3 differ
diff --git a/site/public/audio/make.mp3 b/site/public/audio/make.mp3
new file mode 100644
index 0000000..d35d772
Binary files /dev/null and b/site/public/audio/make.mp3 differ
diff --git a/site/public/audio/map.mp3 b/site/public/audio/map.mp3
new file mode 100644
index 0000000..1dd7f8c
Binary files /dev/null and b/site/public/audio/map.mp3 differ
diff --git a/site/public/audio/maps.mp3 b/site/public/audio/maps.mp3
new file mode 100644
index 0000000..3faa23e
Binary files /dev/null and b/site/public/audio/maps.mp3 differ
diff --git a/site/public/audio/may.mp3 b/site/public/audio/may.mp3
new file mode 100644
index 0000000..917925e
Binary files /dev/null and b/site/public/audio/may.mp3 differ
diff --git a/site/public/audio/next.mp3 b/site/public/audio/next.mp3
new file mode 100644
index 0000000..0af81db
Binary files /dev/null and b/site/public/audio/next.mp3 differ
diff --git a/site/public/audio/night.mp3 b/site/public/audio/night.mp3
new file mode 100644
index 0000000..bf31b0e
Binary files /dev/null and b/site/public/audio/night.mp3 differ
diff --git a/site/public/audio/on.mp3 b/site/public/audio/on.mp3
new file mode 100644
index 0000000..250d002
Binary files /dev/null and b/site/public/audio/on.mp3 differ
diff --git a/site/public/audio/pack.mp3 b/site/public/audio/pack.mp3
new file mode 100644
index 0000000..7d0f9f8
Binary files /dev/null and b/site/public/audio/pack.mp3 differ
diff --git a/site/public/audio/past.mp3 b/site/public/audio/past.mp3
new file mode 100644
index 0000000..2b8cc0d
Binary files /dev/null and b/site/public/audio/past.mp3 differ
diff --git a/site/public/audio/pick.mp3 b/site/public/audio/pick.mp3
new file mode 100644
index 0000000..11e4c3b
Binary files /dev/null and b/site/public/audio/pick.mp3 differ
diff --git a/site/public/audio/plan.mp3 b/site/public/audio/plan.mp3
new file mode 100644
index 0000000..d4bdc9c
Binary files /dev/null and b/site/public/audio/plan.mp3 differ
diff --git a/site/public/audio/pond.mp3 b/site/public/audio/pond.mp3
new file mode 100644
index 0000000..a86d407
Binary files /dev/null and b/site/public/audio/pond.mp3 differ
diff --git a/site/public/audio/rain.mp3 b/site/public/audio/rain.mp3
new file mode 100644
index 0000000..026cd83
Binary files /dev/null and b/site/public/audio/rain.mp3 differ
diff --git a/site/public/audio/red.mp3 b/site/public/audio/red.mp3
new file mode 100644
index 0000000..de84e8c
Binary files /dev/null and b/site/public/audio/red.mp3 differ
diff --git a/site/public/audio/ride.mp3 b/site/public/audio/ride.mp3
new file mode 100644
index 0000000..204b6f4
Binary files /dev/null and b/site/public/audio/ride.mp3 differ
diff --git a/site/public/audio/road.mp3 b/site/public/audio/road.mp3
new file mode 100644
index 0000000..a64925b
Binary files /dev/null and b/site/public/audio/road.mp3 differ
diff --git a/site/public/audio/rocks.mp3 b/site/public/audio/rocks.mp3
new file mode 100644
index 0000000..72eea27
Binary files /dev/null and b/site/public/audio/rocks.mp3 differ
diff --git a/site/public/audio/runs.mp3 b/site/public/audio/runs.mp3
new file mode 100644
index 0000000..c45d187
Binary files /dev/null and b/site/public/audio/runs.mp3 differ
diff --git a/site/public/audio/rush.mp3 b/site/public/audio/rush.mp3
new file mode 100644
index 0000000..19db482
Binary files /dev/null and b/site/public/audio/rush.mp3 differ
diff --git a/site/public/audio/safe.mp3 b/site/public/audio/safe.mp3
new file mode 100644
index 0000000..910a78f
Binary files /dev/null and b/site/public/audio/safe.mp3 differ
diff --git a/site/public/audio/sam.mp3 b/site/public/audio/sam.mp3
new file mode 100644
index 0000000..a35cc51
Binary files /dev/null and b/site/public/audio/sam.mp3 differ
diff --git a/site/public/audio/sand.mp3 b/site/public/audio/sand.mp3
new file mode 100644
index 0000000..f4505ba
Binary files /dev/null and b/site/public/audio/sand.mp3 differ
diff --git a/site/public/audio/see.mp3 b/site/public/audio/see.mp3
new file mode 100644
index 0000000..fa777f4
Binary files /dev/null and b/site/public/audio/see.mp3 differ
diff --git a/site/public/audio/shells.mp3 b/site/public/audio/shells.mp3
new file mode 100644
index 0000000..a353e27
Binary files /dev/null and b/site/public/audio/shells.mp3 differ
diff --git a/site/public/audio/shine.mp3 b/site/public/audio/shine.mp3
new file mode 100644
index 0000000..e931316
Binary files /dev/null and b/site/public/audio/shine.mp3 differ
diff --git a/site/public/audio/sing.mp3 b/site/public/audio/sing.mp3
new file mode 100644
index 0000000..3b4e3e9
Binary files /dev/null and b/site/public/audio/sing.mp3 differ
diff --git a/site/public/audio/sit.mp3 b/site/public/audio/sit.mp3
new file mode 100644
index 0000000..f033c87
Binary files /dev/null and b/site/public/audio/sit.mp3 differ
diff --git a/site/public/audio/sits.mp3 b/site/public/audio/sits.mp3
new file mode 100644
index 0000000..d54f5fa
Binary files /dev/null and b/site/public/audio/sits.mp3 differ
diff --git a/site/public/audio/sky.mp3 b/site/public/audio/sky.mp3
new file mode 100644
index 0000000..a811665
Binary files /dev/null and b/site/public/audio/sky.mp3 differ
diff --git a/site/public/audio/spot.mp3 b/site/public/audio/spot.mp3
new file mode 100644
index 0000000..6e7f1c1
Binary files /dev/null and b/site/public/audio/spot.mp3 differ
diff --git a/site/public/audio/sticks.mp3 b/site/public/audio/sticks.mp3
new file mode 100644
index 0000000..47d6bd6
Binary files /dev/null and b/site/public/audio/sticks.mp3 differ
diff --git a/site/public/audio/stops.mp3 b/site/public/audio/stops.mp3
new file mode 100644
index 0000000..1f7384e
Binary files /dev/null and b/site/public/audio/stops.mp3 differ
diff --git a/site/public/audio/sun.mp3 b/site/public/audio/sun.mp3
new file mode 100644
index 0000000..c4ddd60
Binary files /dev/null and b/site/public/audio/sun.mp3 differ
diff --git a/site/public/audio/swim.mp3 b/site/public/audio/swim.mp3
new file mode 100644
index 0000000..fe8295d
Binary files /dev/null and b/site/public/audio/swim.mp3 differ
diff --git a/site/public/audio/tape.mp3 b/site/public/audio/tape.mp3
new file mode 100644
index 0000000..eaf7c5c
Binary files /dev/null and b/site/public/audio/tape.mp3 differ
diff --git a/site/public/audio/tent.mp3 b/site/public/audio/tent.mp3
new file mode 100644
index 0000000..57c0d0f
Binary files /dev/null and b/site/public/audio/tent.mp3 differ
diff --git a/site/public/audio/tents.mp3 b/site/public/audio/tents.mp3
new file mode 100644
index 0000000..2f24940
Binary files /dev/null and b/site/public/audio/tents.mp3 differ
diff --git a/site/public/audio/tim.mp3 b/site/public/audio/tim.mp3
new file mode 100644
index 0000000..f052687
Binary files /dev/null and b/site/public/audio/tim.mp3 differ
diff --git a/site/public/audio/trails.mp3 b/site/public/audio/trails.mp3
new file mode 100644
index 0000000..64f3ac6
Binary files /dev/null and b/site/public/audio/trails.mp3 differ
diff --git a/site/public/audio/trip.mp3 b/site/public/audio/trip.mp3
new file mode 100644
index 0000000..a1c2f8d
Binary files /dev/null and b/site/public/audio/trip.mp3 differ
diff --git a/site/public/audio/up.mp3 b/site/public/audio/up.mp3
new file mode 100644
index 0000000..a2dbe22
Binary files /dev/null and b/site/public/audio/up.mp3 differ
diff --git a/site/public/audio/wait.mp3 b/site/public/audio/wait.mp3
new file mode 100644
index 0000000..06b78d8
Binary files /dev/null and b/site/public/audio/wait.mp3 differ
diff --git a/site/public/audio/wash.mp3 b/site/public/audio/wash.mp3
new file mode 100644
index 0000000..3983418
Binary files /dev/null and b/site/public/audio/wash.mp3 differ
diff --git a/site/public/audio/wave.mp3 b/site/public/audio/wave.mp3
new file mode 100644
index 0000000..540748c
Binary files /dev/null and b/site/public/audio/wave.mp3 differ
diff --git a/site/public/audio/wet.mp3 b/site/public/audio/wet.mp3
new file mode 100644
index 0000000..c821bd0
Binary files /dev/null and b/site/public/audio/wet.mp3 differ
diff --git a/site/public/audio/wide.mp3 b/site/public/audio/wide.mp3
new file mode 100644
index 0000000..d3b9469
Binary files /dev/null and b/site/public/audio/wide.mp3 differ
diff --git a/site/src/data.rs b/site/src/data.rs
new file mode 100644
index 0000000..00d39a4
--- /dev/null
+++ b/site/src/data.rs
@@ -0,0 +1,831 @@
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Word {
+ pub id: &'static str,
+ pub spelling: &'static str,
+ pub ipa_us: &'static str,
+ pub russian_hint: &'static str,
+ pub audio_path: &'static str,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Token {
+ Word {
+ word_id: &'static str,
+ surface: &'static str,
+ },
+ Punctuation(&'static str),
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct ReadingText {
+ pub id: &'static str,
+ pub title: &'static str,
+ pub tokens: &'static [Token],
+}
+
+macro_rules! entry {
+ ($id:literal, $ipa:literal, $hint:literal) => {
+ Word {
+ id: $id,
+ spelling: $id,
+ ipa_us: $ipa,
+ russian_hint: $hint,
+ audio_path: concat!("/audio/", $id, ".mp3"),
+ }
+ };
+}
+macro_rules! word {
+ ($id:literal) => {
+ Token::Word {
+ word_id: $id,
+ surface: $id,
+ }
+ };
+ ($id:literal, $surface:literal) => {
+ Token::Word {
+ word_id: $id,
+ surface: $surface,
+ }
+ };
+}
+macro_rules! punct {
+ ($value:literal) => {
+ Token::Punctuation($value)
+ };
+}
+
+pub const DICTIONARY: &[Word] = &[
+ entry!("sam", "sæm", "сэм"),
+ entry!("and", "ænd", "энд"),
+ entry!("ben", "bɛn", "бэн"),
+ entry!("sit", "sɪt", "сит"),
+ entry!("at", "æt", "эт"),
+ entry!("camp", "kæmp", "кэмп"),
+ entry!("plan", "plæn", "плэн"),
+ entry!("next", "nɛkst", "нэкст"),
+ entry!("trip", "trɪp", "трип"),
+ entry!("tim", "tɪm", "тим"),
+ entry!("gus", "ɡʌs", "гас"),
+ entry!("pack", "pæk", "пэк"),
+ entry!("red", "rɛd", "рэд"),
+ entry!("bags", "bæɡz", "бэгз"),
+ entry!("fresh", "frɛʃ", "фреш"),
+ entry!("fish", "fɪʃ", "фиш"),
+ entry!("chips", "tʃɪps", "чипс"),
+ entry!("has", "hæz", "хэз"),
+ entry!("his", "hɪz", "хиз"),
+ entry!("map", "mæp", "мэп"),
+ entry!("big", "bɪɡ", "биг"),
+ entry!("maps", "mæps", "мэпс"),
+ entry!("black", "blæk", "блэк"),
+ entry!("in", "ɪn", "ин"),
+ entry!("sits", "sɪts", "ситс"),
+ entry!("past", "pæst", "пэст"),
+ entry!("hills", "hɪlz", "хилз"),
+ entry!("wide", "waɪd", "уайд"),
+ entry!("grass", "ɡræs", "грэс"),
+ entry!("trails", "treɪlz", "трейлз"),
+ entry!("rain", "reɪn", "рейн"),
+ entry!("hits", "hɪts", "хитс"),
+ entry!("runs", "rʌnz", "ранз"),
+ entry!("rocks", "rɑks", "ракс"),
+ entry!("wait", "weɪt", "уэйт"),
+ entry!("tents", "tɛnts", "тентс"),
+ entry!("chat", "tʃæt", "чэт"),
+ entry!("can", "kæn", "кэн"),
+ entry!("see", "siː", "си"),
+ entry!("drip", "drɪp", "дрип"),
+ entry!("on", "ɑn", "ан"),
+ entry!("fast", "fæst", "фэст"),
+ entry!("swim", "swɪm", "свим"),
+ entry!("pond", "pɑnd", "панд"),
+ entry!("grab", "ɡræb", "грэб"),
+ entry!("rush", "rʌʃ", "раш"),
+ entry!("wet", "wɛt", "уэт"),
+ entry!("make", "meɪk", "мейк"),
+ entry!("bright", "braɪt", "брайт"),
+ entry!("light", "laɪt", "лайт"),
+ entry!("dry", "draɪ", "драй"),
+ entry!("sticks", "stɪks", "стикс"),
+ entry!("help", "hɛlp", "хэлп"),
+ entry!("chip", "tʃɪp", "чип"),
+ entry!("dish", "dɪʃ", "диш"),
+ entry!("ends", "ɛndz", "эндз"),
+ entry!("sun", "sʌn", "сан"),
+ entry!("shine", "ʃaɪn", "шайн"),
+ entry!("hike", "haɪk", "хайк"),
+ entry!("ride", "raɪd", "райд"),
+ entry!("bikes", "baɪks", "байкс"),
+ entry!("pick", "pɪk", "пик"),
+ entry!("coins", "kɔɪnz", "койнз"),
+ entry!("sand", "sænd", "сэнд"),
+ entry!("by", "baɪ", "бай"),
+ entry!("road", "roʊd", "роуд"),
+ entry!("shells", "ʃɛlz", "шелз"),
+ entry!("wash", "wɑʃ", "уош"),
+ entry!("chop", "tʃɑp", "чап"),
+ entry!("fix", "fɪks", "фикс"),
+ entry!("bike", "baɪk", "байк"),
+ entry!("tape", "teɪp", "тейп"),
+ entry!("night", "naɪt", "найт"),
+ entry!("fills", "fɪlz", "филз"),
+ entry!("sky", "skaɪ", "скай"),
+ entry!("dims", "dɪmz", "димз"),
+ entry!("sing", "sɪŋ", "синг"),
+ entry!("grin", "ɡrɪn", "грин"),
+ entry!("may", "meɪ", "мей"),
+ entry!("hit", "hɪt", "хит"),
+ entry!("but", "bʌt", "бат"),
+ entry!("lamps", "læmps", "лэмпс"),
+ entry!("keep", "kiːp", "кип"),
+ entry!("tent", "tɛnt", "тент"),
+ entry!("safe", "seɪf", "сейф"),
+ entry!("bag", "bæɡ", "бэг"),
+ entry!("day", "deɪ", "дей"),
+ entry!("stops", "stɑps", "стапс"),
+ entry!("spot", "spɑt", "спат"),
+ entry!("hot", "hɑt", "хат"),
+ entry!("last", "læst", "лэст"),
+ entry!("up", "ʌp", "ап"),
+ entry!("home", "hoʊm", "хоум"),
+ entry!("dusk", "dʌsk", "даск"),
+ entry!("end", "ɛnd", "энд"),
+ entry!("lights", "laɪts", "лайтс"),
+ entry!("wave", "weɪv", "уэйв"),
+];
+
+const BASIC_RULES_TOKENS: &[Token] = &[
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("sit"),
+ word!("at"),
+ word!("camp"),
+ word!("and"),
+ word!("plan"),
+ word!("next"),
+ word!("trip"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("pack"),
+ word!("red"),
+ word!("bags"),
+ word!("fresh"),
+ word!("fish"),
+ word!("and"),
+ word!("chips"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("has"),
+ word!("his"),
+ word!("map"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("has"),
+ word!("his"),
+ word!("big"),
+ word!("map"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("has"),
+ word!("red"),
+ word!("maps"),
+ punct!(";"),
+ word!("gus", "Gus"),
+ word!("has"),
+ word!("black"),
+ word!("maps"),
+ word!("in"),
+ word!("camp"),
+ punct!("."),
+ word!("camp", "Camp"),
+ word!("sits"),
+ word!("past"),
+ word!("big"),
+ word!("hills"),
+ word!("and"),
+ word!("wide"),
+ word!("camp"),
+ word!("grass"),
+ word!("trails"),
+ punct!("."),
+ word!("rain", "Rain"),
+ word!("hits"),
+ word!("grass"),
+ word!("and"),
+ word!("runs"),
+ word!("past"),
+ word!("black"),
+ word!("rocks"),
+ word!("in"),
+ word!("camp"),
+ punct!("."),
+ word!("sam", "Sam"),
+ punct!(","),
+ word!("ben", "Ben"),
+ punct!(","),
+ word!("tim", "Tim"),
+ punct!(","),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("wait"),
+ word!("in"),
+ word!("tents"),
+ word!("and"),
+ word!("chat"),
+ punct!("."),
+ word!("gus", "Gus"),
+ word!("can"),
+ word!("see"),
+ word!("rain"),
+ word!("drip"),
+ word!("on"),
+ word!("red"),
+ word!("camp"),
+ word!("tents"),
+ word!("fast"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("see"),
+ word!("fish"),
+ word!("swim"),
+ word!("in"),
+ word!("big"),
+ word!("camp"),
+ word!("pond"),
+ word!("fast"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("grab"),
+ word!("bags"),
+ word!("and"),
+ word!("rush"),
+ word!("past"),
+ word!("wet"),
+ word!("grass"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("make"),
+ word!("bright"),
+ word!("camp"),
+ word!("light"),
+ punct!(";"),
+ word!("dry"),
+ word!("sticks"),
+ word!("help"),
+ word!("sam", "Sam"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("make"),
+ word!("fresh"),
+ word!("fish"),
+ word!("chip"),
+ word!("dish"),
+ word!("at"),
+ word!("camp"),
+ word!("tent"),
+ punct!("."),
+ word!("rain", "Rain"),
+ word!("ends"),
+ word!("and"),
+ word!("sun"),
+ word!("can"),
+ word!("shine"),
+ word!("on"),
+ word!("hills"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("hike"),
+ word!("past"),
+ word!("big"),
+ word!("rocks"),
+ word!("in"),
+ word!("rain"),
+ word!("fast"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("ride"),
+ word!("bikes"),
+ word!("on"),
+ word!("wide"),
+ word!("camp"),
+ word!("grass"),
+ word!("trails"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("pick"),
+ word!("coins"),
+ word!("in"),
+ word!("sand"),
+ word!("by"),
+ word!("wide"),
+ word!("camp"),
+ word!("road"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("pick"),
+ word!("bright"),
+ word!("shells"),
+ word!("in"),
+ word!("sand"),
+ word!("by"),
+ word!("wide"),
+ word!("road"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("can"),
+ word!("wash"),
+ word!("fish"),
+ word!("and"),
+ word!("chop"),
+ word!("red"),
+ word!("chips"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("gus", "Gus"),
+ word!("can"),
+ word!("fix"),
+ word!("his"),
+ word!("bike"),
+ punct!(";"),
+ word!("black"),
+ word!("tape"),
+ word!("can"),
+ word!("help"),
+ word!("gus", "Gus"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("help"),
+ word!("gus", "Gus"),
+ word!("fix"),
+ word!("his"),
+ word!("bike"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("night", "Night"),
+ word!("fills"),
+ word!("camp"),
+ word!("and"),
+ word!("black"),
+ word!("sky"),
+ word!("dims"),
+ word!("camp"),
+ word!("trails"),
+ word!("fast"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("sit"),
+ word!("by"),
+ word!("bright"),
+ word!("camp"),
+ word!("light"),
+ word!("and"),
+ word!("sing"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("sit"),
+ word!("by"),
+ word!("bright"),
+ word!("camp"),
+ word!("light"),
+ word!("and"),
+ word!("grin"),
+ punct!("."),
+ word!("rain", "Rain"),
+ word!("may"),
+ word!("hit"),
+ word!("tents"),
+ word!("but"),
+ word!("camp"),
+ word!("lamps"),
+ word!("make"),
+ word!("bright"),
+ word!("light"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("keep"),
+ word!("dry"),
+ word!("bags"),
+ word!("in"),
+ word!("his"),
+ word!("tent"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("keep"),
+ word!("his"),
+ word!("map"),
+ word!("safe"),
+ word!("in"),
+ word!("tent"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("can"),
+ word!("keep"),
+ word!("fish"),
+ word!("fresh"),
+ word!("in"),
+ word!("his"),
+ word!("bag"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("gus", "Gus"),
+ word!("can"),
+ word!("keep"),
+ word!("red"),
+ word!("chips"),
+ word!("in"),
+ word!("his"),
+ word!("bag"),
+ word!("at"),
+ word!("camp"),
+ punct!("."),
+ word!("day", "Day"),
+ word!("ends"),
+ punct!(";"),
+ word!("rain"),
+ word!("stops"),
+ punct!("."),
+ word!("sun", "Sun"),
+ word!("can"),
+ word!("shine"),
+ word!("at"),
+ word!("red"),
+ word!("camp"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("plan"),
+ word!("next"),
+ word!("hike"),
+ word!("by"),
+ word!("wide"),
+ word!("camp"),
+ word!("road"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("plan"),
+ word!("next"),
+ word!("bike"),
+ word!("ride"),
+ word!("by"),
+ word!("camp"),
+ word!("road"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("help"),
+ word!("ben", "Ben"),
+ word!("past"),
+ word!("rocks"),
+ word!("and"),
+ word!("wet"),
+ word!("camp"),
+ word!("grass"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("can"),
+ word!("help"),
+ word!("gus", "Gus"),
+ word!("past"),
+ word!("hills"),
+ word!("and"),
+ word!("red"),
+ word!("camp"),
+ word!("tents"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("ride"),
+ word!("bike"),
+ word!("past"),
+ word!("camp"),
+ word!("pond"),
+ word!("and"),
+ word!("rocks"),
+ word!("fast"),
+ punct!("."),
+ word!("gus", "Gus"),
+ word!("can"),
+ word!("ride"),
+ word!("bike"),
+ word!("past"),
+ word!("wet"),
+ word!("grass"),
+ word!("and"),
+ word!("tents"),
+ word!("fast"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("see"),
+ word!("bright"),
+ word!("light"),
+ word!("on"),
+ word!("camp"),
+ word!("road"),
+ word!("at"),
+ word!("night"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("see"),
+ word!("bright"),
+ word!("lights"),
+ word!("at"),
+ word!("night"),
+ word!("in"),
+ word!("his"),
+ word!("camp"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("can"),
+ word!("spot"),
+ word!("red"),
+ word!("fish"),
+ word!("in"),
+ word!("pond"),
+ word!("at"),
+ word!("night"),
+ word!("fast"),
+ punct!("."),
+ word!("gus", "Gus"),
+ word!("can"),
+ word!("spot"),
+ word!("big"),
+ word!("fish"),
+ word!("in"),
+ word!("pond"),
+ word!("at"),
+ word!("night"),
+ word!("fast"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("make"),
+ word!("hot"),
+ word!("fish"),
+ word!("dish"),
+ word!("in"),
+ word!("camp"),
+ word!("tent"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("make"),
+ word!("fresh"),
+ word!("chip"),
+ word!("dish"),
+ word!("in"),
+ word!("camp"),
+ word!("tent"),
+ punct!("."),
+ word!("rain", "Rain"),
+ word!("ends"),
+ word!("and"),
+ word!("camp"),
+ word!("trails"),
+ word!("dry"),
+ word!("in"),
+ word!("sun"),
+ word!("at"),
+ word!("last"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("pick"),
+ word!("wet"),
+ word!("tents"),
+ word!("up"),
+ word!("and"),
+ word!("pack"),
+ word!("fast"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("pack"),
+ word!("fish"),
+ word!("chips"),
+ word!("maps"),
+ word!("and"),
+ word!("bags"),
+ word!("fast"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("and"),
+ word!("ben", "Ben"),
+ word!("hike"),
+ word!("home"),
+ word!("by"),
+ word!("wide"),
+ word!("road"),
+ word!("at"),
+ word!("dusk"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("ride"),
+ word!("home"),
+ word!("by"),
+ word!("wide"),
+ word!("road"),
+ word!("at"),
+ word!("dusk"),
+ punct!("."),
+ word!("sam", "Sam"),
+ word!("can"),
+ word!("see"),
+ word!("rain"),
+ word!("end"),
+ word!("and"),
+ word!("sun"),
+ word!("shine"),
+ word!("at"),
+ word!("last"),
+ punct!("."),
+ word!("ben", "Ben"),
+ word!("can"),
+ word!("see"),
+ word!("night"),
+ word!("sky"),
+ word!("and"),
+ word!("bright"),
+ word!("red"),
+ word!("camp"),
+ word!("lights"),
+ punct!("."),
+ word!("tim", "Tim"),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("grin"),
+ word!("and"),
+ word!("wave"),
+ word!("at"),
+ word!("camp"),
+ word!("at"),
+ word!("last"),
+ punct!("."),
+ word!("sam", "Sam"),
+ punct!(","),
+ word!("ben", "Ben"),
+ punct!(","),
+ word!("tim", "Tim"),
+ punct!(","),
+ word!("and"),
+ word!("gus", "Gus"),
+ word!("plan"),
+ word!("next"),
+ word!("big"),
+ word!("camp"),
+ word!("trip"),
+ punct!("."),
+];
+
+pub const READING_TEXTS: &[ReadingText] = &[ReadingText {
+ id: "basic-rules-01",
+ title: "Текст для чтения",
+ tokens: BASIC_RULES_TOKENS,
+}];
+
+pub fn word_by_id(id: &str) -> Option<&'static Word> {
+ DICTIONARY.iter().find(|word| word.id == id)
+}
+
+#[cfg(test)]
+pub fn tokens_to_text(tokens: &[Token]) -> String {
+ let mut output = String::new();
+ let mut needs_space = false;
+ for token in tokens {
+ match token {
+ Token::Word { surface, .. } => {
+ if needs_space {
+ output.push(' ');
+ }
+ output.push_str(surface);
+ needs_space = true;
+ }
+ Token::Punctuation(value) => {
+ output.push_str(value);
+ needs_space = !matches!(*value, "…" | "—" | "(");
+ }
+ }
+ }
+ output
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::Path;
+
+ fn source_text() -> &'static str {
+ let source =
+ include_str!("../../english/ex/theme-13-reading-rules/reading-text-01-basic-rules.md");
+ source
+ .split_once("")
+ .unwrap()
+ .1
+ .split_once("")
+ .unwrap()
+ .0
+ .trim()
+ }
+
+ #[test]
+ fn tokens_render_exact_source_text() {
+ let text = &READING_TEXTS[0];
+ assert_eq!(tokens_to_text(text.tokens), source_text());
+ assert_eq!(
+ text.tokens
+ .iter()
+ .filter(|token| matches!(token, Token::Word { .. }))
+ .count(),
+ 500
+ );
+ }
+
+ #[test]
+ fn repeated_words_reuse_stable_ids() {
+ let sam_ids: Vec<_> = READING_TEXTS[0]
+ .tokens
+ .iter()
+ .filter_map(|token| match token {
+ Token::Word { word_id, surface } if *surface == "Sam" => Some(*word_id),
+ _ => None,
+ })
+ .collect();
+ assert!(sam_ids.len() > 1);
+ assert!(sam_ids.iter().all(|id| *id == "sam"));
+ }
+
+ #[test]
+ fn dictionary_ids_are_unique_and_audio_files_exist() {
+ assert_eq!(DICTIONARY.len(), 97);
+ assert_eq!(word_by_id("camp").unwrap().ipa_us, "kæmp");
+ assert!(word_by_id("missing").is_none());
+ for (index, word) in DICTIONARY.iter().enumerate() {
+ assert!(
+ !DICTIONARY[..index]
+ .iter()
+ .any(|previous| previous.id == word.id),
+ "duplicate dictionary id: {}",
+ word.id
+ );
+ }
+ for word in DICTIONARY {
+ let asset = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("public")
+ .join(word.audio_path.trim_start_matches('/'));
+ assert!(asset.is_file(), "missing audio: {}", asset.display());
+ }
+ }
+
+ #[test]
+ fn every_text_token_references_exactly_one_dictionary_word() {
+ for text in READING_TEXTS {
+ for token in text.tokens {
+ if let Token::Word { word_id, .. } = token {
+ let matches = DICTIONARY.iter().filter(|word| word.id == *word_id).count();
+ assert_eq!(matches, 1, "{} references {word_id}", text.id);
+ }
+ }
+ }
+ }
+}
diff --git a/site/src/main.rs b/site/src/main.rs
new file mode 100644
index 0000000..dbe866b
--- /dev/null
+++ b/site/src/main.rs
@@ -0,0 +1,247 @@
+mod data;
+
+use data::{word_by_id, ReadingText, Token, Word, DICTIONARY, READING_TEXTS};
+use leptos::ev::KeyboardEvent;
+use leptos::prelude::*;
+use wasm_bindgen_futures::{spawn_local, JsFuture};
+use web_sys::HtmlInputElement;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum AudioState {
+ Ready,
+ Playing,
+ PlaybackBlocked,
+ Unavailable,
+}
+
+fn main() {
+ mount_to_body(App);
+}
+
+#[component]
+fn App() -> impl IntoView {
+ let reading_text = &READING_TEXTS[0];
+ let word_count = reading_text
+ .tokens
+ .iter()
+ .filter(|token| matches!(token, Token::Word { .. }))
+ .count();
+ let selected_id = RwSignal::new(None::<&'static str>);
+ let query = RwSignal::new(String::new());
+ let audio_ref = NodeRef::::new();
+ let audio_state = RwSignal::new(AudioState::Ready);
+
+ let select_word = move |id: &'static str| {
+ if let Some(audio) = audio_ref.get() {
+ let _ = audio.pause();
+ }
+ audio_state.set(AudioState::Ready);
+ selected_id.set(Some(id));
+ };
+ let activate_key = move |event: KeyboardEvent, id: &'static str| {
+ if event.key() == "Enter" || event.key() == " " {
+ event.prevent_default();
+ select_word(id);
+ }
+ };
+
+ let toggle_audio = move |_| {
+ if let Some(audio) = audio_ref.get() {
+ if audio.paused() {
+ match audio.play() {
+ Ok(promise) => {
+ let audio_state = audio_state;
+ spawn_local(async move {
+ if JsFuture::from(promise).await.is_err() {
+ audio_state.set(AudioState::PlaybackBlocked);
+ }
+ });
+ }
+ Err(_) => audio_state.set(AudioState::PlaybackBlocked),
+ }
+ } else {
+ let _ = audio.pause();
+ }
+ }
+ };
+
+ view! {
+
+
+
+
+
+
"01"
+
+
{reading_text.title}
+
"Каждое слово — отдельная карточка для слуха и произношения."
+
+
{format!("{word_count} слов")}
+
+
+ {render_reading_text(reading_text, selected_id, select_word, activate_key)}
+
+
+
+
+ {move || selected_id.get().and_then(word_by_id).map(|word| detail_view(word, audio_ref, audio_state, toggle_audio)).unwrap_or_else(|| view! {
+
+
"Словарная карточка"
+
"Выберите слово в тексте"
+
"Карточка покажет написание, американское произношение и русскую подсказку."
+
+ }.into_any())}
+
+
+
+
+
"02"
+
+
"Внутренний словарь"
+
"Все слова урока; поиск по написанию, IPA или русской подсказке."
+
+
+
+ (&event).value())
+ />
+
+
+ | "Слово" | "US IPA" | "Подсказка" | "Аудио" |
+
+ {move || {
+ let needle = query.get().to_lowercase();
+ DICTIONARY.iter().filter(|word| needle.is_empty() || word.spelling.contains(&needle) || word.ipa_us.contains(&needle) || word.russian_hint.to_lowercase().contains(&needle)).map(|word| {
+ let id = word.id;
+ view! {
+
+ |
+ {word.ipa_us} |
+ {word.russian_hint} |
+
+
+ |
+
+ }
+ }).collect_view()
+ }}
+
+
+
+
+
+ }
+}
+
+fn render_reading_text(
+ reading_text: &'static ReadingText,
+ selected_id: RwSignal