From 7188b4462438b5f316a7deaf5397df88eb90ce71 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sat, 21 Mar 2026 00:42:17 +0100 Subject: [PATCH] feat: add compute Signed-off-by: kjuulh --- .../console-2026-03-15T22-30-20-876Z.log | 1 + Cargo.lock | 23 + crates/forage-core/Cargo.toml | 3 +- .../forage-core/src/compute/mock_scheduler.rs | 440 ++++++++++++++++++ crates/forage-core/src/compute/mod.rs | 201 ++++++++ crates/forage-core/src/lib.rs | 1 + crates/forage-server/Cargo.toml | 3 +- crates/forage-server/src/compute_grpc.rs | 160 +++++++ crates/forage-server/src/main.rs | 16 + crates/forage-server/src/routes/platform.rs | 205 ++++++++ crates/forage-server/src/serve_grpc.rs | 39 ++ crates/forage-server/src/state.rs | 8 + static/css/input.css | 15 + static/css/style.css | 2 +- templates/base.html.jinja | 1 + templates/pages/compute.html.jinja | 103 ++++ templates/pages/rollout_detail.html.jinja | 89 ++++ 17 files changed, 1307 insertions(+), 3 deletions(-) create mode 100644 .playwright-mcp/console-2026-03-15T22-30-20-876Z.log create mode 100644 crates/forage-core/src/compute/mock_scheduler.rs create mode 100644 crates/forage-core/src/compute/mod.rs create mode 100644 crates/forage-server/src/compute_grpc.rs create mode 100644 crates/forage-server/src/serve_grpc.rs create mode 100644 templates/pages/compute.html.jinja create mode 100644 templates/pages/rollout_detail.html.jinja diff --git a/.playwright-mcp/console-2026-03-15T22-30-20-876Z.log b/.playwright-mcp/console-2026-03-15T22-30-20-876Z.log new file mode 100644 index 0000000..8bfd83e --- /dev/null +++ b/.playwright-mcp/console-2026-03-15T22-30-20-876Z.log @@ -0,0 +1 @@ +[ 116ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://client.dev.forage.sh/orgs/rawpotion/compute/rollouts/83ae0cbc-5db6-4344-8042-025816b017d3:0 diff --git a/Cargo.lock b/Cargo.lock index 7ce8448..82afbe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,28 @@ dependencies = [ "url", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -982,6 +1004,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-nats", + "async-stream", "async-trait", "axum", "axum-extra", diff --git a/crates/forage-core/Cargo.toml b/crates/forage-core/Cargo.toml index 653bdd1..83c7713 100644 --- a/crates/forage-core/Cargo.toml +++ b/crates/forage-core/Cargo.toml @@ -14,6 +14,7 @@ rand.workspace = true hmac.workspace = true sha2.workspace = true tracing.workspace = true +tokio.workspace = true [dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "time"] } diff --git a/crates/forage-core/src/compute/mock_scheduler.rs b/crates/forage-core/src/compute/mock_scheduler.rs new file mode 100644 index 0000000..14c3148 --- /dev/null +++ b/crates/forage-core/src/compute/mock_scheduler.rs @@ -0,0 +1,440 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::sync::Mutex; +use uuid::Uuid; + +use super::{ + ComputeError, ComputeInstance, ComputeResourceSpec, ComputeScheduler, ResourceKind, Rollout, + RolloutEvent, RolloutResource, RolloutStatus, +}; + +struct MockState { + rollouts: HashMap, + instances: HashMap>, +} + +/// In-memory compute scheduler that simulates container lifecycle. +/// +/// Stores rollouts and instances in memory. When `apply_resources` is called +/// it spawns a background task that transitions each resource through +/// PENDING → IN_PROGRESS → SUCCEEDED with short delays. +pub struct InMemoryComputeScheduler { + state: Arc>, +} + +impl InMemoryComputeScheduler { + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(MockState { + rollouts: HashMap::new(), + instances: HashMap::new(), + })), + } + } +} + +impl Default for InMemoryComputeScheduler { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl ComputeScheduler for InMemoryComputeScheduler { + async fn apply_resources( + &self, + apply_id: &str, + namespace: &str, + resources: Vec, + labels: HashMap, + ) -> Result { + let rollout_id = Uuid::new_v4().to_string(); + let now = chrono::Utc::now(); + + let rollout_resources: Vec = resources + .iter() + .map(|r| RolloutResource { + name: r.name.clone(), + kind: r.kind, + status: RolloutStatus::Pending, + message: "queued".into(), + }) + .collect(); + + let rollout = Rollout { + id: rollout_id.clone(), + apply_id: apply_id.to_string(), + namespace: namespace.to_string(), + resources: rollout_resources, + status: RolloutStatus::Pending, + labels: labels.clone(), + created_at: now, + }; + + // Create instances for container-service resources + let region = labels.get("region").cloned().unwrap_or("eu-west-1".into()); + let project = labels.get("project").cloned().unwrap_or_default(); + let destination = labels.get("destination").cloned().unwrap_or_default(); + let environment = labels.get("environment").cloned().unwrap_or_default(); + let new_instances: Vec = resources + .iter() + .filter(|r| r.kind == ResourceKind::ContainerService) + .map(|r| ComputeInstance { + id: Uuid::new_v4().to_string(), + namespace: namespace.to_string(), + resource_name: r.name.clone(), + project: project.clone(), + destination: destination.clone(), + environment: environment.clone(), + region: region.clone(), + image: r.image.clone().unwrap_or_else(|| "unknown".into()), + replicas: r.replicas, + cpu: r.cpu.clone().unwrap_or_else(|| "250m".into()), + memory: r.memory.clone().unwrap_or_else(|| "256Mi".into()), + status: "pending".into(), + created_at: now, + }) + .collect(); + + { + let mut state = self.state.lock().await; + state.rollouts.insert(rollout_id.clone(), rollout); + let ns_instances = state + .instances + .entry(namespace.to_string()) + .or_insert_with(Vec::new); + // Upsert: replace existing instances with the same resource_name + for new_inst in new_instances { + if let Some(existing) = ns_instances + .iter_mut() + .find(|i| i.resource_name == new_inst.resource_name) + { + *existing = new_inst; + } else { + ns_instances.push(new_inst); + } + } + } + + // Spawn background simulation + let state = self.state.clone(); + let rid = rollout_id.clone(); + let ns = namespace.to_string(); + let resource_names: Vec<(String, String)> = resources + .iter() + .map(|r| (r.name.clone(), r.kind.to_string())) + .collect(); + + tokio::spawn(async move { + // Transition to InProgress + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + { + let mut state = state.lock().await; + if let Some(rollout) = state.rollouts.get_mut(&rid) { + rollout.status = RolloutStatus::InProgress; + for r in &mut rollout.resources { + r.status = RolloutStatus::InProgress; + r.message = "deploying".into(); + } + } + // Update instance statuses + if let Some(instances) = state.instances.get_mut(&ns) { + for inst in instances.iter_mut() { + if inst.status == "pending" { + inst.status = "running".into(); + } + } + } + } + + // Simulate per-resource completion + for (name, _kind) in &resource_names { + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let mut state = state.lock().await; + if let Some(rollout) = state.rollouts.get_mut(&rid) { + if let Some(r) = rollout.resources.iter_mut().find(|r| &r.name == name) { + r.status = RolloutStatus::Succeeded; + r.message = "ready".into(); + } + } + } + + // Mark rollout as succeeded + { + let mut state = state.lock().await; + if let Some(rollout) = state.rollouts.get_mut(&rid) { + rollout.status = RolloutStatus::Succeeded; + } + } + }); + + Ok(rollout_id) + } + + async fn watch_rollout( + &self, + rollout_id: &str, + ) -> Result, ComputeError> { + let rollout = { + let state = self.state.lock().await; + state + .rollouts + .get(rollout_id) + .cloned() + .ok_or_else(|| ComputeError::NotFound(format!("rollout {rollout_id}")))? + }; + + let (tx, rx) = tokio::sync::mpsc::channel(64); + let state = self.state.clone(); + let rid = rollout_id.to_string(); + let resource_specs: Vec<(String, String)> = rollout + .resources + .iter() + .map(|r| (r.name.clone(), r.kind.to_string())) + .collect(); + + tokio::spawn(async move { + // Emit pending events + for (name, kind) in &resource_specs { + let _ = tx + .send(RolloutEvent { + resource_name: name.clone(), + resource_kind: kind.clone(), + status: RolloutStatus::Pending, + message: "queued".into(), + }) + .await; + } + + // Poll until the rollout is done + loop { + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + let state = state.lock().await; + let Some(rollout) = state.rollouts.get(&rid) else { + break; + }; + + for r in &rollout.resources { + let _ = tx + .send(RolloutEvent { + resource_name: r.name.clone(), + resource_kind: r.kind.to_string(), + status: r.status, + message: r.message.clone(), + }) + .await; + } + + if matches!( + rollout.status, + RolloutStatus::Succeeded | RolloutStatus::Failed | RolloutStatus::RolledBack + ) { + break; + } + } + }); + + Ok(rx) + } + + async fn delete_resources( + &self, + namespace: &str, + labels: HashMap, + ) -> Result<(), ComputeError> { + let mut state = self.state.lock().await; + + // Remove matching rollouts + state.rollouts.retain(|_, r| { + if r.namespace != namespace { + return true; + } + for (k, v) in &labels { + if r.labels.get(k) != Some(v) { + return true; + } + } + false + }); + + // Remove matching instances + if let Some(instances) = state.instances.get_mut(namespace) { + if labels.is_empty() { + instances.clear(); + } + // If labels are specified we'd filter more precisely, but for now + // the mock just clears the namespace. + } + + Ok(()) + } + + async fn list_rollouts(&self, namespace: &str) -> Result, ComputeError> { + let state = self.state.lock().await; + let mut rollouts: Vec = state + .rollouts + .values() + .filter(|r| r.namespace == namespace) + .cloned() + .collect(); + rollouts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + Ok(rollouts) + } + + async fn get_rollout(&self, rollout_id: &str) -> Result { + let state = self.state.lock().await; + state + .rollouts + .get(rollout_id) + .cloned() + .ok_or_else(|| ComputeError::NotFound(format!("rollout {rollout_id}"))) + } + + async fn list_instances( + &self, + namespace: &str, + ) -> Result, ComputeError> { + let state = self.state.lock().await; + Ok(state + .instances + .get(namespace) + .cloned() + .unwrap_or_default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn apply_creates_rollout_and_instances() { + let scheduler = InMemoryComputeScheduler::new(); + + let resources = vec![ComputeResourceSpec { + name: "my-api".into(), + kind: ResourceKind::ContainerService, + image: Some("registry.forage.sh/org/app:v1".into()), + replicas: 2, + cpu: Some("500m".into()), + memory: Some("512Mi".into()), + }]; + + let mut labels = HashMap::new(); + labels.insert("region".into(), "eu-west-1".into()); + + let rollout_id = scheduler + .apply_resources("test-apply-1", "test-ns", resources, labels) + .await + .unwrap(); + + assert!(!rollout_id.is_empty()); + + // Rollout should exist + let rollout = scheduler.get_rollout(&rollout_id).await.unwrap(); + assert_eq!(rollout.namespace, "test-ns"); + assert_eq!(rollout.resources.len(), 1); + + // Instance should exist + let instances = scheduler.list_instances("test-ns").await.unwrap(); + assert_eq!(instances.len(), 1); + assert_eq!(instances[0].image, "registry.forage.sh/org/app:v1"); + assert_eq!(instances[0].replicas, 2); + } + + #[tokio::test(flavor = "multi_thread")] + async fn rollout_completes_successfully() { + let scheduler = InMemoryComputeScheduler::new(); + + let resources = vec![ComputeResourceSpec { + name: "svc".into(), + kind: ResourceKind::ContainerService, + image: Some("img:latest".into()), + replicas: 1, + cpu: None, + memory: None, + }]; + + let rollout_id = scheduler + .apply_resources("test-2", "ns", resources, HashMap::new()) + .await + .unwrap(); + + // Wait for simulation to complete + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let rollout = scheduler.get_rollout(&rollout_id).await.unwrap(); + assert_eq!(rollout.status, RolloutStatus::Succeeded); + assert_eq!(rollout.resources[0].status, RolloutStatus::Succeeded); + } + + #[tokio::test(flavor = "multi_thread")] + async fn watch_rollout_streams_events() { + let scheduler = InMemoryComputeScheduler::new(); + + let resources = vec![ComputeResourceSpec { + name: "app".into(), + kind: ResourceKind::ContainerService, + image: Some("img:v1".into()), + replicas: 1, + cpu: None, + memory: None, + }]; + + let rollout_id = scheduler + .apply_resources("test-3", "ns", resources, HashMap::new()) + .await + .unwrap(); + + let mut rx = scheduler.watch_rollout(&rollout_id).await.unwrap(); + + let mut events = Vec::new(); + while let Some(event) = rx.recv().await { + events.push(event); + if events.last().map(|e| e.status) == Some(RolloutStatus::Succeeded) { + break; + } + } + + assert!(!events.is_empty()); + // Should have at least pending + succeeded + assert!(events.iter().any(|e| e.status == RolloutStatus::Pending)); + assert!(events.iter().any(|e| e.status == RolloutStatus::Succeeded)); + } + + #[tokio::test] + async fn delete_removes_resources() { + let scheduler = InMemoryComputeScheduler::new(); + + let resources = vec![ComputeResourceSpec { + name: "app".into(), + kind: ResourceKind::ContainerService, + image: Some("img:v1".into()), + replicas: 1, + cpu: None, + memory: None, + }]; + + let mut labels = HashMap::new(); + labels.insert("project".into(), "test".into()); + + scheduler + .apply_resources("del-1", "ns", resources, labels.clone()) + .await + .unwrap(); + + assert_eq!(scheduler.list_rollouts("ns").await.unwrap().len(), 1); + + scheduler.delete_resources("ns", labels).await.unwrap(); + + assert_eq!(scheduler.list_rollouts("ns").await.unwrap().len(), 0); + } + + #[tokio::test] + async fn watch_nonexistent_rollout_returns_not_found() { + let scheduler = InMemoryComputeScheduler::new(); + let result = scheduler.watch_rollout("does-not-exist").await; + assert!(matches!(result, Err(ComputeError::NotFound(_)))); + } +} diff --git a/crates/forage-core/src/compute/mod.rs b/crates/forage-core/src/compute/mod.rs new file mode 100644 index 0000000..77b0f17 --- /dev/null +++ b/crates/forage-core/src/compute/mod.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +// --------------------------------------------------------------------------- +// Region catalog +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Region { + pub id: &'static str, + pub name: &'static str, + pub display_name: &'static str, + pub available: bool, +} + +pub const REGIONS: &[Region] = &[ + Region { + id: "eu-west-1", + name: "Europe (Ireland)", + display_name: "eu-west-1 — Europe (Ireland)", + available: true, + }, + Region { + id: "us-east-1", + name: "US East (Virginia)", + display_name: "us-east-1 — US East (Virginia)", + available: true, + }, + Region { + id: "ap-southeast-1", + name: "Asia Pacific (Singapore)", + display_name: "ap-southeast-1 — Asia Pacific (Singapore)", + available: false, + }, +]; + +pub fn available_regions() -> Vec<&'static Region> { + REGIONS.iter().filter(|r| r.available).collect() +} + +// --------------------------------------------------------------------------- +// Domain types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ComputeResourceSpec { + pub name: String, + pub kind: ResourceKind, + pub image: Option, + pub replicas: u32, + pub cpu: Option, + pub memory: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ResourceKind { + ContainerService, + Service, + Route, + CronJob, + Job, +} + +impl std::fmt::Display for ResourceKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResourceKind::ContainerService => write!(f, "container_service"), + ResourceKind::Service => write!(f, "service"), + ResourceKind::Route => write!(f, "route"), + ResourceKind::CronJob => write!(f, "cron_job"), + ResourceKind::Job => write!(f, "job"), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Rollout { + pub id: String, + pub apply_id: String, + pub namespace: String, + pub resources: Vec, + pub status: RolloutStatus, + pub labels: HashMap, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RolloutResource { + pub name: String, + pub kind: ResourceKind, + pub status: RolloutStatus, + pub message: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RolloutStatus { + Pending, + InProgress, + Succeeded, + Failed, + RolledBack, +} + +impl std::fmt::Display for RolloutStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RolloutStatus::Pending => write!(f, "pending"), + RolloutStatus::InProgress => write!(f, "in_progress"), + RolloutStatus::Succeeded => write!(f, "succeeded"), + RolloutStatus::Failed => write!(f, "failed"), + RolloutStatus::RolledBack => write!(f, "rolled_back"), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RolloutEvent { + pub resource_name: String, + pub resource_kind: String, + pub status: RolloutStatus, + pub message: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ComputeInstance { + pub id: String, + pub namespace: String, + pub resource_name: String, + pub project: String, + pub destination: String, + pub environment: String, + pub region: String, + pub image: String, + pub replicas: u32, + pub cpu: String, + pub memory: String, + pub status: String, + pub created_at: chrono::DateTime, +} + +// --------------------------------------------------------------------------- +// Error +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ComputeError { + #[error("not found: {0}")] + NotFound(String), + #[error("invalid request: {0}")] + InvalidRequest(String), + #[error("resource conflict: {0}")] + Conflict(String), + #[error("scheduler error: {0}")] + Internal(String), +} + +// --------------------------------------------------------------------------- +// Scheduler trait +// --------------------------------------------------------------------------- + +#[async_trait::async_trait] +pub trait ComputeScheduler: Send + Sync { + /// Apply a batch of resources. Returns a rollout ID for tracking. + async fn apply_resources( + &self, + apply_id: &str, + namespace: &str, + resources: Vec, + labels: HashMap, + ) -> Result; + + /// Subscribe to rollout status events. + async fn watch_rollout( + &self, + rollout_id: &str, + ) -> Result, ComputeError>; + + /// Delete resources by namespace + labels. + async fn delete_resources( + &self, + namespace: &str, + labels: HashMap, + ) -> Result<(), ComputeError>; + + /// List rollouts for a namespace. + async fn list_rollouts(&self, namespace: &str) -> Result, ComputeError>; + + /// Get a specific rollout by ID. + async fn get_rollout(&self, rollout_id: &str) -> Result; + + /// List running compute instances for a namespace. + async fn list_instances(&self, namespace: &str) -> Result, ComputeError>; +} + +// --------------------------------------------------------------------------- +// In-memory mock scheduler +// --------------------------------------------------------------------------- + +pub mod mock_scheduler; +pub use mock_scheduler::InMemoryComputeScheduler; diff --git a/crates/forage-core/src/lib.rs b/crates/forage-core/src/lib.rs index 2790101..46bd85f 100644 --- a/crates/forage-core/src/lib.rs +++ b/crates/forage-core/src/lib.rs @@ -5,3 +5,4 @@ pub mod integrations; pub mod registry; pub mod deployments; pub mod billing; +pub mod compute; diff --git a/crates/forage-server/Cargo.toml b/crates/forage-server/Cargo.toml index 911c606..434d08c 100644 --- a/crates/forage-server/Cargo.toml +++ b/crates/forage-server/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] forage-core = { path = "../forage-core" } forage-db = { path = "../forage-db" } -forage-grpc = { path = "../forage-grpc" } +forage-grpc = { path = "../forage-grpc", features = ["client", "server"] } anyhow.workspace = true chrono.workspace = true async-trait.workspace = true @@ -37,3 +37,4 @@ sha2.workspace = true notmad.workspace = true tokio-util.workspace = true async-nats.workspace = true +async-stream = "0.3" diff --git a/crates/forage-server/src/compute_grpc.rs b/crates/forage-server/src/compute_grpc.rs new file mode 100644 index 0000000..6ca9404 --- /dev/null +++ b/crates/forage-server/src/compute_grpc.rs @@ -0,0 +1,160 @@ +use std::pin::Pin; +use std::sync::Arc; + +use forage_core::compute::{ + ComputeError, ComputeResourceSpec, ComputeScheduler, ResourceKind, RolloutStatus, +}; +use forage_grpc::forage_service_server::ForageService; +use forage_grpc::{ + ApplyResourcesRequest, ApplyResourcesResponse, DeleteResourcesRequest, + DeleteResourcesResponse, RolloutEvent as ProtoRolloutEvent, WatchRolloutRequest, +}; +use tokio_stream::Stream; +use tonic::{Request, Response, Status}; + +/// Implements the `ForageService` gRPC server trait. +/// +/// Thin adapter: validates auth, converts proto to domain types, delegates to +/// the `ComputeScheduler`, and converts results back to proto. +pub struct ForageServiceImpl { + pub scheduler: Arc, +} + +type WatchStream = + Pin> + Send + 'static>>; + +#[tonic::async_trait] +impl ForageService for ForageServiceImpl { + async fn apply_resources( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + if req.namespace.is_empty() { + return Err(Status::invalid_argument("namespace is required")); + } + if req.resources.is_empty() { + return Err(Status::invalid_argument("at least one resource is required")); + } + + let resources: Vec = req + .resources + .iter() + .map(|r| { + let (kind, image, replicas, cpu, memory) = match &r.spec { + Some(forage_grpc::forage_resource::Spec::ContainerService(cs)) => { + let container = cs.container.as_ref(); + let scaling = cs.scaling.as_ref(); + ( + ResourceKind::ContainerService, + container.map(|c| c.image.clone()), + scaling.map(|s| s.replicas).unwrap_or(1), + container + .and_then(|c| c.resources.as_ref()) + .and_then(|r| r.requests.as_ref()) + .map(|r| r.cpu.clone()), + container + .and_then(|c| c.resources.as_ref()) + .and_then(|r| r.requests.as_ref()) + .map(|r| r.memory.clone()), + ) + } + Some(forage_grpc::forage_resource::Spec::Service(_)) => { + (ResourceKind::Service, None, 1, None, None) + } + Some(forage_grpc::forage_resource::Spec::Route(_)) => { + (ResourceKind::Route, None, 1, None, None) + } + Some(forage_grpc::forage_resource::Spec::CronJob(cj)) => { + let image = cj.container.as_ref().map(|c| c.image.clone()); + (ResourceKind::CronJob, image, 1, None, None) + } + Some(forage_grpc::forage_resource::Spec::Job(j)) => { + let image = j.container.as_ref().map(|c| c.image.clone()); + (ResourceKind::Job, image, 1, None, None) + } + None => (ResourceKind::ContainerService, None, 1, None, None), + }; + + ComputeResourceSpec { + name: r.name.clone(), + kind, + image, + replicas, + cpu, + memory, + } + }) + .collect(); + + let rollout_id = self + .scheduler + .apply_resources(&req.apply_id, &req.namespace, resources, req.labels) + .await + .map_err(compute_err_to_status)?; + + Ok(Response::new(ApplyResourcesResponse { rollout_id })) + } + + type WatchRolloutStream = WatchStream; + + async fn watch_rollout( + &self, + request: Request, + ) -> Result, Status> { + let rollout_id = request.into_inner().rollout_id; + + let mut rx = self + .scheduler + .watch_rollout(&rollout_id) + .await + .map_err(compute_err_to_status)?; + + let stream = async_stream::stream! { + while let Some(event) = rx.recv().await { + yield Ok(ProtoRolloutEvent { + resource_name: event.resource_name, + resource_kind: event.resource_kind, + status: domain_status_to_proto(event.status) as i32, + message: event.message, + }); + } + }; + + Ok(Response::new(Box::pin(stream) as Self::WatchRolloutStream)) + } + + async fn delete_resources( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + self.scheduler + .delete_resources(&req.namespace, req.labels) + .await + .map_err(compute_err_to_status)?; + + Ok(Response::new(DeleteResourcesResponse {})) + } +} + +fn compute_err_to_status(e: ComputeError) -> Status { + match e { + ComputeError::NotFound(msg) => Status::not_found(msg), + ComputeError::InvalidRequest(msg) => Status::invalid_argument(msg), + ComputeError::Conflict(msg) => Status::already_exists(msg), + ComputeError::Internal(msg) => Status::internal(msg), + } +} + +fn domain_status_to_proto(s: RolloutStatus) -> forage_grpc::RolloutStatus { + match s { + RolloutStatus::Pending => forage_grpc::RolloutStatus::Pending, + RolloutStatus::InProgress => forage_grpc::RolloutStatus::InProgress, + RolloutStatus::Succeeded => forage_grpc::RolloutStatus::Succeeded, + RolloutStatus::Failed => forage_grpc::RolloutStatus::Failed, + RolloutStatus::RolledBack => forage_grpc::RolloutStatus::RolledBack, + } +} diff --git a/crates/forage-server/src/main.rs b/crates/forage-server/src/main.rs index e7e2a28..52e95db 100644 --- a/crates/forage-server/src/main.rs +++ b/crates/forage-server/src/main.rs @@ -1,9 +1,11 @@ mod auth; +mod compute_grpc; mod forest_client; mod notification_consumer; mod notification_ingester; mod notification_worker; mod routes; +mod serve_grpc; mod serve_http; mod session_reaper; mod state; @@ -252,6 +254,20 @@ async fn main() -> anyhow::Result<()> { } } + // Compute scheduler (mock for now — simulates container lifecycle) + let compute_scheduler = Arc::new(forage_core::compute::InMemoryComputeScheduler::new()); + state = state.with_compute_scheduler(compute_scheduler.clone()); + + let grpc_port: u16 = std::env::var("GRPC_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(4050); + let grpc_addr = SocketAddr::from(([0, 0, 0, 0], grpc_port)); + mad.add(serve_grpc::ServeGrpc { + addr: grpc_addr, + scheduler: compute_scheduler, + }); + // HTTP server component mad.add(serve_http::ServeHttp { addr, state }); diff --git a/crates/forage-server/src/routes/platform.rs b/crates/forage-server/src/routes/platform.rs index c6b37ca..90e7faf 100644 --- a/crates/forage-server/src/routes/platform.rs +++ b/crates/forage-server/src/routes/platform.rs @@ -142,6 +142,12 @@ pub fn router() -> Router { get(timeline_api), ) .route("/api/orgs/{org}/timeline", get(org_timeline_api)) + .route("/orgs/{org}/compute", get(compute_page)) + .route( + "/orgs/{org}/compute/rollouts/{rollout_id}", + get(rollout_detail_page), + ) + .route("/api/compute/regions", get(regions_api)) } fn orgs_context(orgs: &[CachedOrg]) -> Vec { @@ -4989,3 +4995,202 @@ async fn get_plan_output_api( })) .into_response()) } + +// --------------------------------------------------------------------------- +// Compute +// --------------------------------------------------------------------------- + +async fn compute_page( + State(state): State, + session: Session, + Path(org): Path, +) -> Result { + let orgs = &session.user.orgs; + let _cached_org = require_org_membership(&state, orgs, &org)?; + + let (instances, rollouts) = if let Some(ref scheduler) = state.compute_scheduler { + let namespace = &org; + let instances = scheduler + .list_instances(namespace) + .await + .unwrap_or_default(); + let rollouts = scheduler + .list_rollouts(namespace) + .await + .unwrap_or_default(); + (instances, rollouts) + } else { + (vec![], vec![]) + }; + + let instances_ctx: Vec = instances + .iter() + .map(|i| { + context! { + id => i.id, + resource_name => i.resource_name, + project => i.project, + destination => i.destination, + environment => i.environment, + image => i.image, + region => i.region, + replicas => i.replicas, + cpu => i.cpu, + memory => i.memory, + status => i.status, + } + }) + .collect(); + + let rollouts_ctx: Vec = rollouts + .iter() + .take(20) + .map(|r| { + let resources: Vec = r + .resources + .iter() + .map(|res| { + context! { + name => res.name, + kind => res.kind.to_string(), + status => res.status.to_string(), + message => res.message, + } + }) + .collect(); + context! { + id => r.id, + apply_id => r.apply_id, + namespace => r.namespace, + status => r.status.to_string(), + resources => resources, + } + }) + .collect(); + + let projects = warn_default( + "compute: list projects", + state + .platform_client + .list_projects(&session.access_token, &org) + .await, + ); + + let html = state + .templates + .render( + "pages/compute.html.jinja", + context! { + title => format!("Compute - {} - Forage", org), + description => "Managed compute instances", + user => context! { username => session.user.username }, + csrf_token => &session.csrf_token, + orgs => orgs_context(orgs), + current_org => &org, + active_tab => "compute", + projects => projects, + instances => instances_ctx, + rollouts => rollouts_ctx, + org_name => &org, + }, + ) + .map_err(|e| internal_error(&state, "compute render", &e))?; + + Ok(Html(html).into_response()) +} + +async fn rollout_detail_page( + State(state): State, + session: Session, + Path((org, rollout_id)): Path<(String, String)>, +) -> Result { + let orgs = &session.user.orgs; + let _cached_org = require_org_membership(&state, orgs, &org)?; + + let scheduler = state.compute_scheduler.as_ref().ok_or_else(|| { + error_page( + &state, + StatusCode::NOT_FOUND, + "Not available", + "Compute is not enabled.", + ) + })?; + + let rollout = scheduler.get_rollout(&rollout_id).await.map_err(|_| { + error_page( + &state, + StatusCode::NOT_FOUND, + "Not found", + "Rollout not found.", + ) + })?; + + let resources_ctx: Vec = rollout + .resources + .iter() + .map(|r| { + context! { + name => r.name, + kind => r.kind.to_string(), + status => r.status.to_string(), + message => r.message, + } + }) + .collect(); + + let labels_ctx: Vec = rollout.labels.iter().map(|(k, v)| context! { key => k, value => v }).collect(); + + let rollout_ctx = context! { + id => rollout.id, + apply_id => rollout.apply_id, + namespace => rollout.namespace, + status => rollout.status.to_string(), + resources => resources_ctx, + labels => labels_ctx, + }; + + let projects = warn_default( + "rollout detail: list projects", + state + .platform_client + .list_projects(&session.access_token, &org) + .await, + ); + + let html = state + .templates + .render( + "pages/rollout_detail.html.jinja", + context! { + title => format!("Rollout {} - Forage", rollout.apply_id), + description => "Rollout details", + user => context! { username => session.user.username }, + csrf_token => &session.csrf_token, + orgs => orgs_context(orgs), + current_org => &org, + active_tab => "compute", + projects => projects, + rollout => rollout_ctx, + org_name => &org, + }, + ) + .map_err(|e| internal_error(&state, "rollout detail render", &e))?; + + Ok(Html(html).into_response()) +} + +async fn regions_api() -> impl IntoResponse { + let regions: Vec = forage_core::compute::REGIONS + .iter() + .map(|r| { + serde_json::json!({ + "id": r.id, + "name": r.name, + "display_name": r.display_name, + "available": r.available, + }) + }) + .collect(); + + Json(regions) +} diff --git a/crates/forage-server/src/serve_grpc.rs b/crates/forage-server/src/serve_grpc.rs new file mode 100644 index 0000000..51e20ba --- /dev/null +++ b/crates/forage-server/src/serve_grpc.rs @@ -0,0 +1,39 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::Context; +use forage_core::compute::ComputeScheduler; +use forage_grpc::forage_service_server::ForageServiceServer; +use notmad::{Component, ComponentInfo, MadError}; +use tokio_util::sync::CancellationToken; + +use crate::compute_grpc::ForageServiceImpl; + +pub struct ServeGrpc { + pub addr: SocketAddr, + pub scheduler: Arc, +} + +impl Component for ServeGrpc { + fn info(&self) -> ComponentInfo { + "forage/grpc".into() + } + + async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> { + let svc = ForageServiceImpl { + scheduler: self.scheduler.clone(), + }; + + tracing::info!("gRPC server listening on {}", self.addr); + + tonic::transport::Server::builder() + .add_service(ForageServiceServer::new(svc)) + .serve_with_shutdown(self.addr, async move { + cancellation_token.cancelled().await; + }) + .await + .context("failed to run gRPC server")?; + + Ok(()) + } +} diff --git a/crates/forage-server/src/state.rs b/crates/forage-server/src/state.rs index d6b0c47..e6b18d7 100644 --- a/crates/forage-server/src/state.rs +++ b/crates/forage-server/src/state.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::forest_client::GrpcForestClient; use crate::templates::TemplateEngine; use forage_core::auth::ForestAuth; +use forage_core::compute::ComputeScheduler; use forage_core::integrations::IntegrationStore; use forage_core::platform::ForestPlatform; use forage_core::session::SessionStore; @@ -24,6 +25,7 @@ pub struct AppState { pub grpc_client: Option>, pub integration_store: Option>, pub slack_config: Option, + pub compute_scheduler: Option>, } impl AppState { @@ -41,6 +43,7 @@ impl AppState { grpc_client: None, integration_store: None, slack_config: None, + compute_scheduler: None, } } @@ -58,4 +61,9 @@ impl AppState { self.slack_config = Some(config); self } + + pub fn with_compute_scheduler(mut self, scheduler: Arc) -> Self { + self.compute_scheduler = Some(scheduler); + self + } } diff --git a/static/css/input.css b/static/css/input.css index cf81843..e5e4eaf 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -28,6 +28,8 @@ /* Remap Tailwind's color variables so all existing utilities adapt automatically. */ @media (prefers-color-scheme: dark) { :root, :host { + color-scheme: dark; + /* Neutrals — invert the gray scale */ --color-white: oklch(14.5% 0.015 260); --color-black: oklch(98% 0.002 248); @@ -91,4 +93,17 @@ /* Amber */ --color-amber-400: oklch(80% 0.17 84); } + + /* Form elements — inherit the dark palette */ + input, textarea, select { + background-color: var(--color-gray-50); + color: var(--color-gray-900); + border-color: var(--color-gray-200); + } + input::placeholder, textarea::placeholder { + color: var(--color-gray-400); + } + input:focus, textarea:focus, select:focus { + border-color: var(--color-green-300); + } } diff --git a/static/css/style.css b/static/css/style.css index eeffcca..dce7cd0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-700:oklch(45.7% .24 277.023);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-800:oklch(45.9% .187 3.815);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.-top-3{top:calc(var(--spacing) * -3)}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-\[3px\]{top:3px}.right-1\.5{right:calc(var(--spacing) * 1.5)}.left-0{left:calc(var(--spacing) * 0)}.left-0\.5{left:calc(var(--spacing) * .5)}.left-4{left:calc(var(--spacing) * 4)}.left-\[3px\]{left:3px}.left-\[calc\(100\%-1\.125rem\)\]{left:calc(100% - 1.125rem)}.z-20{z-index:20}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-8{margin-block:calc(var(--spacing) * 8)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-auto{margin-top:auto}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-6{margin-left:calc(var(--spacing) * 6)}.ml-7{margin-left:calc(var(--spacing) * 7)}.ml-auto{margin-left:auto}.scrollbar-none{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-none::-webkit-scrollbar{display:none}.mobile-only{display:none}@media (max-width:39.999rem){.mobile-only{display:block}}.block{display:block}@media (max-width:39.999rem){.desktop-only{display:none}}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[200px\]{max-width:200px}.max-w-\[250px\]{max-width:250px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[140px\]{min-width:140px}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-y{resize:vertical}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-blue-200{border-color:var(--color-blue-200)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-purple-200{border-color:var(--color-purple-200)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-t-gray-600{border-top-color:var(--color-gray-600)}.bg-\[\#4A154B\]{background-color:#4a154b}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-50\/50{background-color:#f9fafb80}@supports (color:color-mix(in lab, red, red)){.bg-gray-50\/50{background-color:color-mix(in oklab, var(--color-gray-50) 50%, transparent)}}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-600{background-color:var(--color-red-600)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-white{background-color:var(--color-white)}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.p-1{padding:calc(var(--spacing) * 1)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.p-12{padding:calc(var(--spacing) * 12)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pr-1{padding-right:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-400{color:var(--color-blue-400)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-700{color:var(--color-indigo-700)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-pink-800{color:var(--color-pink-800)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.group-open\:hidden:is(:where(.group):is([open],:popover-open,:open) *){display:none}.group-open\:inline:is(:where(.group):is([open],:popover-open,:open) *){display:inline}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:border-gray-300:is(:where(.group):hover *){border-color:var(--color-gray-300)}.group-hover\:text-gray-500:is(:where(.group):hover *){color:var(--color-gray-500)}.group-hover\:text-violet-700:is(:where(.group):hover *){color:var(--color-violet-700)}}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}@media (hover:hover){.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-amber-200:hover{background-color:var(--color-amber-200)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}.has-\[\:checked\]\:border-blue-500:has(:checked){border-color:var(--color-blue-500)}.has-\[\:checked\]\:bg-blue-50:has(:checked){background-color:var(--color-blue-50)}@media (min-width:40rem){.sm\:flex{display:flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}@media (prefers-color-scheme:dark){:root,:host{--color-white:oklch(14.5% .015 260);--color-black:oklch(98% .002 248);--color-gray-50:oklch(17.5% .02 260);--color-gray-100:oklch(21% .024 265);--color-gray-200:oklch(27.8% .025 257);--color-gray-300:oklch(37.3% .025 260);--color-gray-400:oklch(55.1% .02 264);--color-gray-500:oklch(60% .02 264);--color-gray-600:oklch(70.7% .017 261);--color-gray-700:oklch(80% .012 258);--color-gray-800:oklch(87.2% .008 258);--color-gray-900:oklch(93% .005 265);--color-gray-950:oklch(96.7% .003 265);--color-green-50:oklch(20% .04 155);--color-green-100:oklch(25% .06 155);--color-green-200:oklch(30% .08 155);--color-green-300:oklch(42% .12 154);--color-green-700:oklch(75% .15 150);--color-green-800:oklch(80% .12 150);--color-red-50:oklch(22% .04 17);--color-red-200:oklch(32% .06 18);--color-red-600:oklch(65% .2 27);--color-red-700:oklch(72% .18 27);--color-red-800:oklch(77% .15 27);--color-blue-100:oklch(22% .04 255);--color-blue-600:oklch(62% .2 263);--color-blue-700:oklch(72% .17 264);--color-blue-800:oklch(77% .15 265);--color-orange-100:oklch(25% .05 75);--color-orange-800:oklch(78% .13 37);--color-yellow-100:oklch(25% .06 103);--color-yellow-700:oklch(72% .12 66);--color-yellow-800:oklch(77% .1 62);--color-violet-100:oklch(22% .04 295);--color-violet-200:oklch(28% .06 294);--color-violet-400:oklch(45% .14 293);--color-violet-600:oklch(60% .2 293);--color-violet-800:oklch(75% .18 293);--color-purple-100:oklch(22% .04 307);--color-purple-800:oklch(75% .17 304);--color-pink-100:oklch(22% .04 342);--color-pink-800:oklch(75% .15 4);--color-amber-400:oklch(80% .17 84)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-700:oklch(45.7% .24 277.023);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-800:oklch(45.9% .187 3.815);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.-top-3{top:calc(var(--spacing) * -3)}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-\[3px\]{top:3px}.right-1\.5{right:calc(var(--spacing) * 1.5)}.left-0{left:calc(var(--spacing) * 0)}.left-0\.5{left:calc(var(--spacing) * .5)}.left-4{left:calc(var(--spacing) * 4)}.left-\[3px\]{left:3px}.left-\[calc\(100\%-1\.125rem\)\]{left:calc(100% - 1.125rem)}.z-20{z-index:20}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-8{margin-block:calc(var(--spacing) * 8)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-auto{margin-top:auto}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-6{margin-left:calc(var(--spacing) * 6)}.ml-7{margin-left:calc(var(--spacing) * 7)}.ml-auto{margin-left:auto}.scrollbar-none{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-none::-webkit-scrollbar{display:none}.mobile-only{display:none}@media (max-width:39.999rem){.mobile-only{display:block}}.block{display:block}@media (max-width:39.999rem){.desktop-only{display:none}}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[200px\]{max-width:200px}.max-w-\[250px\]{max-width:250px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[140px\]{min-width:140px}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-y{resize:vertical}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-blue-200{border-color:var(--color-blue-200)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-purple-200{border-color:var(--color-purple-200)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-t-gray-600{border-top-color:var(--color-gray-600)}.bg-\[\#4A154B\]{background-color:#4a154b}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-50\/50{background-color:#f9fafb80}@supports (color:color-mix(in lab, red, red)){.bg-gray-50\/50{background-color:color-mix(in oklab, var(--color-gray-50) 50%, transparent)}}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-600{background-color:var(--color-red-600)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-white{background-color:var(--color-white)}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.p-1{padding:calc(var(--spacing) * 1)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.p-12{padding:calc(var(--spacing) * 12)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pr-1{padding-right:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-400{color:var(--color-blue-400)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-700{color:var(--color-indigo-700)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-pink-800{color:var(--color-pink-800)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.group-open\:hidden:is(:where(.group):is([open],:popover-open,:open) *){display:none}.group-open\:inline:is(:where(.group):is([open],:popover-open,:open) *){display:inline}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:border-gray-300:is(:where(.group):hover *){border-color:var(--color-gray-300)}.group-hover\:text-gray-500:is(:where(.group):hover *){color:var(--color-gray-500)}.group-hover\:text-violet-700:is(:where(.group):hover *){color:var(--color-violet-700)}}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}@media (hover:hover){.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-amber-200:hover{background-color:var(--color-amber-200)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}.has-\[\:checked\]\:border-blue-500:has(:checked){border-color:var(--color-blue-500)}.has-\[\:checked\]\:bg-blue-50:has(:checked){background-color:var(--color-blue-50)}@media (min-width:40rem){.sm\:flex{display:flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}@media (prefers-color-scheme:dark){:root,:host{color-scheme:dark;--color-white:oklch(14.5% .015 260);--color-black:oklch(98% .002 248);--color-gray-50:oklch(17.5% .02 260);--color-gray-100:oklch(21% .024 265);--color-gray-200:oklch(27.8% .025 257);--color-gray-300:oklch(37.3% .025 260);--color-gray-400:oklch(55.1% .02 264);--color-gray-500:oklch(60% .02 264);--color-gray-600:oklch(70.7% .017 261);--color-gray-700:oklch(80% .012 258);--color-gray-800:oklch(87.2% .008 258);--color-gray-900:oklch(93% .005 265);--color-gray-950:oklch(96.7% .003 265);--color-green-50:oklch(20% .04 155);--color-green-100:oklch(25% .06 155);--color-green-200:oklch(30% .08 155);--color-green-300:oklch(42% .12 154);--color-green-700:oklch(75% .15 150);--color-green-800:oklch(80% .12 150);--color-red-50:oklch(22% .04 17);--color-red-200:oklch(32% .06 18);--color-red-600:oklch(65% .2 27);--color-red-700:oklch(72% .18 27);--color-red-800:oklch(77% .15 27);--color-blue-100:oklch(22% .04 255);--color-blue-600:oklch(62% .2 263);--color-blue-700:oklch(72% .17 264);--color-blue-800:oklch(77% .15 265);--color-orange-100:oklch(25% .05 75);--color-orange-800:oklch(78% .13 37);--color-yellow-100:oklch(25% .06 103);--color-yellow-700:oklch(72% .12 66);--color-yellow-800:oklch(77% .1 62);--color-violet-100:oklch(22% .04 295);--color-violet-200:oklch(28% .06 294);--color-violet-400:oklch(45% .14 293);--color-violet-600:oklch(60% .2 293);--color-violet-800:oklch(75% .18 293);--color-purple-100:oklch(22% .04 307);--color-purple-800:oklch(75% .17 304);--color-pink-100:oklch(22% .04 342);--color-pink-800:oklch(75% .15 4);--color-amber-400:oklch(80% .17 84)}input,textarea,select{background-color:var(--color-gray-50);color:var(--color-gray-900);border-color:var(--color-gray-200)}input::placeholder,textarea::placeholder{color:var(--color-gray-400)}input:focus,textarea:focus,select:focus{border-color:var(--color-green-300)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/templates/base.html.jinja b/templates/base.html.jinja index 60d9efd..70af5c2 100644 --- a/templates/base.html.jinja +++ b/templates/base.html.jinja @@ -99,6 +99,7 @@ Projects Members Destinations + Compute Integrations Usage Tokens diff --git a/templates/pages/compute.html.jinja b/templates/pages/compute.html.jinja new file mode 100644 index 0000000..d596e09 --- /dev/null +++ b/templates/pages/compute.html.jinja @@ -0,0 +1,103 @@ +{% extends "base.html.jinja" %} + +{% block content %} +
+
+

Compute

+
+ +

Managed container instances deployed through forage/containers destinations. Pay-as-you-go compute — no cluster setup required.

+ + {% if instances | length > 0 %} +
+ + + + + + + + + + + + + {% for inst in instances %} + + + + + + + + + {% endfor %} + +
Project / DestinationImageRegionReplicasResourcesStatus
+ {% if inst.project %} + {{ inst.project }} + / + {{ inst.destination }} + {% if inst.environment %} + {{ inst.environment }} + {% endif %} + {% else %} + {{ inst.resource_name }} + {% endif %} + {{ inst.image }} + + + {{ inst.region }} + + {{ inst.replicas }}{{ inst.cpu }} / {{ inst.memory }} + {% if inst.status == "running" %} + + + Running + + {% elif inst.status == "pending" %} + + + Pending + + {% else %} + {{ inst.status }} + {% endif %} +
+
+ {% else %} +
+ +

No compute instances running

+

Create a forage/containers destination and deploy to get started.

+
+ {% endif %} + + {% if rollouts | length > 0 %} +

Recent Rollouts

+ + {% endif %} +
+{% endblock %} diff --git a/templates/pages/rollout_detail.html.jinja b/templates/pages/rollout_detail.html.jinja new file mode 100644 index 0000000..00d5203 --- /dev/null +++ b/templates/pages/rollout_detail.html.jinja @@ -0,0 +1,89 @@ +{% extends "base.html.jinja" %} + +{% block content %} +
+
+ Compute + / + {{ rollout.apply_id }} +
+ +
+
+

Rollout

+ {% if rollout.status == "succeeded" %} + Succeeded + {% elif rollout.status == "in_progress" %} + In Progress + {% elif rollout.status == "failed" %} + Failed + {% else %} + {{ rollout.status }} + {% endif %} +
+
+ +
+
+
Apply ID
+
{{ rollout.apply_id }}
+
+
+
Namespace
+
{{ rollout.namespace }}
+
+
+ +

Resources ({{ rollout.resources | length }})

+
+ + + + + + + + + + + {% for r in rollout.resources %} + + + + + + + {% endfor %} + +
NameKindStatusMessage
{{ r.name }}{{ r.kind }} + {% if r.status == "succeeded" %} + + + Succeeded + + {% elif r.status == "in_progress" %} + + + In Progress + + {% elif r.status == "failed" %} + + + Failed + + {% else %} + {{ r.status }} + {% endif %} + {{ r.message }}
+
+ + {% if rollout.labels | length > 0 %} +

Labels

+
+ {% for label in rollout.labels %} + {{ label.key }}={{ label.value }} + {% endfor %} +
+ {% endif %} +
+{% endblock %}