diff --git a/Cargo.lock b/Cargo.lock index 226969f..8b47356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -162,6 +197,39 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -174,6 +242,92 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -196,6 +350,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -326,6 +486,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.1" @@ -455,6 +637,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.6.1" @@ -709,6 +904,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -860,11 +1065,35 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "aes-gcm", + "base64", "percent-encoding", + "rand 0.8.6", + "subtle", "time", "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1000,6 +1229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1026,6 +1256,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.11" @@ -1310,6 +1549,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.1" @@ -1321,6 +1566,16 @@ dependencies = [ "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 5.4.1", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1442,6 +1697,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1543,6 +1813,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1658,6 +1941,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -1700,6 +1993,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1765,6 +2077,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1891,6 +2209,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1918,6 +2237,22 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1936,9 +2271,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2158,6 +2495,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.47.2" @@ -2264,15 +2610,17 @@ dependencies = [ "async-trait", "axum", "axum-casbin", - "axum-extra", + "axum-extra 0.10.3", "bytes", "chrono", "dotenvy", "fluent-templates", "include_dir", "insta", + "loco-oauth2", "loco-rs", "migration", + "passwords", "regex", "reqwest", "rstest", @@ -2282,6 +2630,7 @@ dependencies = [ "serial_test", "time", "tokio", + "tower-sessions", "tracing", "tracing-subscriber", "unic-langid", @@ -2309,6 +2658,15 @@ dependencies = [ "libc", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2406,6 +2764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", + "serde", ] [[package]] @@ -2430,6 +2789,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "loco-oauth2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2196d3c27508180fc180067c7ddc9011bb216aacfb06c70b9665b30a54064" +dependencies = [ + "async-std", + "async-trait", + "axum", + "axum-extra 0.12.6", + "cookie", + "http", + "loco-rs", + "oauth2", + "reqwest", + "sea-orm", + "sea-orm-migration", + "serde", + "serde_json", + "subtle", + "thiserror 2.0.18", + "time", + "tokio", + "tower-sessions", + "tracing", + "tracing-subscriber", +] + [[package]] name = "loco-rs" version = "0.16.4" @@ -2439,7 +2826,7 @@ dependencies = [ "argon2", "async-trait", "axum", - "axum-extra", + "axum-extra 0.10.3", "axum-test", "backtrace_printer", "byte-unit", @@ -2496,6 +2883,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lru-slab" @@ -2696,6 +3086,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2852,6 +3259,26 @@ dependencies = [ "libm", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2873,6 +3300,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "opendal" version = "0.54.1" @@ -2899,6 +3332,49 @@ dependencies = [ "uuid", ] +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -2991,6 +3467,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "passwords" +version = "3.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11407193a7c2bd14ec6b0ec3394da6fdcf7a4d5dcbc8c3cc38dfb17802c8d59c" +dependencies = [ + "random-pick", +] + [[package]] name = "pem" version = "3.0.6" @@ -3136,6 +3621,23 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3169,6 +3671,32 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3489,6 +4017,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "random-pick" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "593d142cfebb4f8a2f3c97862b2381d711faa98a30f2497df441ac51580a1edf" +dependencies = [ + "rand 0.9.4", +] + [[package]] name = "redis" version = "0.31.0" @@ -3581,16 +4118,21 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -3601,6 +4143,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower 0.5.3", @@ -3891,6 +4434,15 @@ dependencies = [ "sdd", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4091,6 +4643,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.26.0" @@ -4541,7 +5116,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", @@ -4838,6 +5413,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -5029,6 +5625,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.3", @@ -5062,6 +5659,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -5194,6 +5801,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" +dependencies = [ + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.6.10" @@ -5235,6 +5858,57 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "futures", + "http", + "parking_lot", + "rand 0.8.6", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.44" @@ -5470,6 +6144,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5492,6 +6176,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5567,6 +6252,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -5890,6 +6581,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 48f1d6b..57e21e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,9 @@ unic-langid = { version = "0.9" } axum-extra = { version = "0.10", features = ["form"] } bytes = { version = "1" } axum-casbin = "1.3.0" +loco-oauth2 = "0.5.0" +passwords = "3.1.16" +tower-sessions = "0.14" [[bin]] name = "kompress-eshop-cli" diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl index c5ca5ec..31f2898 100644 --- a/assets/i18n/en/main.ftl +++ b/assets/i18n/en/main.ftl @@ -66,6 +66,8 @@ login-email = Email login-password = Password login-no-account = Don't have an account? login-have-account = Already have an account? +auth-or = or +auth-google = Continue with Google nav-login = Sign in nav-register = Register register-title = Create account diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl index 053e6f9..3962cfd 100644 --- a/assets/i18n/sk/main.ftl +++ b/assets/i18n/sk/main.ftl @@ -66,6 +66,8 @@ login-email = E-mail login-password = Heslo login-no-account = Nemáte účet? login-have-account = Už máte účet? +auth-or = alebo +auth-google = Pokračovať cez Google nav-login = Prihlásiť sa nav-register = Registrácia register-title = Vytvoriť účet diff --git a/assets/static/css/app.css b/assets/static/css/app.css index 6c24d9c..447a6d0 100644 --- a/assets/static/css/app.css +++ b/assets/static/css/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-600:oklch(57.7% .245 27.325);--color-amber-500:oklch(76.9% .188 70.08);--color-green-600:oklch(62.7% .194 149.214);--color-emerald-600:oklch(59.6% .145 163.225);--color-sky-500:oklch(68.5% .169 237.323);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-600:oklch(51.1% .262 276.966);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-sm:.25rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-surface:var(--color-white);--color-surface-alt:var(--color-slate-100);--color-on-surface:var(--color-slate-700);--color-on-surface-strong:var(--color-slate-900);--color-primary:var(--color-indigo-600);--color-on-primary:var(--color-white);--color-secondary:var(--color-slate-600);--color-on-secondary:var(--color-white);--color-outline:var(--color-slate-300);--color-outline-strong:var(--color-slate-800);--color-surface-dark:var(--color-slate-900);--color-surface-dark-alt:var(--color-slate-800);--color-on-surface-dark:var(--color-slate-300);--color-on-surface-dark-strong:var(--color-white);--color-primary-dark:var(--color-indigo-400);--color-on-primary-dark:var(--color-slate-950);--color-secondary-dark:var(--color-slate-300);--color-on-secondary-dark:var(--color-slate-950);--color-outline-dark:var(--color-slate-700);--color-outline-dark-strong:var(--color-slate-300);--color-info:var(--color-sky-500);--color-on-info:var(--color-white);--color-success:var(--color-green-600);--color-on-success:var(--color-white);--color-warning:var(--color-amber-500);--color-on-warning:var(--color-white);--color-danger:var(--color-red-600);--color-on-danger:var(--color-white);--radius-radius:.375rem}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-x-8{inset-inline:calc(var(--spacing) * 8)}.inset-y-0{inset-block:0}.-top-1{top:calc(var(--spacing) * -1)}.top-0{top:0}.top-1\/2{top:50%}.top-full{top:100%}.-right-1{right:calc(var(--spacing) * -1)}.right-0{right:0}.right-3{right:calc(var(--spacing) * 3)}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-99{z-index:99}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-2{margin-right:calc(var(--spacing) * 2)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-10{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-14{width:calc(var(--spacing) * 14);height:calc(var(--spacing) * 14)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.size-24{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}.size-full{width:100%;height:100%}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-16{height:calc(var(--spacing) * 16)}.h-44{height:calc(var(--spacing) * 44)}.h-fit{height:fit-content}.max-h-56{max-height:calc(var(--spacing) * 56)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-56{width:calc(var(--spacing) * 56)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-4{min-width:calc(var(--spacing) * 4)}.min-w-40{min-width:calc(var(--spacing) * 40)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-24{--tw-translate-x:calc(var(--spacing) * -24);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-60{--tw-translate-x:calc(var(--spacing) * -60);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-0{rotate:0deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.appearance-none{appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-outline>:not(:last-child)){border-color:var(--color-outline)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-radius{border-radius:var(--radius-radius)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-l-radius{border-top-left-radius:var(--radius-radius);border-bottom-left-radius:var(--radius-radius)}.rounded-r-radius{border-top-right-radius:var(--radius-radius);border-bottom-right-radius:var(--radius-radius)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-danger{border-color:var(--color-danger)}.border-info{border-color:var(--color-info)}.border-outline{border-color:var(--color-outline)}.border-primary{border-color:var(--color-primary)}.border-primary\/40{border-color:#4f39f666}@supports (color:color-mix(in lab, red, red)){.border-primary\/40{border-color:color-mix(in oklab, var(--color-primary) 40%, transparent)}}.border-secondary{border-color:var(--color-secondary)}.border-success{border-color:var(--color-success)}.border-warning{border-color:var(--color-warning)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-danger{background-color:var(--color-danger)}.bg-danger\/10{background-color:#e400141a}@supports (color:color-mix(in lab, red, red)){.bg-danger\/10{background-color:color-mix(in oklab, var(--color-danger) 10%, transparent)}}.bg-danger\/15{background-color:#e4001426}@supports (color:color-mix(in lab, red, red)){.bg-danger\/15{background-color:color-mix(in oklab, var(--color-danger) 15%, transparent)}}.bg-info{background-color:var(--color-info)}.bg-info\/10{background-color:#00a5ef1a}@supports (color:color-mix(in lab, red, red)){.bg-info\/10{background-color:color-mix(in oklab, var(--color-info) 10%, transparent)}}.bg-info\/15{background-color:#00a5ef26}@supports (color:color-mix(in lab, red, red)){.bg-info\/15{background-color:color-mix(in oklab, var(--color-info) 15%, transparent)}}.bg-primary{background-color:var(--color-primary)}.bg-primary\/5{background-color:#4f39f60d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-primary\/10{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.bg-secondary{background-color:var(--color-secondary)}.bg-success{background-color:var(--color-success)}.bg-success\/10{background-color:#00a5441a}@supports (color:color-mix(in lab, red, red)){.bg-success\/10{background-color:color-mix(in oklab, var(--color-success) 10%, transparent)}}.bg-success\/15{background-color:#00a54426}@supports (color:color-mix(in lab, red, red)){.bg-success\/15{background-color:color-mix(in oklab, var(--color-success) 15%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-alt{background-color:var(--color-surface-alt)}.bg-surface-alt\/40{background-color:#f1f5f966}@supports (color:color-mix(in lab, red, red)){.bg-surface-alt\/40{background-color:color-mix(in oklab, var(--color-surface-alt) 40%, transparent)}}.bg-surface-alt\/50{background-color:#f1f5f980}@supports (color:color-mix(in lab, red, red)){.bg-surface-alt\/50{background-color:color-mix(in oklab, var(--color-surface-alt) 50%, transparent)}}.bg-surface\/40{background-color:#fff6}@supports (color:color-mix(in lab, red, red)){.bg-surface\/40{background-color:color-mix(in oklab, var(--color-surface) 40%, transparent)}}.bg-surface\/95{background-color:#fffffff2}@supports (color:color-mix(in lab, red, red)){.bg-surface\/95{background-color:color-mix(in oklab, var(--color-surface) 95%, transparent)}}.bg-transparent{background-color:#0000}.bg-warning{background-color:var(--color-warning)}.bg-warning\/10{background-color:#f99c001a}@supports (color:color-mix(in lab, red, red)){.bg-warning\/10{background-color:color-mix(in oklab, var(--color-warning) 10%, transparent)}}.bg-warning\/15{background-color:#f99c0026}@supports (color:color-mix(in lab, red, red)){.bg-warning\/15{background-color:color-mix(in oklab, var(--color-warning) 15%, transparent)}}.object-cover{object-fit:cover}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:0}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pr-7{padding-right:calc(var(--spacing) * 7)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-1{padding-left:var(--spacing)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-pretty{text-wrap:pretty}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.text-danger{color:var(--color-danger)}.text-info{color:var(--color-info)}.text-on-danger{color:var(--color-on-danger)}.text-on-info{color:var(--color-on-info)}.text-on-primary{color:var(--color-on-primary)}.text-on-secondary{color:var(--color-on-secondary)}.text-on-success{color:var(--color-on-success)}.text-on-surface{color:var(--color-on-surface)}.text-on-surface-strong{color:var(--color-on-surface-strong)}.text-on-surface\/40{color:#31415866}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/40{color:color-mix(in oklab, var(--color-on-surface) 40%, transparent)}}.text-on-surface\/60{color:#31415899}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/60{color:color-mix(in oklab, var(--color-on-surface) 60%, transparent)}}.text-on-surface\/70{color:#314158b3}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/70{color:color-mix(in oklab, var(--color-on-surface) 70%, transparent)}}.text-on-surface\/80{color:#314158cc}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/80{color:color-mix(in oklab, var(--color-on-surface) 80%, transparent)}}.text-on-warning{color:var(--color-on-warning)}.text-outline{color:var(--color-outline)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-success{color:var(--color-success)}.text-warning{color:var(--color-warning)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-danger{outline-color:var(--color-danger)}.outline-primary{outline-color:var(--color-primary)}.outline-secondary{outline-color:var(--color-secondary)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.peer-checked\:visible:is(:where(.peer):checked~*){visibility:visible}.peer-checked\:bg-primary:is(:where(.peer):checked~*){background-color:var(--color-primary)}.peer-focus\:outline-2:is(:where(.peer):focus~*){outline-style:var(--tw-outline-style);outline-width:2px}.peer-focus\:outline-offset-2:is(:where(.peer):focus~*){outline-offset:2px}.peer-focus\:outline-outline-strong:is(:where(.peer):focus~*){outline-color:var(--color-outline-strong)}.peer-focus\:peer-checked\:outline-primary:is(:where(.peer):focus~*):is(:where(.peer):checked~*){outline-color:var(--color-primary)}.peer-active\:outline-offset-0:is(:where(.peer):active~*){outline-offset:0px}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:border-none::file-selector-button{--tw-border-style:none;border-style:none}.file\:bg-surface-alt::file-selector-button{background-color:var(--color-surface-alt)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing) * 4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing) * 2)}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-on-surface-strong::file-selector-button{color:var(--color-on-surface-strong)}.before\:invisible:before{content:var(--tw-content);visibility:hidden}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:0}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:left-1\/2:before{content:var(--tw-content);left:50%}.before\:h-1\.5:before{content:var(--tw-content);height:calc(var(--spacing) * 1.5)}.before\:w-1\.5:before{content:var(--tw-content);width:calc(var(--spacing) * 1.5)}.before\:-translate-x-1\/2:before{content:var(--tw-content);--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.before\:-translate-y-1\/2:before{content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-on-primary:before{content:var(--tw-content);background-color:var(--color-on-primary)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:top-0:after{content:var(--tw-content);top:0}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-\[0\.0625rem\]:after{content:var(--tw-content);left:.0625rem}.after\:my-auto:after{content:var(--tw-content);margin-block:auto}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing) * 5)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing) * 5)}.after\:rounded-full:after{content:var(--tw-content);border-radius:3.40282e38px}.after\:bg-on-surface:after{content:var(--tw-content);background-color:var(--color-on-surface)}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.peer-checked\:after\:translate-x-5:is(:where(.peer):checked~*):after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.peer-checked\:after\:bg-on-primary:is(:where(.peer):checked~*):after{content:var(--tw-content);background-color:var(--color-on-primary)}.checked\:border-primary:checked{border-color:var(--color-primary)}.checked\:bg-primary:checked{background-color:var(--color-primary)}.checked\:before\:visible:checked:before{content:var(--tw-content);visibility:visible}.checked\:before\:bg-primary:checked:before{content:var(--tw-content);background-color:var(--color-primary)}@media (hover:hover){.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:bg-danger\/5:hover{background-color:#e400140d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-danger\/5:hover{background-color:color-mix(in oklab, var(--color-danger) 5%, transparent)}}.hover\:bg-info\/5:hover{background-color:#00a5ef0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-info\/5:hover{background-color:color-mix(in oklab, var(--color-info) 5%, transparent)}}.hover\:bg-primary\/5:hover{background-color:#4f39f60d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/5:hover{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.hover\:bg-surface-alt:hover{background-color:var(--color-surface-alt)}.hover\:bg-surface\/60:hover{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.hover\:bg-surface\/60:hover{background-color:color-mix(in oklab, var(--color-surface) 60%, transparent)}}.hover\:text-on-surface-strong:hover{color:var(--color-on-surface-strong)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-75:hover{opacity:.75}}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-offset-2:focus{outline-offset:2px}.focus\:outline-outline-strong:focus{outline-color:var(--color-outline-strong)}.focus\:outline-primary:focus,.checked\:focus\:outline-primary:checked:focus{outline-color:var(--color-primary)}.focus-visible\:bg-primary\/10:focus-visible{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.focus-visible\:bg-primary\/10:focus-visible{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.focus-visible\:text-on-surface-strong:focus-visible{color:var(--color-on-surface-strong)}.focus-visible\:underline:focus-visible{text-decoration-line:underline}.focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-danger:focus-visible{outline-color:var(--color-danger)}.focus-visible\:outline-info:focus-visible{outline-color:var(--color-info)}.focus-visible\:outline-outline:focus-visible{outline-color:var(--color-outline)}.focus-visible\:outline-primary:focus-visible{outline-color:var(--color-primary)}.focus-visible\:outline-secondary:focus-visible{outline-color:var(--color-secondary)}.focus-visible\:outline-success:focus-visible{outline-color:var(--color-success)}.focus-visible\:outline-warning:focus-visible{outline-color:var(--color-warning)}.active\:opacity-100:active{opacity:1}.active\:outline-offset-0:active{outline-offset:0px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-75:disabled{opacity:.75}.has-checked\:text-on-surface-strong:has(:checked){color:var(--color-on-surface-strong)}.has-disabled\:cursor-not-allowed:has(:disabled){cursor:not-allowed}.has-disabled\:opacity-75:has(:disabled){opacity:.75}.has-\[\:checked\]\:border-primary:has(:checked){border-color:var(--color-primary)}.aria-\[current\=page\]\:bg-primary\/10[aria-current=page]{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.aria-\[current\=page\]\:bg-primary\/10[aria-current=page]{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.aria-\[current\=page\]\:font-semibold[aria-current=page]{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.aria-\[current\=page\]\:text-on-surface-strong[aria-current=page]{color:var(--color-on-surface-strong)}.aria-\[current\=page\]\:text-primary[aria-current=page]{color:var(--color-primary)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:48rem){.md\:top-\[unset\]{top:unset}.md\:right-0{right:0}.md\:bottom-0{bottom:0}.md\:left-\[unset\]{left:unset}.md\:ml-60{margin-left:calc(var(--spacing) * 60)}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-64{height:calc(var(--spacing) * 64)}.md\:max-w-sm{max-width:var(--container-sm)}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:translate-x-24{--tw-translate-x:calc(var(--spacing) * 24);translate:var(--tw-translate-x) var(--tw-translate-y)}}@media (min-width:64rem){.lg\:static{position:static}.lg\:z-auto{z-index:auto}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:hidden{display:none}.lg\:w-64{width:calc(var(--spacing) * 64)}.lg\:shrink-0{flex-shrink:0}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:self-start{align-self:flex-start}.lg\:overflow-visible{overflow:visible}.lg\:rounded-radius{border-radius:var(--radius-radius)}.lg\:border{border-style:var(--tw-border-style);border-width:1px}.lg\:p-3{padding:calc(var(--spacing) * 3)}}@media (min-width:80rem){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}:where(.dark\:divide-outline-dark:where([data-theme=dark],[data-theme=dark] *)>:not(:last-child)){border-color:var(--color-outline-dark)}.dark\:border-danger:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-danger)}.dark\:border-info:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-info)}.dark\:border-outline-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-outline-dark)}.dark\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-primary-dark)}.dark\:border-primary-dark\/40:where([data-theme=dark],[data-theme=dark] *){border-color:#7d87ff66}@supports (color:color-mix(in lab, red, red)){.dark\:border-primary-dark\/40:where([data-theme=dark],[data-theme=dark] *){border-color:color-mix(in oklab, var(--color-primary-dark) 40%, transparent)}}.dark\:border-secondary-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-secondary-dark)}.dark\:border-success:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-success)}.dark\:border-warning:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-warning)}.dark\:bg-danger:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-danger)}.dark\:bg-info:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-info)}.dark\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-primary-dark)}.dark\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *){background-color:#7d87ff1a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-primary-dark) 10%, transparent)}}.dark\:bg-secondary-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-secondary-dark)}.dark\:bg-success:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-success)}.dark\:bg-surface-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-surface-dark)}.dark\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-surface-dark-alt)}.dark\:bg-surface-dark-alt\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#1d293d66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark-alt\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark-alt) 40%, transparent)}}.dark\:bg-surface-dark-alt\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1d293d80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark-alt\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark-alt) 50%, transparent)}}.dark\:bg-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#0f172b66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark) 40%, transparent)}}.dark\:bg-surface-dark\/95:where([data-theme=dark],[data-theme=dark] *){background-color:#0f172bf2}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark\/95:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark) 95%, transparent)}}.dark\:bg-warning:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-warning)}.dark\:text-danger:where([data-theme=dark],[data-theme=dark] *){color:var(--color-danger)}.dark\:text-on-danger:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-danger)}.dark\:text-on-info:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-info)}.dark\:text-on-primary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-primary-dark)}.dark\:text-on-secondary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-secondary-dark)}.dark\:text-on-success:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-success)}.dark\:text-on-surface-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-surface-dark)}.dark\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-surface-dark-strong)}.dark\:text-on-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){color:#cad5e266}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 40%, transparent)}}.dark\:text-on-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *){color:#cad5e299}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 60%, transparent)}}.dark\:text-on-surface-dark\/70:where([data-theme=dark],[data-theme=dark] *){color:#cad5e2b3}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/70:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 70%, transparent)}}.dark\:text-on-surface-dark\/80:where([data-theme=dark],[data-theme=dark] *){color:#cad5e2cc}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/80:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 80%, transparent)}}.dark\:text-on-warning:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-warning)}.dark\:text-outline-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-outline-dark)}.dark\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-primary-dark)}.dark\:text-secondary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-secondary-dark)}.dark\:text-warning:where([data-theme=dark],[data-theme=dark] *){color:var(--color-warning)}.dark\:peer-checked\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):checked~*){background-color:var(--color-primary-dark)}.dark\:peer-focus\:outline-outline-dark-strong:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):focus~*){outline-color:var(--color-outline-dark-strong)}.dark\:peer-focus\:peer-checked\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):focus~*):is(:where(.peer):checked~*){outline-color:var(--color-primary-dark)}.dark\:file\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *)::file-selector-button{background-color:var(--color-surface-dark-alt)}.dark\:file\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *)::file-selector-button{color:var(--color-on-surface-dark-strong)}.dark\:before\:bg-on-primary-dark:where([data-theme=dark],[data-theme=dark] *):before{content:var(--tw-content);background-color:var(--color-on-primary-dark)}.dark\:after\:bg-on-surface-dark:where([data-theme=dark],[data-theme=dark] *):after{content:var(--tw-content);background-color:var(--color-on-surface-dark)}.dark\:peer-checked\:after\:bg-on-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):checked~*):after{content:var(--tw-content);background-color:var(--color-on-primary-dark)}.dark\:checked\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked{border-color:var(--color-primary-dark)}.dark\:checked\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked{background-color:var(--color-primary-dark)}.dark\:checked\:before\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked:before{content:var(--tw-content);background-color:var(--color-primary-dark)}@media (hover:hover){.dark\:hover\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):hover{border-color:var(--color-primary-dark)}.dark\:hover\:bg-primary-dark\/5:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#7d87ff0d}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-primary-dark\/5:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab, var(--color-primary-dark) 5%, transparent)}}.dark\:hover\:bg-surface-dark:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-surface-dark)}.dark\:hover\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-surface-dark-alt)}.dark\:hover\:bg-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#0f172b99}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab, var(--color-surface-dark) 60%, transparent)}}.dark\:hover\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-on-surface-dark-strong)}.dark\:hover\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-primary-dark)}}.dark\:focus\:outline-outline-dark-strong:where([data-theme=dark],[data-theme=dark] *):focus{outline-color:var(--color-outline-dark-strong)}.dark\:checked\:focus\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked:focus{outline-color:var(--color-primary-dark)}.dark\:focus-visible\:outline-danger:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-danger)}.dark\:focus-visible\:outline-info:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-info)}.dark\:focus-visible\:outline-outline-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-outline-dark)}.dark\:focus-visible\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-primary-dark)}.dark\:focus-visible\:outline-secondary-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-secondary-dark)}.dark\:focus-visible\:outline-success:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-success)}.dark\:focus-visible\:outline-warning:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-warning)}.dark\:has-checked\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *):has(:checked){color:var(--color-on-surface-dark-strong)}.dark\:has-\[\:checked\]\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):has(:checked){border-color:var(--color-primary-dark)}.dark\:aria-\[current\=page\]\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{background-color:#7d87ff1a}@supports (color:color-mix(in lab, red, red)){.dark\:aria-\[current\=page\]\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{background-color:color-mix(in oklab, var(--color-primary-dark) 10%, transparent)}}.dark\:aria-\[current\=page\]\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{color:var(--color-on-surface-dark-strong)}.dark\:aria-\[current\=page\]\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{color:var(--color-primary-dark)}}[x-cloak]{display:none!important}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-content{syntax:"*";inherits:false;initial-value:""} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-600:oklch(57.7% .245 27.325);--color-amber-500:oklch(76.9% .188 70.08);--color-green-600:oklch(62.7% .194 149.214);--color-emerald-600:oklch(59.6% .145 163.225);--color-sky-500:oklch(68.5% .169 237.323);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-600:oklch(51.1% .262 276.966);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-sm:.25rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-surface:var(--color-white);--color-surface-alt:var(--color-slate-100);--color-on-surface:var(--color-slate-700);--color-on-surface-strong:var(--color-slate-900);--color-primary:var(--color-indigo-600);--color-on-primary:var(--color-white);--color-secondary:var(--color-slate-600);--color-on-secondary:var(--color-white);--color-outline:var(--color-slate-300);--color-outline-strong:var(--color-slate-800);--color-surface-dark:var(--color-slate-900);--color-surface-dark-alt:var(--color-slate-800);--color-on-surface-dark:var(--color-slate-300);--color-on-surface-dark-strong:var(--color-white);--color-primary-dark:var(--color-indigo-400);--color-on-primary-dark:var(--color-slate-950);--color-secondary-dark:var(--color-slate-300);--color-on-secondary-dark:var(--color-slate-950);--color-outline-dark:var(--color-slate-700);--color-outline-dark-strong:var(--color-slate-300);--color-info:var(--color-sky-500);--color-on-info:var(--color-white);--color-success:var(--color-green-600);--color-on-success:var(--color-white);--color-warning:var(--color-amber-500);--color-on-warning:var(--color-white);--color-danger:var(--color-red-600);--color-on-danger:var(--color-white);--radius-radius:.375rem}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-x-8{inset-inline:calc(var(--spacing) * 8)}.inset-y-0{inset-block:0}.-top-1{top:calc(var(--spacing) * -1)}.top-0{top:0}.top-1\/2{top:50%}.top-full{top:100%}.-right-1{right:calc(var(--spacing) * -1)}.right-0{right:0}.right-3{right:calc(var(--spacing) * 3)}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-99{z-index:99}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-2{margin-right:calc(var(--spacing) * 2)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-10{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-14{width:calc(var(--spacing) * 14);height:calc(var(--spacing) * 14)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.size-24{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}.size-full{width:100%;height:100%}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-16{height:calc(var(--spacing) * 16)}.h-44{height:calc(var(--spacing) * 44)}.h-fit{height:fit-content}.h-px{height:1px}.max-h-56{max-height:calc(var(--spacing) * 56)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-56{width:calc(var(--spacing) * 56)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-4{min-width:calc(var(--spacing) * 4)}.min-w-40{min-width:calc(var(--spacing) * 40)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-24{--tw-translate-x:calc(var(--spacing) * -24);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-60{--tw-translate-x:calc(var(--spacing) * -60);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-0{rotate:0deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.appearance-none{appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-outline>:not(:last-child)){border-color:var(--color-outline)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-radius{border-radius:var(--radius-radius)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-l-radius{border-top-left-radius:var(--radius-radius);border-bottom-left-radius:var(--radius-radius)}.rounded-r-radius{border-top-right-radius:var(--radius-radius);border-bottom-right-radius:var(--radius-radius)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-danger{border-color:var(--color-danger)}.border-info{border-color:var(--color-info)}.border-outline{border-color:var(--color-outline)}.border-primary{border-color:var(--color-primary)}.border-primary\/40{border-color:#4f39f666}@supports (color:color-mix(in lab, red, red)){.border-primary\/40{border-color:color-mix(in oklab, var(--color-primary) 40%, transparent)}}.border-secondary{border-color:var(--color-secondary)}.border-success{border-color:var(--color-success)}.border-warning{border-color:var(--color-warning)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-danger{background-color:var(--color-danger)}.bg-danger\/10{background-color:#e400141a}@supports (color:color-mix(in lab, red, red)){.bg-danger\/10{background-color:color-mix(in oklab, var(--color-danger) 10%, transparent)}}.bg-danger\/15{background-color:#e4001426}@supports (color:color-mix(in lab, red, red)){.bg-danger\/15{background-color:color-mix(in oklab, var(--color-danger) 15%, transparent)}}.bg-info{background-color:var(--color-info)}.bg-info\/10{background-color:#00a5ef1a}@supports (color:color-mix(in lab, red, red)){.bg-info\/10{background-color:color-mix(in oklab, var(--color-info) 10%, transparent)}}.bg-info\/15{background-color:#00a5ef26}@supports (color:color-mix(in lab, red, red)){.bg-info\/15{background-color:color-mix(in oklab, var(--color-info) 15%, transparent)}}.bg-outline{background-color:var(--color-outline)}.bg-primary{background-color:var(--color-primary)}.bg-primary\/5{background-color:#4f39f60d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-primary\/10{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.bg-secondary{background-color:var(--color-secondary)}.bg-success{background-color:var(--color-success)}.bg-success\/10{background-color:#00a5441a}@supports (color:color-mix(in lab, red, red)){.bg-success\/10{background-color:color-mix(in oklab, var(--color-success) 10%, transparent)}}.bg-success\/15{background-color:#00a54426}@supports (color:color-mix(in lab, red, red)){.bg-success\/15{background-color:color-mix(in oklab, var(--color-success) 15%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-alt{background-color:var(--color-surface-alt)}.bg-surface-alt\/40{background-color:#f1f5f966}@supports (color:color-mix(in lab, red, red)){.bg-surface-alt\/40{background-color:color-mix(in oklab, var(--color-surface-alt) 40%, transparent)}}.bg-surface-alt\/50{background-color:#f1f5f980}@supports (color:color-mix(in lab, red, red)){.bg-surface-alt\/50{background-color:color-mix(in oklab, var(--color-surface-alt) 50%, transparent)}}.bg-surface\/40{background-color:#fff6}@supports (color:color-mix(in lab, red, red)){.bg-surface\/40{background-color:color-mix(in oklab, var(--color-surface) 40%, transparent)}}.bg-surface\/95{background-color:#fffffff2}@supports (color:color-mix(in lab, red, red)){.bg-surface\/95{background-color:color-mix(in oklab, var(--color-surface) 95%, transparent)}}.bg-transparent{background-color:#0000}.bg-warning{background-color:var(--color-warning)}.bg-warning\/10{background-color:#f99c001a}@supports (color:color-mix(in lab, red, red)){.bg-warning\/10{background-color:color-mix(in oklab, var(--color-warning) 10%, transparent)}}.bg-warning\/15{background-color:#f99c0026}@supports (color:color-mix(in lab, red, red)){.bg-warning\/15{background-color:color-mix(in oklab, var(--color-warning) 15%, transparent)}}.object-cover{object-fit:cover}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:0}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pr-7{padding-right:calc(var(--spacing) * 7)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-1{padding-left:var(--spacing)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-pretty{text-wrap:pretty}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.text-danger{color:var(--color-danger)}.text-info{color:var(--color-info)}.text-on-danger{color:var(--color-on-danger)}.text-on-info{color:var(--color-on-info)}.text-on-primary{color:var(--color-on-primary)}.text-on-secondary{color:var(--color-on-secondary)}.text-on-success{color:var(--color-on-success)}.text-on-surface{color:var(--color-on-surface)}.text-on-surface-strong{color:var(--color-on-surface-strong)}.text-on-surface\/40{color:#31415866}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/40{color:color-mix(in oklab, var(--color-on-surface) 40%, transparent)}}.text-on-surface\/50{color:#31415880}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/50{color:color-mix(in oklab, var(--color-on-surface) 50%, transparent)}}.text-on-surface\/60{color:#31415899}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/60{color:color-mix(in oklab, var(--color-on-surface) 60%, transparent)}}.text-on-surface\/70{color:#314158b3}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/70{color:color-mix(in oklab, var(--color-on-surface) 70%, transparent)}}.text-on-surface\/80{color:#314158cc}@supports (color:color-mix(in lab, red, red)){.text-on-surface\/80{color:color-mix(in oklab, var(--color-on-surface) 80%, transparent)}}.text-on-warning{color:var(--color-on-warning)}.text-outline{color:var(--color-outline)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-success{color:var(--color-success)}.text-warning{color:var(--color-warning)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-danger{outline-color:var(--color-danger)}.outline-primary{outline-color:var(--color-primary)}.outline-secondary{outline-color:var(--color-secondary)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.peer-checked\:visible:is(:where(.peer):checked~*){visibility:visible}.peer-checked\:bg-primary:is(:where(.peer):checked~*){background-color:var(--color-primary)}.peer-focus\:outline-2:is(:where(.peer):focus~*){outline-style:var(--tw-outline-style);outline-width:2px}.peer-focus\:outline-offset-2:is(:where(.peer):focus~*){outline-offset:2px}.peer-focus\:outline-outline-strong:is(:where(.peer):focus~*){outline-color:var(--color-outline-strong)}.peer-focus\:peer-checked\:outline-primary:is(:where(.peer):focus~*):is(:where(.peer):checked~*){outline-color:var(--color-primary)}.peer-active\:outline-offset-0:is(:where(.peer):active~*){outline-offset:0px}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:border-none::file-selector-button{--tw-border-style:none;border-style:none}.file\:bg-surface-alt::file-selector-button{background-color:var(--color-surface-alt)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing) * 4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing) * 2)}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-on-surface-strong::file-selector-button{color:var(--color-on-surface-strong)}.before\:invisible:before{content:var(--tw-content);visibility:hidden}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:0}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:left-1\/2:before{content:var(--tw-content);left:50%}.before\:h-1\.5:before{content:var(--tw-content);height:calc(var(--spacing) * 1.5)}.before\:w-1\.5:before{content:var(--tw-content);width:calc(var(--spacing) * 1.5)}.before\:-translate-x-1\/2:before{content:var(--tw-content);--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.before\:-translate-y-1\/2:before{content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-on-primary:before{content:var(--tw-content);background-color:var(--color-on-primary)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:top-0:after{content:var(--tw-content);top:0}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-\[0\.0625rem\]:after{content:var(--tw-content);left:.0625rem}.after\:my-auto:after{content:var(--tw-content);margin-block:auto}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing) * 5)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing) * 5)}.after\:rounded-full:after{content:var(--tw-content);border-radius:3.40282e38px}.after\:bg-on-surface:after{content:var(--tw-content);background-color:var(--color-on-surface)}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.peer-checked\:after\:translate-x-5:is(:where(.peer):checked~*):after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.peer-checked\:after\:bg-on-primary:is(:where(.peer):checked~*):after{content:var(--tw-content);background-color:var(--color-on-primary)}.checked\:border-primary:checked{border-color:var(--color-primary)}.checked\:bg-primary:checked{background-color:var(--color-primary)}.checked\:before\:visible:checked:before{content:var(--tw-content);visibility:visible}.checked\:before\:bg-primary:checked:before{content:var(--tw-content);background-color:var(--color-primary)}@media (hover:hover){.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:bg-danger\/5:hover{background-color:#e400140d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-danger\/5:hover{background-color:color-mix(in oklab, var(--color-danger) 5%, transparent)}}.hover\:bg-info\/5:hover{background-color:#00a5ef0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-info\/5:hover{background-color:color-mix(in oklab, var(--color-info) 5%, transparent)}}.hover\:bg-primary\/5:hover{background-color:#4f39f60d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/5:hover{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.hover\:bg-surface-alt:hover{background-color:var(--color-surface-alt)}.hover\:bg-surface\/60:hover{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.hover\:bg-surface\/60:hover{background-color:color-mix(in oklab, var(--color-surface) 60%, transparent)}}.hover\:text-on-surface-strong:hover{color:var(--color-on-surface-strong)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-75:hover{opacity:.75}}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-offset-2:focus{outline-offset:2px}.focus\:outline-outline-strong:focus{outline-color:var(--color-outline-strong)}.focus\:outline-primary:focus,.checked\:focus\:outline-primary:checked:focus{outline-color:var(--color-primary)}.focus-visible\:bg-primary\/10:focus-visible{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.focus-visible\:bg-primary\/10:focus-visible{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.focus-visible\:text-on-surface-strong:focus-visible{color:var(--color-on-surface-strong)}.focus-visible\:underline:focus-visible{text-decoration-line:underline}.focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-danger:focus-visible{outline-color:var(--color-danger)}.focus-visible\:outline-info:focus-visible{outline-color:var(--color-info)}.focus-visible\:outline-outline:focus-visible{outline-color:var(--color-outline)}.focus-visible\:outline-primary:focus-visible{outline-color:var(--color-primary)}.focus-visible\:outline-secondary:focus-visible{outline-color:var(--color-secondary)}.focus-visible\:outline-success:focus-visible{outline-color:var(--color-success)}.focus-visible\:outline-warning:focus-visible{outline-color:var(--color-warning)}.active\:opacity-100:active{opacity:1}.active\:outline-offset-0:active{outline-offset:0px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-75:disabled{opacity:.75}.has-checked\:text-on-surface-strong:has(:checked){color:var(--color-on-surface-strong)}.has-disabled\:cursor-not-allowed:has(:disabled){cursor:not-allowed}.has-disabled\:opacity-75:has(:disabled){opacity:.75}.has-\[\:checked\]\:border-primary:has(:checked){border-color:var(--color-primary)}.aria-\[current\=page\]\:bg-primary\/10[aria-current=page]{background-color:#4f39f61a}@supports (color:color-mix(in lab, red, red)){.aria-\[current\=page\]\:bg-primary\/10[aria-current=page]{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.aria-\[current\=page\]\:font-semibold[aria-current=page]{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.aria-\[current\=page\]\:text-on-surface-strong[aria-current=page]{color:var(--color-on-surface-strong)}.aria-\[current\=page\]\:text-primary[aria-current=page]{color:var(--color-primary)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:48rem){.md\:top-\[unset\]{top:unset}.md\:right-0{right:0}.md\:bottom-0{bottom:0}.md\:left-\[unset\]{left:unset}.md\:ml-60{margin-left:calc(var(--spacing) * 60)}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-64{height:calc(var(--spacing) * 64)}.md\:max-w-sm{max-width:var(--container-sm)}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:translate-x-24{--tw-translate-x:calc(var(--spacing) * 24);translate:var(--tw-translate-x) var(--tw-translate-y)}}@media (min-width:64rem){.lg\:static{position:static}.lg\:z-auto{z-index:auto}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:hidden{display:none}.lg\:w-64{width:calc(var(--spacing) * 64)}.lg\:shrink-0{flex-shrink:0}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:self-start{align-self:flex-start}.lg\:overflow-visible{overflow:visible}.lg\:rounded-radius{border-radius:var(--radius-radius)}.lg\:border{border-style:var(--tw-border-style);border-width:1px}.lg\:p-3{padding:calc(var(--spacing) * 3)}}@media (min-width:80rem){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}:where(.dark\:divide-outline-dark:where([data-theme=dark],[data-theme=dark] *)>:not(:last-child)){border-color:var(--color-outline-dark)}.dark\:border-danger:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-danger)}.dark\:border-info:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-info)}.dark\:border-outline-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-outline-dark)}.dark\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-primary-dark)}.dark\:border-primary-dark\/40:where([data-theme=dark],[data-theme=dark] *){border-color:#7d87ff66}@supports (color:color-mix(in lab, red, red)){.dark\:border-primary-dark\/40:where([data-theme=dark],[data-theme=dark] *){border-color:color-mix(in oklab, var(--color-primary-dark) 40%, transparent)}}.dark\:border-secondary-dark:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-secondary-dark)}.dark\:border-success:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-success)}.dark\:border-warning:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-warning)}.dark\:bg-danger:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-danger)}.dark\:bg-info:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-info)}.dark\:bg-outline-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-outline-dark)}.dark\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-primary-dark)}.dark\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *){background-color:#7d87ff1a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-primary-dark) 10%, transparent)}}.dark\:bg-secondary-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-secondary-dark)}.dark\:bg-success:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-success)}.dark\:bg-surface-dark:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-surface-dark)}.dark\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-surface-dark-alt)}.dark\:bg-surface-dark-alt\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#1d293d66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark-alt\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark-alt) 40%, transparent)}}.dark\:bg-surface-dark-alt\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1d293d80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark-alt\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark-alt) 50%, transparent)}}.dark\:bg-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#0f172b66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark) 40%, transparent)}}.dark\:bg-surface-dark\/95:where([data-theme=dark],[data-theme=dark] *){background-color:#0f172bf2}@supports (color:color-mix(in lab, red, red)){.dark\:bg-surface-dark\/95:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab, var(--color-surface-dark) 95%, transparent)}}.dark\:bg-warning:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-warning)}.dark\:text-danger:where([data-theme=dark],[data-theme=dark] *){color:var(--color-danger)}.dark\:text-on-danger:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-danger)}.dark\:text-on-info:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-info)}.dark\:text-on-primary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-primary-dark)}.dark\:text-on-secondary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-secondary-dark)}.dark\:text-on-success:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-success)}.dark\:text-on-surface-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-surface-dark)}.dark\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-surface-dark-strong)}.dark\:text-on-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){color:#cad5e266}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/40:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 40%, transparent)}}.dark\:text-on-surface-dark\/50:where([data-theme=dark],[data-theme=dark] *){color:#cad5e280}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/50:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 50%, transparent)}}.dark\:text-on-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *){color:#cad5e299}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 60%, transparent)}}.dark\:text-on-surface-dark\/70:where([data-theme=dark],[data-theme=dark] *){color:#cad5e2b3}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/70:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 70%, transparent)}}.dark\:text-on-surface-dark\/80:where([data-theme=dark],[data-theme=dark] *){color:#cad5e2cc}@supports (color:color-mix(in lab, red, red)){.dark\:text-on-surface-dark\/80:where([data-theme=dark],[data-theme=dark] *){color:color-mix(in oklab, var(--color-on-surface-dark) 80%, transparent)}}.dark\:text-on-warning:where([data-theme=dark],[data-theme=dark] *){color:var(--color-on-warning)}.dark\:text-outline-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-outline-dark)}.dark\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-primary-dark)}.dark\:text-secondary-dark:where([data-theme=dark],[data-theme=dark] *){color:var(--color-secondary-dark)}.dark\:text-warning:where([data-theme=dark],[data-theme=dark] *){color:var(--color-warning)}.dark\:peer-checked\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):checked~*){background-color:var(--color-primary-dark)}.dark\:peer-focus\:outline-outline-dark-strong:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):focus~*){outline-color:var(--color-outline-dark-strong)}.dark\:peer-focus\:peer-checked\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):focus~*):is(:where(.peer):checked~*){outline-color:var(--color-primary-dark)}.dark\:file\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *)::file-selector-button{background-color:var(--color-surface-dark-alt)}.dark\:file\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *)::file-selector-button{color:var(--color-on-surface-dark-strong)}.dark\:before\:bg-on-primary-dark:where([data-theme=dark],[data-theme=dark] *):before{content:var(--tw-content);background-color:var(--color-on-primary-dark)}.dark\:after\:bg-on-surface-dark:where([data-theme=dark],[data-theme=dark] *):after{content:var(--tw-content);background-color:var(--color-on-surface-dark)}.dark\:peer-checked\:after\:bg-on-primary-dark:where([data-theme=dark],[data-theme=dark] *):is(:where(.peer):checked~*):after{content:var(--tw-content);background-color:var(--color-on-primary-dark)}.dark\:checked\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked{border-color:var(--color-primary-dark)}.dark\:checked\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked{background-color:var(--color-primary-dark)}.dark\:checked\:before\:bg-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked:before{content:var(--tw-content);background-color:var(--color-primary-dark)}@media (hover:hover){.dark\:hover\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):hover{border-color:var(--color-primary-dark)}.dark\:hover\:bg-primary-dark\/5:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#7d87ff0d}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-primary-dark\/5:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab, var(--color-primary-dark) 5%, transparent)}}.dark\:hover\:bg-surface-dark:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-surface-dark)}.dark\:hover\:bg-surface-dark-alt:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-surface-dark-alt)}.dark\:hover\:bg-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#0f172b99}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-surface-dark\/60:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab, var(--color-surface-dark) 60%, transparent)}}.dark\:hover\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-on-surface-dark-strong)}.dark\:hover\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-primary-dark)}}.dark\:focus\:outline-outline-dark-strong:where([data-theme=dark],[data-theme=dark] *):focus{outline-color:var(--color-outline-dark-strong)}.dark\:checked\:focus\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):checked:focus{outline-color:var(--color-primary-dark)}.dark\:focus-visible\:outline-danger:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-danger)}.dark\:focus-visible\:outline-info:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-info)}.dark\:focus-visible\:outline-outline-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-outline-dark)}.dark\:focus-visible\:outline-primary-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-primary-dark)}.dark\:focus-visible\:outline-secondary-dark:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-secondary-dark)}.dark\:focus-visible\:outline-success:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-success)}.dark\:focus-visible\:outline-warning:where([data-theme=dark],[data-theme=dark] *):focus-visible{outline-color:var(--color-warning)}.dark\:has-checked\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *):has(:checked){color:var(--color-on-surface-dark-strong)}.dark\:has-\[\:checked\]\:border-primary-dark:where([data-theme=dark],[data-theme=dark] *):has(:checked){border-color:var(--color-primary-dark)}.dark\:aria-\[current\=page\]\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{background-color:#7d87ff1a}@supports (color:color-mix(in lab, red, red)){.dark\:aria-\[current\=page\]\:bg-primary-dark\/10:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{background-color:color-mix(in oklab, var(--color-primary-dark) 10%, transparent)}}.dark\:aria-\[current\=page\]\:text-on-surface-dark-strong:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{color:var(--color-on-surface-dark-strong)}.dark\:aria-\[current\=page\]\:text-primary-dark:where([data-theme=dark],[data-theme=dark] *)[aria-current=page]{color:var(--color-primary-dark)}}[x-cloak]{display:none!important}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-content{syntax:"*";inherits:false;initial-value:""} \ No newline at end of file diff --git a/assets/views/auth/login.html b/assets/views/auth/login.html index 925b678..bb053ea 100644 --- a/assets/views/auth/login.html +++ b/assets/views/auth/login.html @@ -46,6 +46,13 @@ {{ ui::button(label=t(key="login-auth", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }} +
+ + {{ t(key="auth-or", lang=lang | default(value='sk')) }} + +
+ {{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='') }} +

{{ t(key="login-no-account", lang=lang | default(value='sk')) }} +

+ {{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='') }} +

{{ t(key="login-have-account", lang=lang | default(value='sk')) }} = 64 bytes). Override in prod. + secret_key: {{ get_env(name="OAUTH_PRIVATE_KEY", default="144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130") }} + authorization_code: + - client_identifier: google + client_credentials: + client_id: {{ get_env(name="OAUTH_CLIENT_ID", default="oauth_client_id") }} + client_secret: {{ get_env(name="OAUTH_CLIENT_SECRET", default="oauth_client_secret") }} + url_config: + auth_url: {{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }} + token_url: {{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }} + redirect_url: {{ get_env(name="OAUTH_REDIRECT_URL", default="http://localhost:5150/api/oauth2/google/callback/cookie") }} + profile_url: {{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }} + scopes: + - "https://www.googleapis.com/auth/userinfo.email" + - "https://www.googleapis.com/auth/userinfo.profile" + cookie_config: + # After loco-oauth2 sets its session cookie it redirects here, where we + # mint our own auth_token cookie (see controllers/oauth2.rs::complete). + protected_url: {{ get_env(name="OAUTH_PROTECTED_URL", default="http://localhost:5150/api/oauth2/protected") }} + timeout_seconds: 600 diff --git a/config/production.yaml b/config/production.yaml index abac1a5..eee8bd4 100644 --- a/config/production.yaml +++ b/config/production.yaml @@ -55,3 +55,25 @@ auth: settings: admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}" uploads_root: "{{ get_env(name="UPLOADS_ROOT", default="data/uploads") }}" + +# loco-oauth2 social login. All values must come from the environment in prod; +# OAUTH_REDIRECT_URL / OAUTH_PROTECTED_URL must use the real public origin. +initializers: + oauth2: + secret_key: "{{ get_env(name="OAUTH_PRIVATE_KEY") }}" + authorization_code: + - client_identifier: google + client_credentials: + client_id: "{{ get_env(name="OAUTH_CLIENT_ID") }}" + client_secret: "{{ get_env(name="OAUTH_CLIENT_SECRET") }}" + url_config: + auth_url: "{{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }}" + token_url: "{{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }}" + redirect_url: "{{ get_env(name="OAUTH_REDIRECT_URL") }}" + profile_url: "{{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }}" + scopes: + - "https://www.googleapis.com/auth/userinfo.email" + - "https://www.googleapis.com/auth/userinfo.profile" + cookie_config: + protected_url: "{{ get_env(name="OAUTH_PROTECTED_URL") }}" + timeout_seconds: 600 diff --git a/docs/integrations/google-oauth.md b/docs/integrations/google-oauth.md new file mode 100644 index 0000000..bfb3c86 --- /dev/null +++ b/docs/integrations/google-oauth.md @@ -0,0 +1,104 @@ +# Google OAuth2 sign-in + +"Continue with Google" on `/login` and `/register` is wired through +[`loco-oauth2`](https://github.com/yinho999/loco-oauth2). The code is complete +and compiles; this doc is the checklist to make the live flow work. Until the +credentials below are set, the button reaches Google and fails at the consent +screen — the rest of auth (password login, registration, verification) is +unaffected. + +## How the flow works (for context) + +1. User clicks **Continue with Google** → `GET /api/oauth2/google` redirects to + Google's consent screen. +2. Google redirects back to `GET /api/oauth2/google/callback/cookie`. + loco-oauth2 exchanges the code, fetches the profile, upserts the user + (`OAuth2UserTrait::upsert_with_oauth`), stores an `o_auth2_sessions` row, sets + its own private session cookie, and redirects to `protected_url`. +3. `protected_url` = `GET /api/oauth2/protected` (our bridge, + `controllers/oauth2.rs::complete`). It mints **our** `auth_token` JWT cookie + and redirects: admins (email == `ADMIN_EMAIL`) → `/admin/dashboard`, + everyone else → `/`. + +From there the user is a normal logged-in user (same JWT cookie as a password +login; the Casbin layer and guards treat them identically). + +## 1. Create Google OAuth credentials + +1. Go to → create/select a project. +2. **APIs & Services → OAuth consent screen**: configure it (External), add the + `.../auth/userinfo.email` and `.../auth/userinfo.profile` scopes, and add + your Google account as a **test user** while the app is in "Testing". +3. **APIs & Services → Credentials → Create Credentials → OAuth client ID**: + - Application type: **Web application**. + - **Authorized redirect URIs** — add exactly (must match the config's + `redirect_url`, no trailing slash): + - dev: `http://localhost:5150/api/oauth2/google/callback/cookie` + - prod: `https://YOUR_DOMAIN/api/oauth2/google/callback/cookie` +4. Copy the generated **Client ID** and **Client secret**. + +## 2. Set environment variables (`.env`) + +Read by `config/development.yaml` → `initializers.oauth2` (and the prod +equivalent). dotenvy loads `.env` on boot. + +```bash +# Required +OAUTH_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com +OAUTH_CLIENT_SECRET=xxxxxxxx + +# Required in PRODUCTION (dev has working defaults) +OAUTH_PRIVATE_KEY="comma,separated,bytes >= 64 long" # key for loco-oauth2's private cookie jar +OAUTH_REDIRECT_URL=https://YOUR_DOMAIN/api/oauth2/google/callback/cookie +OAUTH_PROTECTED_URL=https://YOUR_DOMAIN/api/oauth2/protected +``` + +Notes: +- **dev** ships defaults for everything except `OAUTH_CLIENT_ID` / + `OAUTH_CLIENT_SECRET`, so locally you only need those two. +- `OAUTH_PRIVATE_KEY` must be ≥ 64 bytes (the dev default is a sample key — do + **not** reuse it in production). Generate a fresh one, e.g. + `python3 -c "import os;print(','.join(str(b) for b in os.urandom(64)))"`. +- `OAUTH_REDIRECT_URL` here and the Authorized redirect URI in the Google + console must be byte-for-byte identical. + +## 3. Run / test + +```bash +nix develop -c cargo loco start # MUST be inside nix develop (OpenSSL link, see memory) +``` + +- `auto_migrate: true` (dev) creates the `o_auth2_sessions` table on boot. +- Open `http://localhost:5150/login` → **Continue with Google** → consent → + you should land back on `/` logged in (cart/nav reflect the session). + +## 4. Production checklist + +- [ ] Separate OAuth client (or at least the prod redirect URI) in Google. +- [ ] OAuth consent screen **published** (not just "Testing"), or real users + get blocked. +- [ ] `OAUTH_PRIVATE_KEY` set to a fresh ≥64-byte key (not the dev sample). +- [ ] `OAUTH_REDIRECT_URL` / `OAUTH_PROTECTED_URL` use the real `https://` origin. +- [ ] `server.host` / public origin correct so cookies + redirects resolve. + +## Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| `redirect_uri_mismatch` at Google | Authorized redirect URI ≠ `OAUTH_REDIRECT_URL`. Make them identical (scheme, host, port, path, no trailing slash). | +| 403 / "access blocked: app not verified" | Add your account as a test user, or publish the consent screen. | +| `openssl-sys ... Could not find directory` at build | You ran `cargo` outside the dev shell. Use `nix develop -c cargo ...`. | +| Callback 500 / "could not create oauth2 store" | `initializers.oauth2` missing/invalid, or `OAUTH_PRIVATE_KEY` < 64 bytes. | +| Logged into Google but not into the app | The bridge (`/api/oauth2/protected`) didn't run — check `protected_url` (`OAUTH_PROTECTED_URL`) points at it. | + +## Where things live + +- Config: `config/development.yaml` / `config/production.yaml` → + `initializers.oauth2` +- Client store + session initializers: `src/initializers/oauth2.rs`, + `src/initializers/oauth2_session.rs` +- Routes + bridge handler: `src/controllers/oauth2.rs` +- User upsert (random password per advisory LOC-2025-04): `src/models/users.rs` + (`OAuth2UserTrait`) +- Session table: `src/models/o_auth2_sessions.rs` + + `migration/.../m20260618_000001_o_auth2_sessions.rs` diff --git a/flake.nix b/flake.nix index ab6d0c4..025db54 100644 --- a/flake.nix +++ b/flake.nix @@ -86,7 +86,10 @@ buildInputs = [ rust pkgs.pkg-config + # OpenSSL for crypto dependencies (loco-oauth2 -> oauth2/reqwest + # use native-tls); .dev provides headers + pkg-config metadata. pkgs.openssl + pkgs.openssl.dev pkgs.cmake pkgs.llvmPackages.clang pkgs.llvmPackages.libclang.lib diff --git a/migration/src/lib.rs b/migration/src/lib.rs index d84281f..8db63e6 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -30,6 +30,7 @@ mod m20260616_160000_add_parent_to_categories; mod m20260617_000001_add_carrier_to_shipping_methods; mod m20260617_000002_add_shipment_to_orders; mod m20260617_000003_add_phone_to_orders; +mod m20260618_000001_o_auth2_sessions; pub struct Migrator; #[async_trait::async_trait] @@ -64,6 +65,7 @@ impl MigratorTrait for Migrator { Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration), Box::new(m20260617_000002_add_shipment_to_orders::Migration), Box::new(m20260617_000003_add_phone_to_orders::Migration), + Box::new(m20260618_000001_o_auth2_sessions::Migration), // inject-above (do not remove this comment) ] } diff --git a/migration/src/m20260618_000001_o_auth2_sessions.rs b/migration/src/m20260618_000001_o_auth2_sessions.rs new file mode 100644 index 0000000..26a5093 --- /dev/null +++ b/migration/src/m20260618_000001_o_auth2_sessions.rs @@ -0,0 +1,29 @@ +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> { + // OAuth2 session store used by loco-oauth2 to correlate the provider's + // access token with a local user during the callback flow. `user` adds + // a user_id FK to the users table. + create_table( + m, + "o_auth2_sessions", + &[ + ("id", ColType::PkAuto), + ("session_id", ColType::StringUniq), + ("expires_at", ColType::TimestampWithTimeZone), + ], + &[("user", "")], + ) + .await + } + + async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> { + drop_table(m, "o_auth2_sessions").await + } +} diff --git a/src/app.rs b/src/app.rs index 6410387..a4e00e9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,8 @@ use std::{path::Path, sync::Arc}; use crate::{ controllers::{ admin_categories, admin_dashboard, admin_form, admin_orders, - admin_products, admin_shipping, auth, auth_pages, cart, checkout, home, i18n, media, shop, + admin_products, admin_shipping, auth, auth_pages, cart, checkout, home, i18n, media, oauth2, + shop, }, initializers, models::_entities::users, @@ -75,6 +76,8 @@ impl Hooks for App { Box::new(initializers::view_engine::ViewEngineInitializer), Box::new(initializers::admin_seeder::AdminSeeder), Box::new(initializers::shipping_seeder::ShippingSeeder), + Box::new(initializers::oauth2::OAuth2StoreInitializer), + Box::new(initializers::oauth2_session::OAuth2SessionInitializer), ]) } @@ -88,6 +91,7 @@ impl Hooks for App { // cross-cutting .add_route(auth::routes()) .add_route(auth_pages::routes()) + .add_route(oauth2::routes()) .add_route(i18n::routes()) .add_route(media::routes()) // admin diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 86151a9..85d8ed5 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod auth_pages; +pub mod oauth2; pub mod admin_categories; pub mod admin_dashboard; pub mod admin_form; diff --git a/src/controllers/oauth2.rs b/src/controllers/oauth2.rs new file mode 100644 index 0000000..5ff5096 --- /dev/null +++ b/src/controllers/oauth2.rs @@ -0,0 +1,56 @@ +//! HTML OAuth2 (Google) sign-in. +//! +//! The provider round-trip is handled by loco-oauth2's built-in authorize + +//! cookie-callback handlers. The callback upserts the user, stores an OAuth2 +//! session, sets loco-oauth2's *private* session cookie, and redirects to the +//! configured `protected_url` — which is our [`complete`] handler. There we +//! trade the OAuth2 session for OUR Loco `auth_token` JWT cookie, so the rest of +//! the app (the Casbin layer, `guard`, the unified `/login`) treats a Google +//! user exactly like a password login. Admins (matching `ADMIN_EMAIL`) land on +//! the dashboard, everyone else on the storefront. + +use loco_oauth2::controllers::{ + middleware::OAuth2CookieUser, + oauth2::{google_authorization_url, google_callback_cookie}, +}; +use loco_rs::prelude::*; + +use crate::{ + controllers::auth as auth_controller, + models::{o_auth2_sessions, users, users::OAuth2UserProfile}, + shared::guard, +}; + +type GoogleCookieUser = OAuth2CookieUser; + +/// Bridge from loco-oauth2's session cookie to our own auth cookie. +#[debug_handler] +async fn complete(State(ctx): State, user: GoogleCookieUser) -> Result { + let user: &users::Model = user.as_ref(); + let jwt_secret = ctx.config.get_jwt_config()?; + let token = user + .generate_jwt(&jwt_secret.secret, jwt_secret.expiration) + .or_else(|_| unauthorized("unauthorized!"))?; + let dest = if guard::is_admin(&ctx, user) { + "/admin/dashboard" + } else { + "/" + }; + format::render() + .cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration)])? + .redirect(dest) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("api/oauth2") + // Redirects the browser to Google's consent screen. + .add("/google", get(google_authorization_url)) + // Google redirects back here; loco-oauth2 exchanges the code, upserts + // the user, and redirects to `protected_url` (/api/oauth2/protected). + .add( + "/google/callback/cookie", + get(google_callback_cookie::), + ) + .add("/protected", get(complete)) +} diff --git a/src/initializers/mod.rs b/src/initializers/mod.rs index 0f2a40c..d5c0301 100644 --- a/src/initializers/mod.rs +++ b/src/initializers/mod.rs @@ -1,3 +1,5 @@ pub mod admin_seeder; +pub mod oauth2; +pub mod oauth2_session; pub mod shipping_seeder; pub mod view_engine; diff --git a/src/initializers/oauth2.rs b/src/initializers/oauth2.rs new file mode 100644 index 0000000..a21aeb7 --- /dev/null +++ b/src/initializers/oauth2.rs @@ -0,0 +1,36 @@ +//! Builds the loco-oauth2 client store from `initializers.oauth2` config and +//! injects it as an Axum extension so the oauth2 controllers can reach it. + +use axum::{Extension, Router as AxumRouter}; +use loco_oauth2::{config::Config as OAuth2Config, OAuth2ClientStore}; +use loco_rs::prelude::*; + +pub struct OAuth2StoreInitializer; + +#[async_trait] +impl Initializer for OAuth2StoreInitializer { + fn name(&self) -> String { + "oauth2-store".to_string() + } + + async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + let settings = ctx.config.initializers.clone().ok_or_else(|| { + Error::Message("Initializers config not configured for OAuth2".to_string()) + })?; + let oauth2_config_value = settings + .get("oauth2") + .ok_or_else(|| { + Error::Message("oauth2 config not found in initializers configuration".to_string()) + })? + .clone(); + let oauth2_config: OAuth2Config = oauth2_config_value.try_into().map_err(|e| { + tracing::error!(error = ?e, "could not convert oauth2 config from yaml"); + Error::Message("could not convert oauth2 config from yaml".to_string()) + })?; + let oauth2_store = OAuth2ClientStore::new(oauth2_config).map_err(|e| { + tracing::error!(error = ?e, "could not create oauth2 store from config"); + Error::Message("could not create oauth2 store from config".to_string()) + })?; + Ok(router.layer(Extension(oauth2_store))) + } +} diff --git a/src/initializers/oauth2_session.rs b/src/initializers/oauth2_session.rs new file mode 100644 index 0000000..96cdc03 --- /dev/null +++ b/src/initializers/oauth2_session.rs @@ -0,0 +1,25 @@ +//! tower-sessions layer that loco-oauth2 uses to hold the short-lived CSRF / +//! PKCE state between the authorize redirect and the provider callback. An +//! in-memory store is sufficient since the state only needs to survive the +//! round-trip to the provider. + +use axum::Router as AxumRouter; +use loco_rs::prelude::*; +use tower_sessions::{cookie::time::Duration, Expiry, MemoryStore, SessionManagerLayer}; + +pub struct OAuth2SessionInitializer; + +#[async_trait] +impl Initializer for OAuth2SessionInitializer { + fn name(&self) -> String { + "oauth2-session".to_string() + } + + async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(Duration::minutes(10))); + Ok(router.layer(session_layer)) + } +} diff --git a/src/models/_entities/mod.rs b/src/models/_entities/mod.rs index 91dce73..660d322 100644 --- a/src/models/_entities/mod.rs +++ b/src/models/_entities/mod.rs @@ -4,6 +4,7 @@ pub mod prelude; pub mod audit_logs; pub mod categories; +pub mod o_auth2_sessions; pub mod order_items; pub mod orders; pub mod product_images; diff --git a/src/models/_entities/o_auth2_sessions.rs b/src/models/_entities/o_auth2_sessions.rs new file mode 100644 index 0000000..3536d4e --- /dev/null +++ b/src/models/_entities/o_auth2_sessions.rs @@ -0,0 +1,36 @@ +//! `SeaORM` Entity for loco-oauth2 sessions. Hand-written to match the +//! `o_auth2_sessions` migration (the rest of `_entities/` is codegen; this table +//! is owned by the loco-oauth2 integration). + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "o_auth2_sessions")] +pub struct Model { + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + #[sea_orm(primary_key)] + pub id: i32, + pub session_id: String, + pub expires_at: DateTimeUtc, + pub user_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} diff --git a/src/models/_entities/prelude.rs b/src/models/_entities/prelude.rs index 393605e..7f2b205 100644 --- a/src/models/_entities/prelude.rs +++ b/src/models/_entities/prelude.rs @@ -2,6 +2,7 @@ pub use super::audit_logs::Entity as AuditLogs; pub use super::categories::Entity as Categories; +pub use super::o_auth2_sessions::Entity as OAuth2Sessions; pub use super::order_items::Entity as OrderItems; pub use super::orders::Entity as Orders; pub use super::product_images::Entity as ProductImages; diff --git a/src/models/mod.rs b/src/models/mod.rs index 206f25d..37f2ff8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -8,6 +8,7 @@ pub mod _entities; pub mod audit_logs; pub mod categories; +pub mod o_auth2_sessions; pub mod order_items; pub mod orders; pub mod product_images; diff --git a/src/models/o_auth2_sessions.rs b/src/models/o_auth2_sessions.rs new file mode 100644 index 0000000..46e57db --- /dev/null +++ b/src/models/o_auth2_sessions.rs @@ -0,0 +1,79 @@ +pub use super::_entities::o_auth2_sessions::{ActiveModel, Column, Entity, Model}; +use crate::models::{o_auth2_sessions, users}; +use async_trait::async_trait; +use chrono::Utc; +use loco_oauth2::base_oauth2::{basic::BasicTokenResponse, TokenResponse}; +use loco_oauth2::models::oauth2_sessions::OAuth2SessionsTrait; +use loco_rs::prelude::*; +use sea_orm::entity::prelude::*; + +pub type OAuth2Sessions = Entity; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = sea_orm::ActiveValue::Set(Utc::now()); + Ok(this) + } else { + Ok(self) + } + } +} + +#[async_trait] +impl OAuth2SessionsTrait for Model { + /// Whether the session identified by `session_id` has expired. + async fn is_expired(db: &DatabaseConnection, session_id: &str) -> ModelResult { + let session = o_auth2_sessions::Entity::find() + .filter(o_auth2_sessions::Column::SessionId.eq(session_id)) + .one(db) + .await? + .ok_or_else(|| ModelError::EntityNotFound)?; + Ok(session.expires_at < Utc::now()) + } + + /// Create or refresh the session row for `user` from the provider token. + async fn upsert_with_oauth2( + db: &DatabaseConnection, + token: &BasicTokenResponse, + user: &users::Model, + ) -> ModelResult { + let txn = db.begin().await?; + let session_id = token.access_token().secret().clone(); + let expires_at = Utc::now() + + token + .expires_in() + .unwrap_or(std::time::Duration::from_secs(3600)); + + let session = match o_auth2_sessions::Entity::find() + .filter(o_auth2_sessions::Column::UserId.eq(user.id)) + .one(&txn) + .await? + { + Some(session) => { + let mut session: o_auth2_sessions::ActiveModel = session.into(); + session.session_id = ActiveValue::set(session_id); + session.expires_at = ActiveValue::set(expires_at); + session.updated_at = ActiveValue::set(Utc::now()); + session.update(&txn).await? + } + None => { + o_auth2_sessions::ActiveModel { + session_id: ActiveValue::set(session_id), + expires_at: ActiveValue::set(expires_at), + user_id: ActiveValue::set(user.id), + ..Default::default() + } + .insert(&txn) + .await? + } + }; + txn.commit().await?; + Ok(session) + } +} diff --git a/src/models/users.rs b/src/models/users.rs index 431711a..97e47bd 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,10 +1,13 @@ use async_trait::async_trait; use chrono::{offset::Local, Duration}; +use loco_oauth2::models::users::OAuth2UserTrait; use loco_rs::{auth::jwt, hash, prelude::*}; +use passwords::PasswordGenerator; use serde::{Deserialize, Serialize}; use serde_json::Map; use uuid::Uuid; +use crate::models::_entities::o_auth2_sessions; pub use crate::models::_entities::users::{self, ActiveModel, Entity, Model}; pub const MAGIC_LINK_LENGTH: i8 = 32; @@ -367,3 +370,93 @@ impl ActiveModel { self.update(db).await.map_err(ModelError::from) } } + +/// Google OpenID Connect user profile (the fields our scopes request). +/// +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct OAuth2UserProfile { + pub email: String, + pub name: String, + pub sub: String, + pub email_verified: bool, + pub given_name: Option, + pub family_name: Option, + pub picture: Option, + pub locale: Option, +} + +#[async_trait] +impl OAuth2UserTrait for Model { + /// Resolve the user behind an active OAuth2 session id. + async fn find_by_oauth2_session_id( + db: &DatabaseConnection, + session_id: &str, + ) -> ModelResult { + let session = o_auth2_sessions::Entity::find() + .filter(o_auth2_sessions::Column::SessionId.eq(session_id)) + .one(db) + .await? + .ok_or_else(|| ModelError::EntityNotFound)?; + users::Entity::find_by_id(session.user_id) + .one(db) + .await? + .ok_or_else(|| ModelError::EntityNotFound) + } + + /// Find-or-create the local user for a verified OAuth2 profile. + /// + /// Per security advisory LOC-2025-04, OAuth2-created accounts get a strong + /// RANDOM password (never the provider `sub`) — they sign in via the + /// provider, and the random secret just satisfies the NOT NULL column. + /// Google has already verified the email, so we mark it verified. + async fn upsert_with_oauth( + db: &DatabaseConnection, + profile: &OAuth2UserProfile, + ) -> ModelResult { + let txn = db.begin().await?; + let user = match users::Entity::find() + .filter(users::Column::Email.eq(&profile.email)) + .one(&txn) + .await? + { + Some(user) => user, + None => { + let password = PasswordGenerator::new() + .length(16) + .numbers(true) + .lowercase_letters(true) + .uppercase_letters(true) + .symbols(true) + .exclude_similar_characters(true) + .strict(true) + .generate_one() + .map_err(|e| ModelError::Any(e.into()))?; + let password_hash = + hash::hash_password(&password).map_err(|e| ModelError::Any(e.into()))?; + users::ActiveModel { + email: ActiveValue::set(profile.email.to_string()), + name: ActiveValue::set(profile.name.to_string()), + email_verified_at: ActiveValue::set(Some(Local::now().into())), + password: ActiveValue::set(password_hash), + ..Default::default() + } + .insert(&txn) + .await + .map_err(|e| { + tracing::error!(error = %e, "failed to create OAuth2 user"); + ModelError::Any(e.into()) + })? + } + }; + txn.commit().await?; + Ok(user) + } + + /// Required by the trait; mirrors the inherent [`Model::generate_jwt`] + /// (inlined to avoid the inherent/trait name clash). + fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult { + jwt::JWT::new(secret) + .generate_token(*expiration, self.pid.to_string(), Map::new()) + .map_err(ModelError::from) + } +}