diff --git a/Cargo.lock b/Cargo.lock index c8c87a4..30e9488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,17 +238,23 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "fast-xray" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", + "bytes", "clap", "console", "futures", + "http-body-util", + "hyper", + "hyper-util", "indicatif", "ipnet", "rand 0.8.6", "reqwest", "rustls", + "serde", + "serde_json", "tokio", "tokio-rustls", "tokio-tungstenite", @@ -442,6 +448,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.10.1" @@ -455,6 +467,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -992,6 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7e855ad..4ea06e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,26 @@ [package] name = "fast-xray" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.85" description = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes." [dependencies] anyhow = "1" +bytes = "1" clap = { version = "4", features = ["derive"] } console = "0.15" futures = "0.3" +http-body-util = "0.1" +hyper = { version = "1", features = ["http1", "server"] } +hyper-util = { version = "0.1", features = ["tokio"] } indicatif = "0.17" ipnet = "2" rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] } tokio-tungstenite = "0.24" diff --git a/src/cli.rs b/src/cli.rs index 38317fa..a228c37 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,13 +4,13 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; -/// With no subcommand, the top-level args run the full auto pipeline -/// (ping → latency → speed → export). +/// With a node but no subcommand, the top-level args run the full auto pipeline +/// (ping → latency → speed → export). With no arguments at all (e.g. a +/// double-clicked binary), the local web UI launches. #[derive(Parser)] #[command( name = "fast-xray", about = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes.", - arg_required_else_help = true, args_conflicts_with_subcommands = true )] pub(crate) struct Cli { @@ -33,6 +33,8 @@ pub(crate) enum Command { Export(ExportArgs), /// Find one IP fast enough to hit a target speed, then stop. Easy(EasyArgs), + /// Launch the local web UI (zero-install front end for `easy`). + Web(WebArgs), } #[derive(Args)] @@ -246,6 +248,27 @@ pub(crate) struct EasyArgs { pub(crate) output: PathBuf, } +#[derive(Args)] +pub(crate) struct WebArgs { + /// Address to bind. Use 0.0.0.0 to expose on the LAN or behind a reverse proxy. + #[arg(long, default_value = "127.0.0.1")] + pub(crate) host: String, + + /// Port to listen on. + #[arg(short = 'p', long, default_value_t = 8080)] + pub(crate) port: u16, + + /// Don't open the browser automatically on startup. + #[arg(long)] + pub(crate) no_open: bool, +} + +impl Default for WebArgs { + fn default() -> Self { + Self { host: "127.0.0.1".to_string(), port: 8080, no_open: false } + } +} + #[derive(Args)] pub(crate) struct ExportArgs { /// Input vless:// node URL (or use --node-file). diff --git a/src/easy/engine.rs b/src/easy/engine.rs new file mode 100644 index 0000000..cd64f8b --- /dev/null +++ b/src/easy/engine.rs @@ -0,0 +1,67 @@ +//! The headless `easy` engine: measure the direct link, resolve a target, then +//! drive the producer/consumer search against a shared [`State`]. The terminal +//! CLI and the web server both build their UI around this — one observes the +//! [`State`] as an in-place panel, the other streams its snapshots over SSE. + +use std::sync::Arc; + +use anyhow::{Result, anyhow}; + +use crate::cloudflare::CfRanges; +use crate::speed; +use crate::vless::VlessNode; + +use super::consumer::consume; +use super::producer::produce; +use super::state::{Found, Phase, State}; +use super::{DEFAULT_SPEED_FRACTION, DIRECT_TIMEOUT, SPEED_BYTES, secs}; + +/// Measure the direct (no-proxy) download speed — the link ceiling and the +/// source of a default target. One longer measurement: the mirror's throughput +/// is too bimodal for short samples to agree. `None` if direct access is blocked. +pub(crate) async fn measure_baseline() -> Option { + speed::measure_direct(secs(DIRECT_TIMEOUT), SPEED_BYTES).await.ok() +} + +/// Resolve the speed target from an explicit request (if any) and the measured +/// `baseline`, returning `(target, note, explicit)`. With neither a request nor +/// a measured link there's nothing to aim for; a request above the link is +/// rejected. +pub(crate) fn resolve_target( + speed: Option, + baseline: Option, +) -> Result<(f64, String, bool)> { + match (speed, baseline) { + (Some(s), _) if s <= 0.0 => Err(anyhow!("target speed must be greater than 0")), + (Some(s), Some(base)) if s > base => Err(anyhow!( + "direct speed is only {base:.2} MB/s — can't find a node ≥ {s:.2} MB/s" + )), + (Some(s), _) => Ok((s, String::new(), true)), + (None, Some(base)) => Ok(( + base * DEFAULT_SPEED_FRACTION, + format!(" ({:.0}% of direct {base:.2})", DEFAULT_SPEED_FRACTION * 100.0), + false, + )), + (None, None) => Err(anyhow!( + "couldn't measure direct speed to pick a default target — set a target speed" + )), + } +} + +/// Run the search: mark the state `Searching`, spawn discovery, run the +/// screen/confirm loop, and return the winner (or `None` if the source dried up +/// first). The caller settles the final outcome on the state. +pub(crate) async fn search( + node: Arc, + ranges: Arc, + max: usize, + ipv6: bool, + explicit_target: bool, + state: State, +) -> Option { + state.set_phase(Phase::Searching); + let producer = tokio::spawn(produce(ranges, node.clone(), max, ipv6, state.clone())); + let hit = consume(node, state.clone(), explicit_target).await; + producer.abort(); + hit +} diff --git a/src/easy/mod.rs b/src/easy/mod.rs index 7503b0b..5160da4 100644 --- a/src/easy/mod.rs +++ b/src/easy/mod.rs @@ -12,6 +12,7 @@ //! results, sharing one mutex-guarded [`state`] bucket between the three tasks. mod consumer; +mod engine; mod producer; mod state; mod ui; @@ -27,13 +28,14 @@ use indicatif::ProgressBar; use crate::cli::EasyArgs; use crate::cloudflare::{self, Family}; use crate::report::{print_easy_found, resolve_node, write_file}; -use crate::speed; -use consumer::consume; -use producer::produce; -use state::{Found, State}; use ui::{Panel, render_loop}; +// Surface the headless engine and observable state for the web server, which +// builds an alternate (SSE) UI around the same search. +pub(crate) use engine::{measure_baseline, resolve_target, search}; +pub(crate) use state::{FeedStage, FeedState, Found, Outcome, Phase, Snapshot, State}; + // Fixed knobs — the point of `easy` is to not expose these. const CONCURRENCY: usize = 5; // independent screens in flight, also the screen floor divisor const QUEUE_HIGH: usize = 50; // validated-queue refill target @@ -70,19 +72,20 @@ pub(crate) async fn run(args: EasyArgs) -> Result<()> { return Err(anyhow!("--speed must be greater than 0")); } + let state = State::new(); + // 0. Direct (no-proxy) speed via a domestic mirror — the link ceiling, and - // the source of the default target when --speed is omitted. One longer - // (8 s) measurement: the mirror's throughput is too bimodal for repeated - // short samples to agree on. + // the source of the default target when --speed is omitted. let spinner = spin("Measuring direct (no-proxy) download speed…"); - let baseline = speed::measure_direct(secs(DIRECT_TIMEOUT), SPEED_BYTES).await; - match &baseline { - Ok(mbs) => spinner.finish_with_message(format!( + let baseline = engine::measure_baseline().await; + state.set_baseline(baseline); + match baseline { + Some(mbs) => spinner.finish_with_message(format!( "{} direct download speed: {} MB/s", style("✓").green().bold(), style(format!("{mbs:.2}")).cyan() )), - Err(_) => spinner.finish_with_message(format!( + None => spinner.finish_with_message(format!( "{} direct speed unavailable (direct access blocked?)", style("!").yellow().bold() )), @@ -91,24 +94,8 @@ pub(crate) async fn run(args: EasyArgs) -> Result<()> { // Resolve the target: explicit --speed, else a fraction of the measured // direct speed. Reject a target the local link can't reach; with neither a // target nor a measured link there's nothing to aim for. - let explicit_target = args.speed.is_some(); - let (target, note) = match (args.speed, baseline) { - (Some(s), Ok(base)) if s > base => { - return Err(anyhow!( - "direct speed is only {base:.2} MB/s — can't find a node ≥ {s:.2} MB/s" - )); - } - (Some(s), _) => (s, String::new()), - (None, Ok(base)) => ( - base * DEFAULT_SPEED_FRACTION, - format!(" ({:.0}% of direct {base:.2})", DEFAULT_SPEED_FRACTION * 100.0), - ), - (None, Err(_)) => { - return Err(anyhow!( - "couldn't measure direct speed to pick a default target — pass --speed " - )); - } - }; + let (target, note, explicit_target) = engine::resolve_target(args.speed, baseline)?; + state.set_target(target); let family = if args.ipv6 { Family::Both } else { Family::V4 }; let spinner = spin("Fetching Cloudflare ranges…"); @@ -127,16 +114,16 @@ pub(crate) async fn run(args: EasyArgs) -> Result<()> { args.max ); - let state = State::new(target); - let producer = tokio::spawn(produce(ranges.clone(), node.clone(), args.max, args.ipv6, state.clone())); let stop = Arc::new(AtomicBool::new(false)); let ui = tokio::spawn(render_loop(state.clone(), Panel::new(), args.max, stop.clone())); - let hit = consume(node.clone(), state.clone(), explicit_target).await; + let hit = + engine::search(node.clone(), ranges, args.max, args.ipv6, explicit_target, state.clone()) + .await; stop.store(true, Ordering::Relaxed); let _ = ui.await; // render loop clears the view before returning - producer.abort(); + state.set_phase(Phase::Done); match hit { Some(Found { ip, latency, mbs }) => { diff --git a/src/easy/producer.rs b/src/easy/producer.rs index 0f44f43..8a030be 100644 --- a/src/easy/producer.rs +++ b/src/easy/producer.rs @@ -14,8 +14,7 @@ use crate::latency::{self, LatStatus}; use crate::ping; use crate::vless::VlessNode; -use super::state::State; -use super::ui::{lat_feed_line, ping_feed_line}; +use super::state::{FeedEntry, FeedStage, FeedState, State}; use super::{ LAT_CONCURRENCY, LAT_MAX_MS, LAT_TIMEOUT, MAX_BARREN_ROUNDS, PING_CONCURRENCY, PING_MAX_MS, PING_MIN_MS, PING_TIMEOUT, POLL, PRODUCE_BATCH, QUEUE_HIGH, QUEUE_LOW, millis, secs, @@ -90,9 +89,19 @@ async fn discover_round( let ping_side = async { ping::run(ranges, cfg, move |p| match p { ping::Progress::Probing { ip, status, latency, .. } => { - if let Some(line) = ping_feed_line(ip, &status, latency) { - state_ping.push_feed(line); - let _ = tx.unbounded_send(ip); + // Only reachable IPs (with a timed RTT) feed the live log and + // flow on to the latency stage; the unreachable firehose is hidden. + if matches!(status, ping::ProbeStatus::Ok) { + if let Some(d) = latency { + state_ping.push_feed(FeedEntry { + stage: FeedStage::Ping, + ip, + ms: Some(d.as_secs_f64() * 1000.0), + state: FeedState::Ok, + note: None, + }); + let _ = tx.unbounded_send(ip); + } } } }) @@ -116,7 +125,30 @@ async fn discover_round( Ok(d) => LatStatus::TooSlow(d), Err(e) => LatStatus::Failed(e.to_string()), }; - state_lat.push_feed(lat_feed_line(ip, &status)); + let entry = match &status { + LatStatus::Ok(d) => FeedEntry { + stage: FeedStage::Latency, + ip, + ms: Some(d.as_secs_f64() * 1000.0), + state: FeedState::Ok, + note: None, + }, + LatStatus::TooSlow(d) => FeedEntry { + stage: FeedStage::Latency, + ip, + ms: Some(d.as_secs_f64() * 1000.0), + state: FeedState::Slow, + note: None, + }, + LatStatus::Failed(why) => FeedEntry { + stage: FeedStage::Latency, + ip, + ms: None, + state: FeedState::Fail, + note: Some(why.clone()), + }, + }; + state_lat.push_feed(entry); if let LatStatus::Ok(d) = status { if state_lat.try_enqueue(ip, d, max_valid) { minted += 1; diff --git a/src/easy/state.rs b/src/easy/state.rs index d376bfe..ce9867f 100644 --- a/src/easy/state.rs +++ b/src/easy/state.rs @@ -2,7 +2,8 @@ //! queue, and the live testing/feed/recent views, all behind one [`State`] //! handle whose methods are the *only* way to touch the bucket. A plain mutex //! (never held across `.await`) is enough: producer and consumer drive it -//! through these methods, the renderer reads a [`Snapshot`]. +//! through these methods; a renderer (the terminal panel) or the web SSE loop +//! reads a [`Snapshot`]. use std::collections::VecDeque; use std::net::IpAddr; @@ -12,21 +13,68 @@ use std::time::Duration; use super::{FEED, RECENT}; /// The winning IP: address, carried latency, measured speed (MB/s). -pub(super) struct Found { - pub(super) ip: IpAddr, - pub(super) latency: Duration, - pub(super) mbs: f64, +pub(crate) struct Found { + pub(crate) ip: IpAddr, + pub(crate) latency: Duration, + pub(crate) mbs: f64, } -/// A point-in-time copy of everything the renderer draws. -pub(super) struct Snapshot { - pub(super) testing: Vec, - pub(super) feed: Vec, - pub(super) recent: Vec<(IpAddr, Option, bool)>, // newest first - pub(super) valid: usize, - pub(super) queued: usize, - pub(super) confirming: bool, - pub(super) recalibrating: bool, +/// Coarse run phase, surfaced to the web UI. `confirming`/`recalibrating` are +/// finer flags layered on top of `Searching`. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Phase { + Measuring, + Searching, + Done, +} + +/// Which discovery stage produced a feed line. +#[derive(Clone, Copy)] +pub(crate) enum FeedStage { + Ping, + Latency, +} + +/// How a feed line turned out. +#[derive(Clone, Copy)] +pub(crate) enum FeedState { + Ok, + Slow, + Fail, +} + +/// One structured discovery-feed line: the terminal panel formats it into ANSI, +/// the web layer serializes it to JSON — one source of truth for both. +#[derive(Clone)] +pub(crate) struct FeedEntry { + pub(crate) stage: FeedStage, + pub(crate) ip: IpAddr, + pub(crate) ms: Option, + pub(crate) state: FeedState, + pub(crate) note: Option, +} + +/// The terminal end of a run, surfaced to the web UI as the final SSE frame. +#[derive(Clone)] +pub(crate) enum Outcome { + Pending, + Found { ip: IpAddr, mbs: f64, latency_ms: f64, url: String }, + NotFound, +} + +/// A point-in-time copy of everything an observer draws. +pub(crate) struct Snapshot { + pub(crate) phase: Phase, + pub(crate) baseline: Option, + pub(crate) target: f64, + pub(crate) testing: Vec, + pub(crate) feed: Vec, + pub(crate) recent: Vec<(IpAddr, Option, bool)>, // newest first + pub(crate) valid: usize, + pub(crate) queued: usize, + pub(crate) confirming: bool, + pub(crate) recalibrating: bool, + pub(crate) outcome: Outcome, } /// One finished speed test, kept for the "recent" panel. @@ -40,24 +88,28 @@ struct Bucket { queue: VecDeque<(IpAddr, Duration)>, // validated, awaiting a concurrent screen confirm: VecDeque<(IpAddr, Duration)>, // promising (≥ target/n), awaiting a solo confirm testing: Vec, // IPs under test right now (screen or confirm) - feed: VecDeque, // last FEED discovery lines (producer's ping/latency) + feed: VecDeque, // last FEED discovery lines (producer's ping/latency) recent: VecDeque, // last RECENT finished, oldest first valid: usize, // total minted (gates --max + header) fails: usize, // tested-and-rejected count (drives recalibration) + phase: Phase, // coarse phase for the web UI + baseline: Option, // measured direct (no-proxy) speed, MB/s target: f64, // current target MB/s (recalibrated mid-run) confirming: bool, // a solo confirm is running (pauses screens, tints UI) recalibrating: bool, // a direct re-measure is running producer_done: bool, found: bool, + outcome: Outcome, // final result, once the run settles } /// Cloneable handle to the shared bucket. All locking lives here. #[derive(Clone)] -pub(super) struct State(Arc>); +pub(crate) struct State(Arc>); impl State { - /// A fresh state aimed at `target` MB/s. - pub(super) fn new(target: f64) -> Self { + /// A fresh state. The target starts at 0 and is set once the baseline is + /// measured (see [`State::set_target`]); the run starts in `Measuring`. + pub(crate) fn new() -> Self { State(Arc::new(Mutex::new(Bucket { queue: VecDeque::new(), confirm: VecDeque::new(), @@ -66,11 +118,14 @@ impl State { recent: VecDeque::new(), valid: 0, fails: 0, - target, + phase: Phase::Measuring, + baseline: None, + target: 0.0, confirming: false, recalibrating: false, producer_done: false, found: false, + outcome: Outcome::Pending, }))) } @@ -84,7 +139,7 @@ impl State { self.lock().target } - pub(super) fn valid(&self) -> usize { + pub(crate) fn valid(&self) -> usize { self.lock().valid } @@ -104,6 +159,34 @@ impl State { b.fails > 0 && b.fails % every == 0 && !b.found } + // --- phase / baseline / target (driven by the engine) --- + + pub(crate) fn set_phase(&self, phase: Phase) { + self.lock().phase = phase; + } + + pub(crate) fn set_baseline(&self, baseline: Option) { + self.lock().baseline = baseline; + } + + pub(crate) fn set_target(&self, target: f64) { + self.lock().target = target; + } + + /// Record the winning node and settle the run (phase `Done`). + pub(crate) fn finish_found(&self, ip: IpAddr, mbs: f64, latency_ms: f64, url: String) { + let mut b = self.lock(); + b.phase = Phase::Done; + b.outcome = Outcome::Found { ip, mbs, latency_ms, url }; + } + + /// Settle the run with no fast-enough IP found (phase `Done`). + pub(crate) fn finish_not_found(&self) { + let mut b = self.lock(); + b.phase = Phase::Done; + b.outcome = Outcome::NotFound; + } + // --- consumer --- pub(super) fn pop_queue(&self) -> Option<(IpAddr, Duration)> { @@ -186,10 +269,10 @@ impl State { true } - /// Push one line onto the bounded discovery feed (newest last). - pub(super) fn push_feed(&self, line: String) { + /// Push one entry onto the bounded discovery feed (newest last). + pub(super) fn push_feed(&self, entry: FeedEntry) { let mut b = self.lock(); - b.feed.push_back(line); + b.feed.push_back(entry); while b.feed.len() > FEED { b.feed.pop_front(); } @@ -199,11 +282,14 @@ impl State { self.lock().producer_done = true; } - // --- renderer --- + // --- observers (terminal panel / web SSE) --- - pub(super) fn snapshot(&self) -> Snapshot { + pub(crate) fn snapshot(&self) -> Snapshot { let b = self.lock(); Snapshot { + phase: b.phase, + baseline: b.baseline, + target: b.target, testing: b.testing.clone(), feed: b.feed.iter().cloned().collect(), recent: b.recent.iter().rev().map(|f| (f.ip, f.mbs, f.pass)).collect(), @@ -211,6 +297,7 @@ impl State { queued: b.queue.len(), confirming: b.confirming, recalibrating: b.recalibrating, + outcome: b.outcome.clone(), } } } diff --git a/src/easy/ui.rs b/src/easy/ui.rs index 3a639f9..7931d31 100644 --- a/src/easy/ui.rs +++ b/src/easy/ui.rs @@ -1,18 +1,13 @@ //! The fixed in-place panel — header, discovery feed, testing rows, recent rows //! — and the line formatters that feed it. -use std::net::IpAddr; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; use console::{Style, style}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use crate::latency::LatStatus; -use crate::ping; - -use super::state::{Snapshot, State}; +use super::state::{FeedEntry, FeedStage, FeedState, Snapshot, State}; use super::{CONCURRENCY, FEED, FRAME, FRAMES, QUEUE_LOW, RECENT}; /// Periodically redraw the panel until `stop`, then clear it. @@ -64,7 +59,7 @@ impl Panel { fn render(&self, state: &State, max: usize, tick: usize) { let frame = FRAMES[tick % FRAMES.len()]; - let Snapshot { testing, feed, recent, valid, queued, confirming, recalibrating } = + let Snapshot { testing, feed, recent, valid, queued, confirming, recalibrating, .. } = state.snapshot(); // Header: the search budget remaining (max valid IPs minus those minted). @@ -92,7 +87,7 @@ impl Panel { // Discovery feed: the ping + latency live log, scrolling (newest last). for (i, pb) in self.discover.iter().enumerate() { match feed.get(i) { - Some(line) => pb.set_message(line.clone()), + Some(entry) => pb.set_message(feed_line(entry)), None => pb.set_message(String::new()), } } @@ -153,37 +148,19 @@ fn health_color(ratio: f64) -> u8 { RAMP[i] } -/// A ping-stage feed line for an IP that passed the filters -/// (`[ping] `); `None` for the unreachable/filtered firehose. -pub(super) fn ping_feed_line(ip: IpAddr, status: &ping::ProbeStatus, latency: Option) -> Option { - if !matches!(status, ping::ProbeStatus::Ok) { - return None; - } - let ms = format!("{:.0}ms", latency?.as_secs_f64() * 1000.0); - Some(format!( - " {} {} {}", - style(format!("{:<9}", "[ping]")).blue().dim(), - style(format!("{:<15}", ip.to_string())).dim(), - style(ms).green().dim(), - )) -} - -/// A latency-stage feed line (`[latency] `), dimmed like the -/// `latency` command's live log. -pub(super) fn lat_feed_line(ip: IpAddr, status: &LatStatus) -> String { - let tag = style(format!("{:<9}", "[latency]")).cyan().dim(); - let addr = style(format!("{:<15}", ip.to_string())).dim(); - match status { - LatStatus::Ok(d) => { - let ms = format!("{:.0}ms", d.as_secs_f64() * 1000.0); - format!(" {tag} {addr} {}", style(ms).green().dim()) - } - LatStatus::TooSlow(d) => { - let ms = format!("{:.0}ms", d.as_secs_f64() * 1000.0); - format!(" {tag} {addr} {}", style(format!("{ms} too slow")).red().dim()) - } - LatStatus::Failed(why) => { - format!(" {tag} {addr} {}", style(why.clone()).red().dim()) - } - } +/// Format one structured discovery-feed entry into a dimmed ANSI line +/// (`[ping]/[latency] `), matching the live logs of the +/// standalone `ping`/`latency` commands. +fn feed_line(e: &FeedEntry) -> String { + let tag = match e.stage { + FeedStage::Ping => style(format!("{:<9}", "[ping]")).blue().dim(), + FeedStage::Latency => style(format!("{:<9}", "[latency]")).cyan().dim(), + }; + let addr = style(format!("{:<15}", e.ip.to_string())).dim(); + let tail = match e.state { + FeedState::Ok => style(format!("{:.0}ms", e.ms.unwrap_or(0.0))).green().dim(), + FeedState::Slow => style(format!("{:.0}ms too slow", e.ms.unwrap_or(0.0))).red().dim(), + FeedState::Fail => style(e.note.clone().unwrap_or_default()).red().dim(), + }; + format!(" {tag} {addr} {tail}") } diff --git a/src/main.rs b/src/main.rs index 9278305..e62faf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,11 +15,12 @@ mod ping; mod report; mod speed; mod vless; +mod web; use anyhow::Result; use clap::Parser; -use cli::{Cli, Command}; +use cli::{Cli, Command, WebArgs}; #[tokio::main] async fn main() -> Result<()> { @@ -33,6 +34,12 @@ async fn main() -> Result<()> { Some(Command::Speed(args)) => commands::run_speed(args).await, Some(Command::Export(args)) => commands::run_export(args), Some(Command::Easy(args)) => easy::run(args).await, - None => commands::run_auto(cli.auto).await, + Some(Command::Web(args)) => web::run(args).await, + // A node without a subcommand runs the auto pipeline; bare invocation + // (e.g. a double-clicked binary) opens the web UI. + None => match cli.auto.node { + Some(_) => commands::run_auto(cli.auto).await, + None => web::run(WebArgs::default()).await, + }, } } diff --git a/src/web/event.rs b/src/web/event.rs new file mode 100644 index 0000000..a4b6130 --- /dev/null +++ b/src/web/event.rs @@ -0,0 +1,122 @@ +//! Maps an `easy` [`Snapshot`] into the JSON snapshot the browser renders, one +//! field-for-field with what `static/app.js` reads (see its `paint`/`feedLine`/ +//! `recentLine`/`showResult`). Serialized with serde and pushed over SSE. + +use serde::Serialize; + +use crate::easy::{FeedStage, FeedState, Outcome, Phase, Snapshot}; + +#[derive(Serialize)] +struct Event { + phase: &'static str, + baseline: Option, + target: Option, + max: usize, + valid: usize, + queued: usize, + feed: Vec, + testing: Vec, + recent: Vec, + outcome: Option<&'static str>, + result: Option, +} + +#[derive(Serialize)] +struct Feed { + stage: &'static str, + ip: String, + ms: Option, + state: &'static str, + note: Option, +} + +#[derive(Serialize)] +struct Testing { + ip: String, + mode: &'static str, +} + +#[derive(Serialize)] +struct Recent { + ip: String, + mbs: Option, + pass: bool, +} + +#[derive(Serialize)] +struct NodeResult { + ip: String, + mbs: f64, + latency_ms: f64, + url: String, +} + +/// Serialize a snapshot to the browser's `data:` payload. `max` is the run's +/// valid-IP budget (carried by the session, not the snapshot). +pub(super) fn to_json(snap: &Snapshot, max: usize) -> String { + let done = snap.phase == Phase::Done; + // The frontend's phase pill: most specific state wins. + let phase = if done { + "done" + } else if snap.recalibrating { + "recalibrating" + } else if snap.confirming { + "confirming" + } else if snap.phase == Phase::Measuring { + "measuring" + } else { + "searching" + }; + + let feed = snap + .feed + .iter() + .map(|e| Feed { + stage: match e.stage { + FeedStage::Ping => "ping", + FeedStage::Latency => "latency", + }, + ip: e.ip.to_string(), + ms: e.ms.map(|m| m.round() as i64), + state: match e.state { + FeedState::Ok => "ok", + FeedState::Slow => "slow", + FeedState::Fail => "fail", + }, + note: e.note.clone(), + }) + .collect(); + + let mode = if snap.confirming { "confirm" } else { "screen" }; + let testing = snap.testing.iter().map(|ip| Testing { ip: ip.to_string(), mode }).collect(); + + let recent = snap + .recent + .iter() + .map(|(ip, mbs, pass)| Recent { ip: ip.to_string(), mbs: *mbs, pass: *pass }) + .collect(); + + let (outcome, result) = match &snap.outcome { + Outcome::Found { ip, mbs, latency_ms, url } => ( + Some("found"), + Some(NodeResult { ip: ip.to_string(), mbs: *mbs, latency_ms: *latency_ms, url: url.clone() }), + ), + Outcome::NotFound => (Some("notfound"), None), + Outcome::Pending => (None, None), + }; + + let event = Event { + phase, + baseline: snap.baseline, + target: (snap.target > 0.0).then_some(snap.target), + max, + valid: snap.valid, + queued: snap.queued, + feed, + testing, + recent, + outcome, + result, + }; + serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_string()) +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 0000000..c0e2556 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,60 @@ +//! `web`: a zero-install local UI for the `easy` search. Serves a small embedded +//! page that drives one search at a time and streams its progress over SSE, so +//! anyone can double-click the binary and optimize a node in the browser — no +//! toolchain, no terminal. + +mod event; +mod server; +mod session; + +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use console::style; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + +use crate::cli::WebArgs; +use session::Session; + +pub(crate) async fn run(args: WebArgs) -> Result<()> { + let addr: SocketAddr = format!("{}:{}", args.host, args.port) + .parse() + .with_context(|| format!("invalid host/port {}:{}", args.host, args.port))?; + let listener = TcpListener::bind(addr).await.with_context(|| format!("bind {addr}"))?; + + // Show a clickable localhost URL even when bound to 0.0.0.0 (LAN / proxy). + let display_host = if args.host == "0.0.0.0" { "localhost" } else { args.host.as_str() }; + let url = format!("http://{display_host}:{}/", args.port); + eprintln!("{} fast-xray web → {}", style("✓").green().bold(), style(&url).cyan()); + eprintln!(" {}", style("按 Ctrl+C 退出").dim()); + if !args.no_open { + open_browser(&url); + } + + let session = Arc::new(Session::new()); + loop { + let (stream, _) = listener.accept().await.context("accept connection")?; + let io = TokioIo::new(stream); + let session = session.clone(); + tokio::spawn(async move { + let service = service_fn(move |req| server::handle(req, session.clone())); + // A client hanging up mid-SSE surfaces here as an error; it's normal. + let _ = http1::Builder::new().serve_connection(io, service).await; + }); + } +} + +/// Best-effort open of the default browser at `url`; failure is silently fine. +fn open_browser(url: &str) { + use std::process::Command; + #[cfg(target_os = "windows")] + let _ = Command::new("cmd").args(["/C", "start", "", url]).spawn(); + #[cfg(target_os = "macos")] + let _ = Command::new("open").arg(url).spawn(); + #[cfg(all(unix, not(target_os = "macos")))] + let _ = Command::new("xdg-open").arg(url).spawn(); +} diff --git a/src/web/server.rs b/src/web/server.rs new file mode 100644 index 0000000..64b4d94 --- /dev/null +++ b/src/web/server.rs @@ -0,0 +1,115 @@ +//! HTTP routing for the web UI on a hand-rolled hyper service: three embedded +//! static assets, a `POST /api/run` to launch a search, and a `GET /api/events` +//! SSE stream that pushes a live snapshot every frame until the run settles. + +use std::convert::Infallible; +use std::sync::Arc; +use std::time::Duration; + +use bytes::Bytes; +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, Full, StreamBody}; +use hyper::body::{Frame, Incoming}; +use hyper::header::{CACHE_CONTROL, CONTENT_TYPE}; +use hyper::{Method, Request, Response, StatusCode}; +use serde::Deserialize; + +use super::session::Session; + +// The whole frontend, baked into the binary so a single exe is all anyone needs. +const INDEX_HTML: &str = include_str!("static/index.html"); +const APP_JS: &str = include_str!("static/app.js"); +const PICO_CSS: &str = include_str!("static/pico.min.css"); + +const FRAME: Duration = Duration::from_millis(120); // SSE push cadence (matches the UI tick) + +type Body = BoxBody; + +/// Request payload for `POST /api/run`. +#[derive(Deserialize)] +struct RunRequest { + node: String, + #[serde(default)] + max: Option, +} + +/// Route a request. Infallible: every path yields a response. +pub(super) async fn handle( + req: Request, + session: Arc, +) -> Result, Infallible> { + let resp = match (req.method(), req.uri().path()) { + (&Method::GET, "/") => asset(INDEX_HTML, "text/html; charset=utf-8"), + (&Method::GET, "/app.js") => asset(APP_JS, "text/javascript; charset=utf-8"), + (&Method::GET, "/pico.min.css") => asset(PICO_CSS, "text/css; charset=utf-8"), + (&Method::POST, "/api/run") => start_run(req, session).await, + (&Method::GET, "/api/events") => events(session), + _ => text(StatusCode::NOT_FOUND, "not found"), + }; + Ok(resp) +} + +/// Parse the node + budget and start a search; 400 on a bad body or node URL. +async fn start_run(req: Request, session: Arc) -> Response { + let bytes = match req.into_body().collect().await { + Ok(collected) => collected.to_bytes(), + Err(_) => return text(StatusCode::BAD_REQUEST, "could not read request body"), + }; + let parsed: RunRequest = match serde_json::from_slice(&bytes) { + Ok(parsed) => parsed, + Err(_) => return text(StatusCode::BAD_REQUEST, "invalid JSON body"), + }; + match session.start(parsed.node.trim(), parsed.max.unwrap_or(1000)) { + Ok(()) => json(StatusCode::OK, "{\"ok\":true}"), + Err(e) => text(StatusCode::BAD_REQUEST, &e.to_string()), + } +} + +/// Server-sent events: one snapshot frame per `FRAME`, ending after the frame +/// that reports `done` so the browser's EventSource can close cleanly. +fn events(session: Arc) -> Response { + let stream = futures::stream::unfold(false, move |stop| { + let session = session.clone(); + async move { + if stop { + return None; + } + tokio::time::sleep(FRAME).await; + let (payload, done) = match session.event() { + Some((json, done)) => (format!("data: {json}\n\n"), done), + None => (": waiting\n\n".to_string(), false), // no run yet — keep-alive comment + }; + let frame = Ok::<_, Infallible>(Frame::data(Bytes::from(payload))); + Some((frame, done)) + } + }); + Response::builder() + .header(CONTENT_TYPE, "text/event-stream") + .header(CACHE_CONTROL, "no-cache") + .body(StreamBody::new(stream).boxed()) + .unwrap() +} + +fn full(body: impl Into) -> Body { + Full::new(body.into()).boxed() +} + +fn asset(body: &'static str, content_type: &str) -> Response { + Response::builder().header(CONTENT_TYPE, content_type).body(full(body)).unwrap() +} + +fn json(status: StatusCode, body: &'static str) -> Response { + Response::builder() + .status(status) + .header(CONTENT_TYPE, "application/json") + .body(full(body)) + .unwrap() +} + +fn text(status: StatusCode, body: &str) -> Response { + Response::builder() + .status(status) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .body(full(body.to_string())) + .unwrap() +} diff --git a/src/web/session.rs b/src/web/session.rs new file mode 100644 index 0000000..e9332a5 --- /dev/null +++ b/src/web/session.rs @@ -0,0 +1,89 @@ +//! The single-run session behind the web server: one `easy` search at a time. +//! `start` kicks off a background task that drives the search to completion; +//! `event` reads the live snapshot for the SSE stream. Starting again replaces +//! (and aborts) any previous run. + +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use tokio::task::JoinHandle; + +use crate::cloudflare::{self, Family}; +use crate::easy::{self, Phase, State}; +use crate::vless::VlessNode; + +use super::event; + +/// One in-progress (or finished) run: its observable state, the run's valid-IP +/// budget, and the driving task (kept so a restart can abort it). +struct Run { + state: State, + max: usize, + task: JoinHandle<()>, +} + +/// Holds at most one run. Cloneable via `Arc`. +pub(super) struct Session { + current: Mutex>, +} + +impl Session { + pub(super) fn new() -> Self { + Session { current: Mutex::new(None) } + } + + /// Validate the node and start a fresh search, replacing any previous run. + pub(super) fn start(&self, node_url: &str, max: usize) -> Result<()> { + let node = Arc::new(VlessNode::parse(node_url)?); + let max = max.clamp(1, 5000); + let state = State::new(); + let task = tokio::spawn(drive(node, max, state.clone())); + + let mut current = self.current.lock().unwrap(); + if let Some(old) = current.replace(Run { state, max, task }) { + old.task.abort(); + } + Ok(()) + } + + /// The current snapshot as an SSE JSON payload, plus whether it has settled. + /// `None` before any run has started. + pub(super) fn event(&self) -> Option<(String, bool)> { + let current = self.current.lock().unwrap(); + let run = current.as_ref()?; + let snap = run.state.snapshot(); + let done = snap.phase == Phase::Done; + Some((event::to_json(&snap, run.max), done)) + } +} + +/// Drive one search to completion, recording the final outcome on `state`. +/// Mirrors `easy::run` minus the terminal UI: measure → target → ranges → +/// search. The web UI never sets an explicit target, so it always aims for the +/// default fraction of the measured direct speed. +async fn drive(node: Arc, max: usize, state: State) { + state.set_phase(Phase::Measuring); + let baseline = easy::measure_baseline().await; + state.set_baseline(baseline); + + let (target, _note, explicit) = match easy::resolve_target(None, baseline) { + Ok(resolved) => resolved, + Err(_) => return state.finish_not_found(), // direct blocked → no target to aim for + }; + state.set_target(target); + + let ranges = match cloudflare::fetch_ranges(Family::V4).await { + Ok(ranges) => Arc::new(ranges), + Err(_) => return state.finish_not_found(), + }; + + match easy::search(node.clone(), ranges, max, false, explicit, state.clone()).await { + Some(found) => { + let latency_ms = found.latency.as_secs_f64() * 1000.0; + let alias = format!("{:.2}MB-{:.0}ms-{}", found.mbs, latency_ms, found.ip); + let url = node.to_url(found.ip, &alias); + state.finish_found(found.ip, found.mbs, latency_ms, url); + } + None => state.finish_not_found(), + } +} diff --git a/src/web/static/app.js b/src/web/static/app.js new file mode 100644 index 0000000..bfef490 --- /dev/null +++ b/src/web/static/app.js @@ -0,0 +1,245 @@ +"use strict"; + +// fast-xray web frontend. +// +// Renders a single live "snapshot" object each frame. When a real backend is +// present it streams snapshots over SSE (/api/events); opened directly (file:// +// or no backend) it falls back to a self-contained DEMO that synthesises the +// same shape, so the page can be previewed without the server. + +const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const $ = (id) => document.getElementById(id); + +// ---- theme (light/dark, remembered) --------------------------------------- + +const themeBtn = $("theme-toggle"); +function applyTheme(t) { + document.documentElement.setAttribute("data-theme", t); + themeBtn.textContent = t === "dark" ? "☀" : "🌙"; // icon shows what you'd switch TO +} +applyTheme(localStorage.getItem("theme") || "dark"); +themeBtn.addEventListener("click", () => { + const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark"; + localStorage.setItem("theme", next); + applyTheme(next); +}); + +let tick = 0; +let last = null; // most recent snapshot, re-rendered on every spinner tick +setInterval(() => { tick++; if (last) paint(last); }, 120); + +// ---- formatting helpers ---------------------------------------------------- + +const spinner = (cls) => `${FRAMES[tick % FRAMES.length]}`; +const pad = (s, n) => { s = String(s); return s + " ".repeat(Math.max(0, n - s.length)); }; +const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c])); + +function feedLine(f) { + const tag = f.stage === "ping" + ? `${pad("[ping]", 9)}` + : `${pad("[latency]", 9)}`; + const ip = `${pad(f.ip, 15)}`; + let tail; + if (f.state === "ok") tail = `${f.ms}ms`; + else if (f.state === "slow") tail = `${f.ms}ms too slow`; + else tail = `${esc(f.note || "failed")}`; + return `
${tag} ${ip} ${tail}
`; +} + +function testingLine(t) { + const confirm = t.mode === "confirm"; + const verb = confirm ? `confirming…` : `testing…`; + return `
${spinner(confirm ? "confirm" : "")} ${pad(t.ip, 15)} ${verb}
`; +} + +function recentLine(r) { + const mark = r.pass ? `` : ``; + const val = r.mbs == null + ? `failed` + : `${r.mbs.toFixed(2)} MB/s`; + return `
${mark} ${pad(r.ip, 15)} ${val}
`; +} + +// ---- the single render entry point ---------------------------------------- + +const PHASE = { + measuring: ["searching", "测直连速度…"], + searching: ["searching", "筛选中"], + confirming: ["confirming", "确认中"], + recalibrating: ["recalibrating", "重测直连"], + done: ["done", "完成"], +}; + +function paint(s) { + // status bar + $("status").classList.add("on"); + $("s-base").textContent = s.baseline == null ? (s.phase === "measuring" ? "测量中…" : "不可用") : `${s.baseline.toFixed(2)} MB/s`; + $("s-target").textContent = s.target == null ? "—" : `${s.target.toFixed(2)} MB/s`; + $("s-budget").textContent = s.max == null ? "—" : Math.max(0, s.max - (s.valid || 0)); + $("s-queue").textContent = s.queued == null ? "—" : s.queued; + const [cls, label] = PHASE[s.phase] || PHASE.searching; + const phaseEl = $("s-phase"); + phaseEl.className = `pill ${cls}`; + phaseEl.textContent = label; + + // live panels + $("p-queue").textContent = s.queued == null ? "" : `队列 ${s.queued}`; + $("feed").innerHTML = (s.feed && s.feed.length) ? s.feed.map(feedLine).join("") : `等待发现…`; + $("testing-head").textContent = s.phase === "confirming" ? "confirming(单路确认)" : "testing(并发初筛)"; + $("testing").innerHTML = (s.testing && s.testing.length) ? s.testing.map(testingLine).join("") : `空闲`; + $("recent").innerHTML = (s.recent && s.recent.length) ? s.recent.map(recentLine).join("") : `暂无结果`; + + // result + if (s.outcome) showResult(s); + if (s.phase === "done") setRunning(false); +} + +function resetResult() { + const box = $("result"); + box.className = "result on"; + $("r-title").textContent = "优选中…"; + $("r-metrics").innerHTML = `完成后在此给出最终节点`; + $("r-url").style.display = "none"; +} + +function showResult(s) { + const box = $("result"); + box.classList.add("on"); + if (s.outcome === "found" && s.result) { + const r = s.result; + box.className = "result on win"; + $("r-title").innerHTML = ` 找到节点`; + $("r-metrics").innerHTML = + `
速度${r.mbs.toFixed(2)} MB/s
` + + `
延迟${r.latency_ms.toFixed(0)} ms
` + + `
IP${esc(r.ip)}
`; + $("r-url").style.display = "flex"; + $("r-code").textContent = r.url; + } else { + box.className = "result on lose"; + $("r-title").innerHTML = ` 没找到达标的 IP`; + $("r-metrics").innerHTML = `测了 ${s.valid || 0} 个有效 IP 仍未达到目标速度,可调高最大个数再试。`; + $("r-url").style.display = "none"; + } +} + +// ---- form + lifecycle ------------------------------------------------------ + +let running = false; +function setRunning(on) { + running = on; + $("start").textContent = on ? "优选中…" : "开始优选"; + $("start").setAttribute("aria-busy", on ? "true" : "false"); + $("node").disabled = on; + $("max").disabled = on; +} + +$("form").addEventListener("submit", async (e) => { + e.preventDefault(); + if (running) return; + const node = $("node").value.trim(); + const max = parseInt($("max").value, 10) || 100; + if (!node.startsWith("vless://")) { + $("hint").textContent = "请粘贴一个 vless:// 开头的节点链接"; + return; + } + $("hint").textContent = ""; + resetResult(); + last = null; + setRunning(true); + + try { + const resp = await fetch("/api/run", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ node, max }), + }); + if (!resp.ok) throw new Error(await resp.text()); + streamEvents(); + } catch (err) { + // No backend (preview / file://) → run the local demo instead. + runDemo(max); + } +}); + +function streamEvents() { + const es = new EventSource("/api/events"); + es.onmessage = (ev) => { + const snap = JSON.parse(ev.data); + last = snap; + paint(snap); + if (snap.phase === "done") { es.close(); } + }; + es.onerror = () => { /* EventSource auto-reconnects; nothing to do */ }; +} + +$("copy").addEventListener("click", async () => { + try { + await navigator.clipboard.writeText($("r-code").textContent); + $("copy").textContent = "已复制"; + setTimeout(() => ($("copy").textContent = "复制"), 1200); + } catch { /* clipboard blocked on insecure origin */ } +}); + +// ---- DEMO: synthesise snapshots so the page is alive without a backend ----- + +function rndIp() { + const seg = [[104, 19], [172, 67], [162, 159], [188, 114]][Math.floor(Math.random() * 4)]; + return `${seg[0]}.${seg[1]}.${(Math.random() * 256) | 0}.${(Math.random() * 256) | 0}`; +} + +function runDemo(max) { + const s = { + phase: "measuring", baseline: null, target: null, max, + valid: 0, queued: 0, feed: [], testing: [], recent: [], outcome: null, result: null, + }; + last = s; paint(s); + + setTimeout(() => { + s.baseline = 12.4; s.target = 9.92; s.phase = "searching"; s.queued = 4; + }, 1500); + + const timer = setInterval(() => { + if (s.phase === "measuring") return; + + // grow discovery feed + const ip = rndIp(); + const pingMs = 8 + ((Math.random() * 40) | 0); + s.feed.push({ stage: "ping", ip, ms: pingMs, state: "ok" }); + if (Math.random() < 0.7) { + const latMs = 120 + ((Math.random() * 200) | 0); + const slow = latMs > 290; + s.feed.push({ stage: "latency", ip, ms: latMs, state: slow ? "slow" : "ok" }); + if (!slow) { s.valid++; s.queued = Math.min(50, s.queued + 1); } + } + while (s.feed.length > 5) s.feed.shift(); + + // testing rows (concurrent screens) + s.testing = Array.from({ length: 3 + ((Math.random() * 3) | 0) }, () => ({ ip: rndIp(), mode: "screen" })); + if (Math.random() < 0.18) s.testing = [{ ip: rndIp(), mode: "confirm" }], s.phase = "confirming"; + else if (s.phase === "confirming") s.phase = "searching"; + + // recent results + if (Math.random() < 0.6) { + const mbs = +(Math.random() * 12).toFixed(2); + s.recent.unshift({ ip: rndIp(), mbs, pass: false }); + while (s.recent.length > 3) s.recent.pop(); + if (s.queued > 0) s.queued--; + } + + last = s; paint(s); + + // settle on a win after enough valid IPs + if (s.valid >= 14) { + clearInterval(timer); + const ip = rndIp(), mbs = 10.21, lat = 156; + s.phase = "done"; s.outcome = "found"; s.testing = []; + s.recent.unshift({ ip, mbs, pass: true }); + s.result = { + ip, mbs, latency_ms: lat, + url: `vless://b831381d-6324-4d53-ad4f-8cda48b30811@${ip}:443?encryption=none&security=tls&type=ws&host=example.com&path=%2F#${mbs.toFixed(2)}MB-${lat}ms-${ip}`, + }; + last = s; paint(s); + } + }, 320); +} diff --git a/src/web/static/index.html b/src/web/static/index.html new file mode 100644 index 0000000..238d0a8 --- /dev/null +++ b/src/web/static/index.html @@ -0,0 +1,187 @@ + + + + + + fast-xray · 优选 Cloudflare IP + + + + +
+

fast-xray

+ +
+ +
+ 直连 + 目标 + 剩余预算 + 队列 + 准备中 +
+ +
+ +
+
+ +
+ + +
+ +
+ +
结果
+
+
+

等待开始

+
完成后在此给出最终节点
+ +
+
+
+ + +
+
过程
+
+
discovering · ping + latency
+
等待发现…
+
+
+
testing
+
空闲
+
+
+
recent · 已测结果
+
暂无结果
+
+
+
+ + + + diff --git a/src/web/static/pico.min.css b/src/web/static/pico.min.css new file mode 100644 index 0000000..3e19952 --- /dev/null +++ b/src/web/static/pico.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";/*! + * Pico CSS ✨ v2.1.1 (https://picocss.com) + * Copyright 2019-2025 - Licensed under MIT + */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}[role=search]{--pico-border-radius:5rem}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button])::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(2, 154, 232, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#0172ad;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 114, 173, 0.5);--pico-primary-hover:#015887;--pico-primary-hover-background:#02659a;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(2, 154, 232, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal)}@media (min-width:576px){body>footer,body>header,body>main{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){body>footer,body>header,body>main{max-width:700px}}@media (min-width:1024px){body>footer,body>header,body>main{max-width:950px}}@media (min-width:1280px){body>footer,body>header,body>main{max-width:1200px}}@media (min-width:1536px){body>footer,body>header,body>main{max-width:1450px}}section{margin-bottom:var(--pico-block-spacing-vertical)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}[type=file]::file-selector-button:focus,[type=reset]:focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} \ No newline at end of file