diff --git a/SimpleModule.slnx b/SimpleModule.slnx index f31109b6..672e2baa 100644 --- a/SimpleModule.slnx +++ b/SimpleModule.slnx @@ -114,6 +114,11 @@ + + + + + diff --git a/framework/SimpleModule.Core/IModule.cs b/framework/SimpleModule.Core/IModule.cs index e9541d08..5457425e 100644 --- a/framework/SimpleModule.Core/IModule.cs +++ b/framework/SimpleModule.Core/IModule.cs @@ -7,6 +7,7 @@ using SimpleModule.Core.Authorization; using SimpleModule.Core.FeatureFlags; using SimpleModule.Core.Menu; +using SimpleModule.Core.RateLimiting; using SimpleModule.Core.Settings; namespace SimpleModule.Core; @@ -21,6 +22,7 @@ virtual void ConfigurePermissions(PermissionRegistryBuilder builder) { } virtual void ConfigureSettings(ISettingsBuilder settings) { } virtual void ConfigureFeatureFlags(IFeatureFlagBuilder builder) { } virtual void ConfigureAgents(IAgentBuilder builder) { } + virtual void ConfigureRateLimits(IRateLimitBuilder builder) { } /// /// Called once during application startup after all services are built but before diff --git a/framework/SimpleModule.Core/RateLimiting/EndpointRateLimitExtensions.cs b/framework/SimpleModule.Core/RateLimiting/EndpointRateLimitExtensions.cs new file mode 100644 index 00000000..440a737f --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/EndpointRateLimitExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Builder; + +namespace SimpleModule.Core.RateLimiting; + +public static class EndpointRateLimitExtensions +{ + public static TBuilder RateLimit(this TBuilder builder, string policyName) + where TBuilder : IEndpointConventionBuilder + { + builder.RequireRateLimiting(policyName); + return builder; + } +} diff --git a/framework/SimpleModule.Core/RateLimiting/IRateLimitBuilder.cs b/framework/SimpleModule.Core/RateLimiting/IRateLimitBuilder.cs new file mode 100644 index 00000000..1be0784a --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/IRateLimitBuilder.cs @@ -0,0 +1,6 @@ +namespace SimpleModule.Core.RateLimiting; + +public interface IRateLimitBuilder +{ + IRateLimitBuilder Add(RateLimitPolicyDefinition policy); +} diff --git a/framework/SimpleModule.Core/RateLimiting/IRateLimitPolicyRegistry.cs b/framework/SimpleModule.Core/RateLimiting/IRateLimitPolicyRegistry.cs new file mode 100644 index 00000000..9e329397 --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/IRateLimitPolicyRegistry.cs @@ -0,0 +1,7 @@ +namespace SimpleModule.Core.RateLimiting; + +public interface IRateLimitPolicyRegistry +{ + IReadOnlyList GetPolicies(); + RateLimitPolicyDefinition? GetPolicy(string name); +} diff --git a/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyDefinition.cs b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyDefinition.cs new file mode 100644 index 00000000..04be0cb8 --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyDefinition.cs @@ -0,0 +1,15 @@ +namespace SimpleModule.Core.RateLimiting; + +public sealed class RateLimitPolicyDefinition +{ + public required string Name { get; init; } + public RateLimitPolicyType PolicyType { get; init; } = RateLimitPolicyType.FixedWindow; + public RateLimitTarget Target { get; init; } = RateLimitTarget.Ip; + public int PermitLimit { get; init; } = 60; + public TimeSpan Window { get; init; } = TimeSpan.FromMinutes(1); + public int SegmentsPerWindow { get; init; } = 4; + public int TokenLimit { get; init; } = 100; + public int TokensPerPeriod { get; init; } = 10; + public TimeSpan ReplenishmentPeriod { get; init; } = TimeSpan.FromSeconds(10); + public int QueueLimit { get; init; } +} diff --git a/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyRegistry.cs b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyRegistry.cs new file mode 100644 index 00000000..02556d94 --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyRegistry.cs @@ -0,0 +1,19 @@ +namespace SimpleModule.Core.RateLimiting; + +public sealed class RateLimitPolicyRegistry : IRateLimitBuilder, IRateLimitPolicyRegistry +{ + private readonly Dictionary _policies = new( + StringComparer.OrdinalIgnoreCase + ); + + public IRateLimitBuilder Add(RateLimitPolicyDefinition policy) + { + _policies[policy.Name] = policy; + return this; + } + + public IReadOnlyList GetPolicies() => + _policies.Values.ToList().AsReadOnly(); + + public RateLimitPolicyDefinition? GetPolicy(string name) => _policies.GetValueOrDefault(name); +} diff --git a/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyType.cs b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyType.cs new file mode 100644 index 00000000..653a178c --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/RateLimitPolicyType.cs @@ -0,0 +1,8 @@ +namespace SimpleModule.Core.RateLimiting; + +public enum RateLimitPolicyType +{ + FixedWindow, + SlidingWindow, + TokenBucket, +} diff --git a/framework/SimpleModule.Core/RateLimiting/RateLimitTarget.cs b/framework/SimpleModule.Core/RateLimiting/RateLimitTarget.cs new file mode 100644 index 00000000..ff182209 --- /dev/null +++ b/framework/SimpleModule.Core/RateLimiting/RateLimitTarget.cs @@ -0,0 +1,9 @@ +namespace SimpleModule.Core.RateLimiting; + +public enum RateLimitTarget +{ + Ip, + User, + IpAndUser, + Global, +} diff --git a/framework/SimpleModule.Generator/Discovery/DiscoveryData.cs b/framework/SimpleModule.Generator/Discovery/DiscoveryData.cs index 2f989905..95c617e7 100644 --- a/framework/SimpleModule.Generator/Discovery/DiscoveryData.cs +++ b/framework/SimpleModule.Generator/Discovery/DiscoveryData.cs @@ -126,6 +126,7 @@ internal readonly record struct ModuleInfoRecord( bool HasConfigureSettings, bool HasConfigureFeatureFlags, bool HasConfigureAgents, + bool HasConfigureRateLimits, bool HasRazorComponents, string RoutePrefix, string ViewPrefix, @@ -146,6 +147,7 @@ public bool Equals(ModuleInfoRecord other) && HasConfigureSettings == other.HasConfigureSettings && HasConfigureFeatureFlags == other.HasConfigureFeatureFlags && HasConfigureAgents == other.HasConfigureAgents + && HasConfigureRateLimits == other.HasConfigureRateLimits && HasRazorComponents == other.HasRazorComponents && RoutePrefix == other.RoutePrefix && ViewPrefix == other.ViewPrefix @@ -167,6 +169,7 @@ public override int GetHashCode() hash = HashHelper.Combine(hash, HasConfigureSettings.GetHashCode()); hash = HashHelper.Combine(hash, HasConfigureFeatureFlags.GetHashCode()); hash = HashHelper.Combine(hash, HasConfigureAgents.GetHashCode()); + hash = HashHelper.Combine(hash, HasConfigureRateLimits.GetHashCode()); hash = HashHelper.Combine(hash, HasRazorComponents.GetHashCode()); hash = HashHelper.Combine(hash, (RoutePrefix ?? "").GetHashCode()); hash = HashHelper.Combine(hash, (ViewPrefix ?? "").GetHashCode()); @@ -462,6 +465,7 @@ internal sealed class ModuleInfo public bool HasConfigureSettings { get; set; } public bool HasConfigureFeatureFlags { get; set; } public bool HasConfigureAgents { get; set; } + public bool HasConfigureRateLimits { get; set; } public bool HasRazorComponents { get; set; } public string RoutePrefix { get; set; } = ""; public string ViewPrefix { get; set; } = ""; diff --git a/framework/SimpleModule.Generator/Discovery/SymbolDiscovery.cs b/framework/SimpleModule.Generator/Discovery/SymbolDiscovery.cs index b5b7a473..b9149fa9 100644 --- a/framework/SimpleModule.Generator/Discovery/SymbolDiscovery.cs +++ b/framework/SimpleModule.Generator/Discovery/SymbolDiscovery.cs @@ -683,6 +683,7 @@ is not IAssemblySymbol assemblySymbol m.HasConfigureSettings, m.HasConfigureFeatureFlags, m.HasConfigureAgents, + m.HasConfigureRateLimits, m.HasRazorComponents, m.RoutePrefix, m.ViewPrefix, @@ -910,6 +911,10 @@ moduleSettingsSymbol is not null "ConfigureFeatureFlags" ), HasConfigureAgents = DeclaresMethod(typeSymbol, "ConfigureAgents"), + HasConfigureRateLimits = DeclaresMethod( + typeSymbol, + "ConfigureRateLimits" + ), RoutePrefix = routePrefix, ViewPrefix = viewPrefix, Location = GetSourceLocation(typeSymbol), diff --git a/framework/SimpleModule.Generator/Emitters/ModuleExtensionsEmitter.cs b/framework/SimpleModule.Generator/Emitters/ModuleExtensionsEmitter.cs index 984102fe..2ebbd3d1 100644 --- a/framework/SimpleModule.Generator/Emitters/ModuleExtensionsEmitter.cs +++ b/framework/SimpleModule.Generator/Emitters/ModuleExtensionsEmitter.cs @@ -205,6 +205,21 @@ public void Emit(SourceProductionContext context, DiscoveryData data) " services.AddSingleton(featureFlagBuilder.Build());" ); + // Collect rate limit policy definitions from modules + sb.AppendLine( + " var rateLimitRegistry = new SimpleModule.Core.RateLimiting.RateLimitPolicyRegistry();" + ); + foreach (var module in sortedModules.Where(m => m.HasConfigureRateLimits)) + { + var fieldName = TypeMappingHelpers.GetModuleFieldName(module.FullyQualifiedName); + sb.AppendLine( + $" ((global::SimpleModule.Core.IModule){fieldName}).ConfigureRateLimits(rateLimitRegistry);" + ); + } + sb.AppendLine( + " SimpleModule.Hosting.RateLimiting.RateLimitingSetup.AddSimpleModuleRateLimiting(services, rateLimitRegistry);" + ); + if (hasDtoTypes) { sb.AppendLine(); diff --git a/framework/SimpleModule.Hosting/RateLimiting/RateLimitHeaderMiddleware.cs b/framework/SimpleModule.Hosting/RateLimiting/RateLimitHeaderMiddleware.cs new file mode 100644 index 00000000..16465432 --- /dev/null +++ b/framework/SimpleModule.Hosting/RateLimiting/RateLimitHeaderMiddleware.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RateLimiting; +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.Hosting.RateLimiting; + +public sealed class RateLimitHeaderMiddleware( + RequestDelegate next, + IRateLimitPolicyRegistry registry +) +{ + public async Task InvokeAsync(HttpContext context) + { + var endpoint = context.GetEndpoint(); + var rateLimitMetadata = endpoint?.Metadata.GetMetadata(); + + if (rateLimitMetadata is { PolicyName: { } policyName }) + { + var policy = registry.GetPolicy(policyName); + if (policy is not null) + { + context.Response.OnStarting(() => + { + if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests) + { + context.Response.Headers["X-RateLimit-Policy"] = policy.Name; + context.Response.Headers["X-RateLimit-Limit"] = + policy.PolicyType == RateLimitPolicyType.TokenBucket + ? policy.TokenLimit.ToString(CultureInfo.InvariantCulture) + : policy.PermitLimit.ToString(CultureInfo.InvariantCulture); + } + + return Task.CompletedTask; + }); + } + } + + await next(context); + } +} diff --git a/framework/SimpleModule.Hosting/RateLimiting/RateLimitingSetup.cs b/framework/SimpleModule.Hosting/RateLimiting/RateLimitingSetup.cs new file mode 100644 index 00000000..81805e83 --- /dev/null +++ b/framework/SimpleModule.Hosting/RateLimiting/RateLimitingSetup.cs @@ -0,0 +1,143 @@ +using System.Globalization; +using System.Security.Claims; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.Hosting.RateLimiting; + +public static class RateLimitingSetup +{ + public static IServiceCollection AddSimpleModuleRateLimiting( + this IServiceCollection services, + IRateLimitPolicyRegistry registry + ) + { + services.AddSingleton(registry); + + services.AddRateLimiter(options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + + options.OnRejected = async (context, cancellationToken) => + { + context.HttpContext.Response.Headers["Retry-After"] = context.Lease.TryGetMetadata( + MetadataName.RetryAfter, + out var retryAfter + ) + ? ((int)retryAfter.TotalSeconds).ToString(CultureInfo.InvariantCulture) + : "60"; + + context.HttpContext.Response.ContentType = "application/problem+json"; + await context.HttpContext.Response.WriteAsync( + """{"type":"https://httpstatuses.io/429","title":"Too Many Requests","status":429,"detail":"Rate limit exceeded. Please retry after the period indicated in the Retry-After header."}""", + cancellationToken + ); + }; + + foreach (var policy in registry.GetPolicies()) + { + RegisterPolicy(options, policy); + } + }); + + return services; + } + + public static WebApplication UseSimpleModuleRateLimiting(this WebApplication app) + { + app.UseMiddleware(); + app.UseRateLimiter(); + return app; + } + + private static void RegisterPolicy(RateLimiterOptions options, RateLimitPolicyDefinition policy) + { + switch (policy.PolicyType) + { + case RateLimitPolicyType.FixedWindow: + { + var limiterOptions = new FixedWindowRateLimiterOptions + { + PermitLimit = policy.PermitLimit, + Window = policy.Window, + QueueLimit = policy.QueueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + }; + options.AddPolicy( + policy.Name, + context => + RateLimitPartition.GetFixedWindowLimiter( + ResolvePartitionKey(context, policy.Target), + _ => limiterOptions + ) + ); + break; + } + case RateLimitPolicyType.SlidingWindow: + { + var limiterOptions = new SlidingWindowRateLimiterOptions + { + PermitLimit = policy.PermitLimit, + Window = policy.Window, + SegmentsPerWindow = policy.SegmentsPerWindow, + QueueLimit = policy.QueueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + }; + options.AddPolicy( + policy.Name, + context => + RateLimitPartition.GetSlidingWindowLimiter( + ResolvePartitionKey(context, policy.Target), + _ => limiterOptions + ) + ); + break; + } + case RateLimitPolicyType.TokenBucket: + { + var limiterOptions = new TokenBucketRateLimiterOptions + { + TokenLimit = policy.TokenLimit, + TokensPerPeriod = policy.TokensPerPeriod, + ReplenishmentPeriod = policy.ReplenishmentPeriod, + QueueLimit = policy.QueueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + }; + options.AddPolicy( + policy.Name, + context => + RateLimitPartition.GetTokenBucketLimiter( + ResolvePartitionKey(context, policy.Target), + _ => limiterOptions + ) + ); + break; + } + default: + options.AddPolicy( + policy.Name, + context => + RateLimitPartition.GetNoLimiter(ResolvePartitionKey(context, policy.Target)) + ); + break; + } + } + + private static string ResolvePartitionKey(HttpContext context, RateLimitTarget target) + { + return target switch + { + RateLimitTarget.Ip => context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + RateLimitTarget.User => context.User.FindFirstValue(ClaimTypes.NameIdentifier) + ?? "anonymous", + RateLimitTarget.IpAndUser => + $"{context.Connection.RemoteIpAddress}:{context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous"}", + RateLimitTarget.Global => "__global__", + _ => context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + }; + } +} diff --git a/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs b/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs index f1c210b7..0ad1b32a 100644 --- a/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs +++ b/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs @@ -14,10 +14,12 @@ using SimpleModule.Core.Exceptions; using SimpleModule.Core.Inertia; using SimpleModule.Core.Menu; +using SimpleModule.Core.RateLimiting; using SimpleModule.Database; using SimpleModule.Database.Health; using SimpleModule.Database.Interceptors; using SimpleModule.DevTools; +using SimpleModule.Hosting.RateLimiting; namespace SimpleModule.Hosting; @@ -157,6 +159,7 @@ public static async Task UseSimpleModuleInfrastructure(this WebApplication app) app.UseAuthentication(); app.UseAuthorization(); + app.UseSimpleModuleRateLimiting(); // Module middleware is added by the source-generated UseSimpleModule() // via IModule.ConfigureMiddleware() calls. diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/CreateRateLimitRuleRequest.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/CreateRateLimitRuleRequest.cs new file mode 100644 index 00000000..9849a7cf --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/CreateRateLimitRuleRequest.cs @@ -0,0 +1,19 @@ +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Contracts; + +public class CreateRateLimitRuleRequest +{ + public string PolicyName { get; set; } = string.Empty; + public RateLimitPolicyType PolicyType { get; set; } = RateLimitPolicyType.FixedWindow; + public RateLimitTarget Target { get; set; } = RateLimitTarget.Ip; + public int PermitLimit { get; set; } = 60; + public int WindowSeconds { get; set; } = 60; + public int SegmentsPerWindow { get; set; } = 4; + public int TokenLimit { get; set; } = 100; + public int TokensPerPeriod { get; set; } = 10; + public int ReplenishmentPeriodSeconds { get; set; } = 10; + public int QueueLimit { get; set; } + public string? EndpointPattern { get; set; } + public bool IsEnabled { get; set; } = true; +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/IRateLimitingContracts.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/IRateLimitingContracts.cs new file mode 100644 index 00000000..0fe75c6a --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/IRateLimitingContracts.cs @@ -0,0 +1,10 @@ +namespace SimpleModule.RateLimiting.Contracts; + +public interface IRateLimitingContracts +{ + Task> GetAllRulesAsync(); + Task GetRuleByIdAsync(RateLimitRuleId id); + Task CreateRuleAsync(CreateRateLimitRuleRequest request); + Task UpdateRuleAsync(RateLimitRuleId id, UpdateRateLimitRuleRequest request); + Task DeleteRuleAsync(RateLimitRuleId id); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRule.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRule.cs new file mode 100644 index 00000000..8260f3be --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRule.cs @@ -0,0 +1,22 @@ +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Contracts; + +public class RateLimitRule +{ + public RateLimitRuleId Id { get; set; } + public string PolicyName { get; set; } = string.Empty; + public RateLimitPolicyType PolicyType { get; set; } = RateLimitPolicyType.FixedWindow; + public RateLimitTarget Target { get; set; } = RateLimitTarget.Ip; + public int PermitLimit { get; set; } = 60; + public int WindowSeconds { get; set; } = 60; + public int SegmentsPerWindow { get; set; } = 4; + public int TokenLimit { get; set; } = 100; + public int TokensPerPeriod { get; set; } = 10; + public int ReplenishmentPeriodSeconds { get; set; } = 10; + public int QueueLimit { get; set; } + public string? EndpointPattern { get; set; } + public bool IsEnabled { get; set; } = true; + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRuleId.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRuleId.cs new file mode 100644 index 00000000..0c3116cf --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/RateLimitRuleId.cs @@ -0,0 +1,6 @@ +using Vogen; + +namespace SimpleModule.RateLimiting.Contracts; + +[ValueObject(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)] +public readonly partial struct RateLimitRuleId; diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/SimpleModule.RateLimiting.Contracts.csproj b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/SimpleModule.RateLimiting.Contracts.csproj new file mode 100644 index 00000000..97fdfa07 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/SimpleModule.RateLimiting.Contracts.csproj @@ -0,0 +1,12 @@ + + + net10.0 + Library + $(DefineConstants);VOGEN_NO_VALIDATION + + + + + + + diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/UpdateRateLimitRuleRequest.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/UpdateRateLimitRuleRequest.cs new file mode 100644 index 00000000..02e00695 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/UpdateRateLimitRuleRequest.cs @@ -0,0 +1,18 @@ +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Contracts; + +public class UpdateRateLimitRuleRequest +{ + public RateLimitPolicyType PolicyType { get; set; } = RateLimitPolicyType.FixedWindow; + public RateLimitTarget Target { get; set; } = RateLimitTarget.Ip; + public int PermitLimit { get; set; } = 60; + public int WindowSeconds { get; set; } = 60; + public int SegmentsPerWindow { get; set; } = 4; + public int TokenLimit { get; set; } = 100; + public int TokensPerPeriod { get; set; } = 10; + public int ReplenishmentPeriodSeconds { get; set; } = 10; + public int QueueLimit { get; set; } + public string? EndpointPattern { get; set; } + public bool IsEnabled { get; set; } = true; +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateEndpoint.cs new file mode 100644 index 00000000..987c0314 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateEndpoint.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Endpoints; +using SimpleModule.Core.Exceptions; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class CreateEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapPost( + "/", + (CreateRateLimitRuleRequest request, IRateLimitingContracts contracts) => + { + var validation = CreateRequestValidator.Validate(request); + if (!validation.IsValid) + { + throw new ValidationException(validation.Errors); + } + + return CrudEndpoints.Create( + () => contracts.CreateRuleAsync(request), + r => $"{RateLimitingConstants.RoutePrefix}/{r.Id}" + ); + } + ) + .RequirePermission(RateLimitingPermissions.Create); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateRequestValidator.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateRequestValidator.cs new file mode 100644 index 00000000..834e16bc --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/CreateRequestValidator.cs @@ -0,0 +1,21 @@ +using SimpleModule.Core.Validation; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public static class CreateRequestValidator +{ + public static ValidationResult Validate(CreateRateLimitRuleRequest request) => + new ValidationBuilder() + .AddErrorIf( + string.IsNullOrWhiteSpace(request.PolicyName), + "PolicyName", + "Policy name is required." + ) + .AddErrorIf( + request.PermitLimit <= 0, + "PermitLimit", + "Permit limit must be greater than zero." + ) + .Build(); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/DeleteEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/DeleteEndpoint.cs new file mode 100644 index 00000000..60ed4054 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/DeleteEndpoint.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Endpoints; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class DeleteEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapDelete( + "/{id:int}", + (int id, IRateLimitingContracts contracts) => + CrudEndpoints.Delete(() => contracts.DeleteRuleAsync(RateLimitRuleId.From(id))) + ) + .RequirePermission(RateLimitingPermissions.Delete); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetActivePoliciesEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetActivePoliciesEndpoint.cs new file mode 100644 index 00000000..31b30fb0 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetActivePoliciesEndpoint.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class GetActivePoliciesEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapGet( + "/active", + (IRateLimitPolicyRegistry registry) => + TypedResults.Ok( + registry + .GetPolicies() + .Select(p => new + { + p.Name, + PolicyType = p.PolicyType.ToString(), + Target = p.Target.ToString(), + p.PermitLimit, + WindowSeconds = (int)p.Window.TotalSeconds, + p.SegmentsPerWindow, + p.TokenLimit, + p.TokensPerPeriod, + ReplenishmentPeriodSeconds = (int) + p.ReplenishmentPeriod.TotalSeconds, + p.QueueLimit, + }) + ) + ) + .RequirePermission(RateLimitingPermissions.View); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetAllEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetAllEndpoint.cs new file mode 100644 index 00000000..85a86090 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetAllEndpoint.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Endpoints; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class GetAllEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapGet( + "/", + (IRateLimitingContracts contracts) => + CrudEndpoints.GetAll(contracts.GetAllRulesAsync) + ) + .RequirePermission(RateLimitingPermissions.View); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetByIdEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetByIdEndpoint.cs new file mode 100644 index 00000000..87b8151b --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/GetByIdEndpoint.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Endpoints; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class GetByIdEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapGet( + "/{id:int}", + (int id, IRateLimitingContracts contracts) => + CrudEndpoints.GetById(() => + contracts.GetRuleByIdAsync(RateLimitRuleId.From(id)) + ) + ) + .RequirePermission(RateLimitingPermissions.View); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/UpdateEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/UpdateEndpoint.cs new file mode 100644 index 00000000..6509c9ba --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Endpoints/Policies/UpdateEndpoint.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Endpoints; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Endpoints.Policies; + +public class UpdateEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder app) => + app.MapPut( + "/{id:int}", + (int id, UpdateRateLimitRuleRequest request, IRateLimitingContracts contracts) => + CrudEndpoints.Update(() => + contracts.UpdateRuleAsync(RateLimitRuleId.From(id), request) + ) + ) + .RequirePermission(RateLimitingPermissions.Update); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/EntityConfigurations/RateLimitRuleConfiguration.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/EntityConfigurations/RateLimitRuleConfiguration.cs new file mode 100644 index 00000000..49bae630 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/EntityConfigurations/RateLimitRuleConfiguration.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.EntityConfigurations; + +public class RateLimitRuleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedOnAdd(); + builder.Property(r => r.PolicyName).IsRequired().HasMaxLength(100); + builder.HasIndex(r => r.PolicyName).IsUnique(); + builder.Property(r => r.EndpointPattern).HasMaxLength(500); + builder.Property(r => r.PolicyType).HasConversion().HasMaxLength(50); + builder.Property(r => r.Target).HasConversion().HasMaxLength(50); + } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Locales/en.json b/modules/RateLimiting/src/SimpleModule.RateLimiting/Locales/en.json new file mode 100644 index 00000000..5f7e387a --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Locales/en.json @@ -0,0 +1,11 @@ +{ + "Admin.Title": "Rate Limiting", + "Admin.Description": "Manage rate limiting policies for API endpoints.", + "Admin.CreateButton": "Create Rule", + "Admin.EmptyTitle": "No rate limit rules", + "Admin.EmptyDescription": "Get started by creating your first rate limit rule.", + "Admin.DeleteButton": "Delete", + "Admin.DeleteDialog.Title": "Delete Rate Limit Rule", + "Admin.DeleteDialog.Confirm": "Are you sure you want to delete \"{name}\"? This action cannot be undone.", + "Admin.CancelButton": "Cancel" +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Pages/index.ts b/modules/RateLimiting/src/SimpleModule.RateLimiting/Pages/index.ts new file mode 100644 index 00000000..1e632f74 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Pages/index.ts @@ -0,0 +1,3 @@ +export const pages: Record = { + 'RateLimiting/Admin': () => import('../Views/Admin'), +}; diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingConstants.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingConstants.cs new file mode 100644 index 00000000..fa286396 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingConstants.cs @@ -0,0 +1,7 @@ +namespace SimpleModule.RateLimiting; + +public static class RateLimitingConstants +{ + public const string ModuleName = "RateLimiting"; + public const string RoutePrefix = "/api/rate-limiting"; +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingDbContext.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingDbContext.cs new file mode 100644 index 00000000..47a993d8 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingDbContext.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using SimpleModule.Database; +using SimpleModule.RateLimiting.Contracts; +using SimpleModule.RateLimiting.EntityConfigurations; + +namespace SimpleModule.RateLimiting; + +public class RateLimitingDbContext( + DbContextOptions options, + IOptions dbOptions +) : DbContext(options) +{ + public DbSet Rules => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new RateLimitRuleConfiguration()); + modelBuilder.ApplyModuleSchema("RateLimiting", dbOptions.Value); + } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder + .Properties() + .HaveConversion< + RateLimitRuleId.EfCoreValueConverter, + RateLimitRuleId.EfCoreValueComparer + >(); + } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingModule.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingModule.cs new file mode 100644 index 00000000..bac2707d --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingModule.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SimpleModule.Core; +using SimpleModule.Core.Menu; +using SimpleModule.Core.RateLimiting; +using SimpleModule.Database; + +namespace SimpleModule.RateLimiting; + +[Module( + RateLimitingConstants.ModuleName, + RoutePrefix = RateLimitingConstants.RoutePrefix, + ViewPrefix = "/rate-limiting" +)] +public class RateLimitingModule : IModule, IModuleMenu +{ + public void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddModuleDbContext( + configuration, + RateLimitingConstants.ModuleName + ); + } + + public void ConfigureRateLimits(IRateLimitBuilder builder) + { + builder + .Add( + new RateLimitPolicyDefinition + { + Name = "fixed-default", + PolicyType = RateLimitPolicyType.FixedWindow, + Target = RateLimitTarget.Ip, + PermitLimit = 60, + Window = TimeSpan.FromMinutes(1), + } + ) + .Add( + new RateLimitPolicyDefinition + { + Name = "sliding-strict", + PolicyType = RateLimitPolicyType.SlidingWindow, + Target = RateLimitTarget.IpAndUser, + PermitLimit = 30, + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = 6, + } + ) + .Add( + new RateLimitPolicyDefinition + { + Name = "token-bucket", + PolicyType = RateLimitPolicyType.TokenBucket, + Target = RateLimitTarget.Ip, + TokenLimit = 100, + TokensPerPeriod = 10, + ReplenishmentPeriod = TimeSpan.FromSeconds(10), + } + ) + .Add( + new RateLimitPolicyDefinition + { + Name = "auth-strict", + PolicyType = RateLimitPolicyType.FixedWindow, + Target = RateLimitTarget.Ip, + PermitLimit = 10, + Window = TimeSpan.FromMinutes(1), + } + ); + } + + public void ConfigureMenu(IMenuBuilder menus) + { + menus.Add( + new MenuItem + { + Label = "Rate Limiting", + Url = "/rate-limiting", + Icon = + """""", + Order = 85, + Section = MenuSection.AdminSidebar, + } + ); + } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingPermissions.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingPermissions.cs new file mode 100644 index 00000000..ad28677d --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingPermissions.cs @@ -0,0 +1,11 @@ +using SimpleModule.Core.Authorization; + +namespace SimpleModule.RateLimiting; + +public sealed class RateLimitingPermissions : IModulePermissions +{ + public const string View = "RateLimiting.View"; + public const string Create = "RateLimiting.Create"; + public const string Update = "RateLimiting.Update"; + public const string Delete = "RateLimiting.Delete"; +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingService.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingService.cs new file mode 100644 index 00000000..2a4885a2 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/RateLimitingService.cs @@ -0,0 +1,126 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting; + +public partial class RateLimitingService( + RateLimitingDbContext db, + ILogger logger +) : IRateLimitingContracts +{ + public async Task> GetAllRulesAsync() => + await db.Rules.AsNoTracking().OrderBy(r => r.PolicyName).ToListAsync(); + + public async Task GetRuleByIdAsync(RateLimitRuleId id) + { + var rule = await db.Rules.FindAsync(id); + if (rule is null) + { + LogRuleNotFound(logger, id); + } + + return rule; + } + + public async Task CreateRuleAsync(CreateRateLimitRuleRequest request) + { + var rule = new RateLimitRule + { + PolicyName = request.PolicyName, + PolicyType = request.PolicyType, + Target = request.Target, + PermitLimit = request.PermitLimit, + WindowSeconds = request.WindowSeconds, + SegmentsPerWindow = request.SegmentsPerWindow, + TokenLimit = request.TokenLimit, + TokensPerPeriod = request.TokensPerPeriod, + ReplenishmentPeriodSeconds = request.ReplenishmentPeriodSeconds, + QueueLimit = request.QueueLimit, + EndpointPattern = request.EndpointPattern, + IsEnabled = request.IsEnabled, + CreatedAt = DateTime.UtcNow, + }; + + db.Rules.Add(rule); + await db.SaveChangesAsync(); + + LogRuleCreated(logger, rule.Id, rule.PolicyName); + + return rule; + } + + public async Task UpdateRuleAsync( + RateLimitRuleId id, + UpdateRateLimitRuleRequest request + ) + { + var rule = await db.Rules.FindAsync(id); + if (rule is null) + { + throw new Core.Exceptions.NotFoundException("RateLimitRule", id); + } + + rule.PolicyType = request.PolicyType; + rule.Target = request.Target; + rule.PermitLimit = request.PermitLimit; + rule.WindowSeconds = request.WindowSeconds; + rule.SegmentsPerWindow = request.SegmentsPerWindow; + rule.TokenLimit = request.TokenLimit; + rule.TokensPerPeriod = request.TokensPerPeriod; + rule.ReplenishmentPeriodSeconds = request.ReplenishmentPeriodSeconds; + rule.QueueLimit = request.QueueLimit; + rule.EndpointPattern = request.EndpointPattern; + rule.IsEnabled = request.IsEnabled; + rule.UpdatedAt = DateTime.UtcNow; + + await db.SaveChangesAsync(); + + LogRuleUpdated(logger, rule.Id, rule.PolicyName); + + return rule; + } + + public async Task DeleteRuleAsync(RateLimitRuleId id) + { + var rule = await db.Rules.FindAsync(id); + if (rule is null) + { + throw new Core.Exceptions.NotFoundException("RateLimitRule", id); + } + + db.Rules.Remove(rule); + await db.SaveChangesAsync(); + + LogRuleDeleted(logger, id); + } + + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Rate limit rule with ID {RuleId} not found" + )] + private static partial void LogRuleNotFound(ILogger logger, RateLimitRuleId ruleId); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Rate limit rule {RuleId} created: {PolicyName}" + )] + private static partial void LogRuleCreated( + ILogger logger, + RateLimitRuleId ruleId, + string policyName + ); + + [LoggerMessage( + Level = LogLevel.Information, + Message = "Rate limit rule {RuleId} updated: {PolicyName}" + )] + private static partial void LogRuleUpdated( + ILogger logger, + RateLimitRuleId ruleId, + string policyName + ); + + [LoggerMessage(Level = LogLevel.Information, Message = "Rate limit rule {RuleId} deleted")] + private static partial void LogRuleDeleted(ILogger logger, RateLimitRuleId ruleId); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/SimpleModule.RateLimiting.csproj b/modules/RateLimiting/src/SimpleModule.RateLimiting/SimpleModule.RateLimiting.csproj new file mode 100644 index 00000000..d68b733a --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/SimpleModule.RateLimiting.csproj @@ -0,0 +1,19 @@ + + + net10.0 + + + + + + + + + + + + + %(Filename)Endpoint.cs + + + diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/Admin.tsx b/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/Admin.tsx new file mode 100644 index 00000000..c8b5d5fa --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/Admin.tsx @@ -0,0 +1,404 @@ +import { router } from '@inertiajs/react'; +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + Input, + Label, + PageShell, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Switch, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@simplemodule/ui'; +import { useState } from 'react'; + +interface RateLimitRule { + id: number; + policyName: string; + policyType: string; + target: string; + permitLimit: number; + windowSeconds: number; + segmentsPerWindow: number; + tokenLimit: number; + tokensPerPeriod: number; + replenishmentPeriodSeconds: number; + queueLimit: number; + endpointPattern: string | null; + isEnabled: boolean; + createdAt: string; + updatedAt: string | null; +} + +interface ActivePolicy { + name: string; + policyType: string; + target: string; + permitLimit: number; + windowSeconds: number; + tokenLimit: number; + tokensPerPeriod: number; +} + +interface AdminProps { + rules: RateLimitRule[]; + activePolicies: ActivePolicy[]; +} + +const policyTypes = ['FixedWindow', 'SlidingWindow', 'TokenBucket']; +const targets = ['Ip', 'User', 'IpAndUser', 'Global']; +const API_BASE = '/api/rate-limiting'; + +function PolicyTypeBadge({ type }: { type: string }) { + const variant = + type === 'TokenBucket' ? 'secondary' : type === 'SlidingWindow' ? 'outline' : 'default'; + return {type}; +} + +function TargetBadge({ target }: { target: string }) { + return {target}; +} + +export default function Admin({ rules, activePolicies }: AdminProps) { + const [showCreateDialog, setShowCreateDialog] = useState(false); + const [formData, setFormData] = useState({ + policyName: '', + policyType: 'FixedWindow', + target: 'Ip', + permitLimit: 60, + windowSeconds: 60, + segmentsPerWindow: 4, + tokenLimit: 100, + tokensPerPeriod: 10, + replenishmentPeriodSeconds: 10, + queueLimit: 0, + endpointPattern: '', + isEnabled: true, + }); + + const handleCreate = async () => { + await fetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); + setShowCreateDialog(false); + router.reload(); + }; + + const handleDelete = async (id: number) => { + await fetch(`${API_BASE}/${id}`, { method: 'DELETE' }); + router.reload(); + }; + + const handleToggle = async (rule: RateLimitRule) => { + await fetch(`${API_BASE}/${rule.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + policyType: rule.policyType, + target: rule.target, + permitLimit: rule.permitLimit, + windowSeconds: rule.windowSeconds, + segmentsPerWindow: rule.segmentsPerWindow, + tokenLimit: rule.tokenLimit, + tokensPerPeriod: rule.tokensPerPeriod, + replenishmentPeriodSeconds: rule.replenishmentPeriodSeconds, + queueLimit: rule.queueLimit, + endpointPattern: rule.endpointPattern, + isEnabled: !rule.isEnabled, + }), + }); + router.reload(); + }; + + return ( + + + + Stored Rules + Active Policies + + + + + +
+
+ Rate Limit Rules + + Manage rate limiting policies stored in the database. + +
+ + + + + + + Create Rate Limit Rule + +
+
+ + + setFormData((prev) => ({ ...prev, policyName: e.target.value })) + } + placeholder="e.g., api-default" + /> +
+
+
+ + +
+
+ + +
+
+ {formData.policyType !== 'TokenBucket' && ( +
+
+ + + setFormData((prev) => ({ + ...prev, + permitLimit: Number.parseInt(e.target.value, 10), + })) + } + /> +
+
+ + + setFormData((prev) => ({ + ...prev, + windowSeconds: Number.parseInt(e.target.value, 10), + })) + } + /> +
+
+ )} + {formData.policyType === 'TokenBucket' && ( +
+
+ + + setFormData((prev) => ({ + ...prev, + tokenLimit: Number.parseInt(e.target.value, 10), + })) + } + /> +
+
+ + + setFormData((prev) => ({ + ...prev, + tokensPerPeriod: Number.parseInt(e.target.value, 10), + })) + } + /> +
+
+ )} +
+ + + setFormData((prev) => ({ ...prev, endpointPattern: e.target.value })) + } + placeholder="e.g., /api/products/*" + /> +
+ +
+
+
+
+
+ + {rules.length === 0 ? ( +

+ No rate limit rules configured yet. +

+ ) : ( + + + + Policy Name + Type + Target + Limit + Endpoint + Enabled + + + + + {rules.map((rule) => ( + + {rule.policyName} + + + + + + + + {rule.policyType === 'TokenBucket' + ? `${rule.tokenLimit} tokens` + : `${rule.permitLimit} req/${rule.windowSeconds}s`} + + + {rule.endpointPattern ?? 'All'} + + + handleToggle(rule)} + /> + + + + + + ))} + +
+ )} +
+
+
+ + + + + Active Policies + + Rate limit policies currently active in the middleware pipeline. These are + registered at application startup via module ConfigureRateLimits() hooks. + + + + {activePolicies.length === 0 ? ( +

+ No active rate limit policies. +

+ ) : ( + + + + Name + Type + Target + Limit + + + + {activePolicies.map((policy) => ( + + + {policy.name} + + + + + + + + + {policy.policyType === 'TokenBucket' + ? `${policy.tokenLimit} tokens (${policy.tokensPerPeriod}/period)` + : `${policy.permitLimit} req/${policy.windowSeconds}s`} + + + ))} + +
+ )} +
+
+
+
+
+ ); +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/AdminEndpoint.cs b/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/AdminEndpoint.cs new file mode 100644 index 00000000..e9e9190b --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/Views/AdminEndpoint.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using SimpleModule.Core; +using SimpleModule.Core.Authorization; +using SimpleModule.Core.Inertia; +using SimpleModule.Core.RateLimiting; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Views; + +[ViewPage("RateLimiting/Admin")] +public class AdminEndpoint : IViewEndpoint +{ + public void Map(IEndpointRouteBuilder app) + { + app.MapGet( + "/", + async (IRateLimitingContracts contracts, IRateLimitPolicyRegistry policyRegistry) => + { + var rules = await contracts.GetAllRulesAsync(); + var activePolicies = policyRegistry.GetPolicies(); + return Inertia.Render( + "RateLimiting/Admin", + new + { + rules, + activePolicies = activePolicies.Select(p => new + { + p.Name, + PolicyType = p.PolicyType.ToString(), + Target = p.Target.ToString(), + p.PermitLimit, + WindowSeconds = (int)p.Window.TotalSeconds, + p.TokenLimit, + p.TokensPerPeriod, + }), + } + ); + } + ) + .RequirePermission(RateLimitingPermissions.View); + } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/package.json b/modules/RateLimiting/src/SimpleModule.RateLimiting/package.json new file mode 100644 index 00000000..1b93ada1 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "name": "@simplemodule/ratelimiting", + "version": "0.0.0", + "scripts": { + "build": "cross-env VITE_MODE=prod vite build --configLoader runner", + "build:dev": "cross-env VITE_MODE=dev vite build --configLoader runner", + "watch": "cross-env VITE_MODE=dev vite build --configLoader runner --watch" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/types.ts b/modules/RateLimiting/src/SimpleModule.RateLimiting/types.ts new file mode 100644 index 00000000..7dce28da --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/types.ts @@ -0,0 +1,48 @@ +// Auto-generated from [Dto] types — do not edit +export interface CreateRateLimitRuleRequest { + policyName: string; + policyType: any; + target: any; + permitLimit: number; + windowSeconds: number; + segmentsPerWindow: number; + tokenLimit: number; + tokensPerPeriod: number; + replenishmentPeriodSeconds: number; + queueLimit: number; + endpointPattern: string; + isEnabled: boolean; +} + +export interface RateLimitRule { + id: number; + policyName: string; + policyType: any; + target: any; + permitLimit: number; + windowSeconds: number; + segmentsPerWindow: number; + tokenLimit: number; + tokensPerPeriod: number; + replenishmentPeriodSeconds: number; + queueLimit: number; + endpointPattern: string; + isEnabled: boolean; + createdAt: string; + updatedAt: string | null; +} + +export interface UpdateRateLimitRuleRequest { + policyType: any; + target: any; + permitLimit: number; + windowSeconds: number; + segmentsPerWindow: number; + tokenLimit: number; + tokensPerPeriod: number; + replenishmentPeriodSeconds: number; + queueLimit: number; + endpointPattern: string; + isEnabled: boolean; +} + diff --git a/modules/RateLimiting/src/SimpleModule.RateLimiting/vite.config.ts b/modules/RateLimiting/src/SimpleModule.RateLimiting/vite.config.ts new file mode 100644 index 00000000..a247db62 --- /dev/null +++ b/modules/RateLimiting/src/SimpleModule.RateLimiting/vite.config.ts @@ -0,0 +1,3 @@ +import { defineModuleConfig } from '@simplemodule/client/module'; + +export default defineModuleConfig(import.meta.dirname); diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/GlobalUsings.cs b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/GlobalUsings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitPolicyRegistryTests.cs b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitPolicyRegistryTests.cs new file mode 100644 index 00000000..44d76804 --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitPolicyRegistryTests.cs @@ -0,0 +1,114 @@ +using FluentAssertions; +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Tests; + +public class RateLimitPolicyRegistryTests +{ + [Fact] + public void Add_ShouldRegisterPolicy() + { + var registry = new RateLimitPolicyRegistry(); + + registry.Add( + new RateLimitPolicyDefinition + { + Name = "test-policy", + PolicyType = RateLimitPolicyType.FixedWindow, + PermitLimit = 100, + Window = TimeSpan.FromMinutes(1), + } + ); + + registry.GetPolicies().Should().HaveCount(1); + registry.GetPolicies()[0].Name.Should().Be("test-policy"); + } + + [Fact] + public void GetPolicy_ShouldReturnPolicy_WhenExists() + { + var registry = new RateLimitPolicyRegistry(); + registry.Add( + new RateLimitPolicyDefinition + { + Name = "my-policy", + PolicyType = RateLimitPolicyType.SlidingWindow, + PermitLimit = 50, + } + ); + + var policy = registry.GetPolicy("my-policy"); + + policy.Should().NotBeNull(); + policy!.PolicyType.Should().Be(RateLimitPolicyType.SlidingWindow); + policy.PermitLimit.Should().Be(50); + } + + [Fact] + public void GetPolicy_ShouldReturnNull_WhenNotFound() + { + var registry = new RateLimitPolicyRegistry(); + + var policy = registry.GetPolicy("nonexistent"); + + policy.Should().BeNull(); + } + + [Fact] + public void GetPolicy_ShouldBeCaseInsensitive() + { + var registry = new RateLimitPolicyRegistry(); + registry.Add(new RateLimitPolicyDefinition { Name = "Test-Policy" }); + + var policy = registry.GetPolicy("test-policy"); + + policy.Should().NotBeNull(); + } + + [Fact] + public void Add_ShouldSupportChainingMultiplePolicies() + { + var registry = new RateLimitPolicyRegistry(); + + registry + .Add( + new RateLimitPolicyDefinition + { + Name = "fixed", + PolicyType = RateLimitPolicyType.FixedWindow, + } + ) + .Add( + new RateLimitPolicyDefinition + { + Name = "sliding", + PolicyType = RateLimitPolicyType.SlidingWindow, + } + ) + .Add( + new RateLimitPolicyDefinition + { + Name = "token", + PolicyType = RateLimitPolicyType.TokenBucket, + } + ); + + registry.GetPolicies().Should().HaveCount(3); + } + + [Fact] + public void DefaultValues_ShouldBeReasonable() + { + var definition = new RateLimitPolicyDefinition { Name = "defaults" }; + + definition.PolicyType.Should().Be(RateLimitPolicyType.FixedWindow); + definition.Target.Should().Be(RateLimitTarget.Ip); + definition.PermitLimit.Should().Be(60); + definition.Window.Should().Be(TimeSpan.FromMinutes(1)); + definition.SegmentsPerWindow.Should().Be(4); + definition.TokenLimit.Should().Be(100); + definition.TokensPerPeriod.Should().Be(10); + definition.ReplenishmentPeriod.Should().Be(TimeSpan.FromSeconds(10)); + definition.QueueLimit.Should().Be(0); + } +} diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingEndpointTests.cs b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingEndpointTests.cs new file mode 100644 index 00000000..e2905058 --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingEndpointTests.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Security.Claims; +using FluentAssertions; +using SimpleModule.Tests.Shared.Fixtures; + +namespace SimpleModule.RateLimiting.Tests; + +public class RateLimitingEndpointTests : IClassFixture +{ + private readonly SimpleModuleWebApplicationFactory _factory; + + public RateLimitingEndpointTests(SimpleModuleWebApplicationFactory factory) => + _factory = factory; + + [Fact] + public async Task AdminPage_ReturnsOk_ForAuthenticatedAdmin() + { + using var client = _factory.CreateAuthenticatedClient(new Claim(ClaimTypes.Role, "Admin")); + + var response = await client.GetAsync("/rate-limiting"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Contain("RateLimiting/Admin"); + } + + [Fact] + public async Task ActivePoliciesApi_ReturnsOk_ForAuthenticatedAdmin() + { + using var client = _factory.CreateAuthenticatedClient(new Claim(ClaimTypes.Role, "Admin")); + + var response = await client.GetAsync("/api/rate-limiting/active"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Contain("fixed-default"); + } + + [Fact] + public async Task RulesApi_ReturnsOk_ForAuthenticatedAdmin() + { + using var client = _factory.CreateAuthenticatedClient(new Claim(ClaimTypes.Role, "Admin")); + + var response = await client.GetAsync("/api/rate-limiting"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingModuleTests.cs b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingModuleTests.cs new file mode 100644 index 00000000..33c83164 --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingModuleTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using SimpleModule.Core.RateLimiting; + +namespace SimpleModule.RateLimiting.Tests; + +public class RateLimitingModuleTests +{ + [Fact] + public void ConfigureRateLimits_ShouldRegisterBuiltInPolicies() + { + var module = new RateLimitingModule(); + var registry = new RateLimitPolicyRegistry(); + + module.ConfigureRateLimits(registry); + + var policies = registry.GetPolicies(); + policies.Should().HaveCount(4); + policies.Should().Contain(p => p.Name == "fixed-default"); + policies.Should().Contain(p => p.Name == "sliding-strict"); + policies.Should().Contain(p => p.Name == "token-bucket"); + policies.Should().Contain(p => p.Name == "auth-strict"); + } + + [Fact] + public void ConfigureRateLimits_FixedDefault_ShouldHaveCorrectSettings() + { + var module = new RateLimitingModule(); + var registry = new RateLimitPolicyRegistry(); + + module.ConfigureRateLimits(registry); + + var policy = registry.GetPolicy("fixed-default"); + policy.Should().NotBeNull(); + policy!.PolicyType.Should().Be(RateLimitPolicyType.FixedWindow); + policy.Target.Should().Be(RateLimitTarget.Ip); + policy.PermitLimit.Should().Be(60); + policy.Window.Should().Be(TimeSpan.FromMinutes(1)); + } + + [Fact] + public void ConfigureRateLimits_AuthStrict_ShouldHaveLowLimit() + { + var module = new RateLimitingModule(); + var registry = new RateLimitPolicyRegistry(); + + module.ConfigureRateLimits(registry); + + var policy = registry.GetPolicy("auth-strict"); + policy.Should().NotBeNull(); + policy!.PermitLimit.Should().Be(10); + } + + [Fact] + public void ConfigureRateLimits_TokenBucket_ShouldHaveCorrectSettings() + { + var module = new RateLimitingModule(); + var registry = new RateLimitPolicyRegistry(); + + module.ConfigureRateLimits(registry); + + var policy = registry.GetPolicy("token-bucket"); + policy.Should().NotBeNull(); + policy!.PolicyType.Should().Be(RateLimitPolicyType.TokenBucket); + policy.TokenLimit.Should().Be(100); + policy.TokensPerPeriod.Should().Be(10); + policy.ReplenishmentPeriod.Should().Be(TimeSpan.FromSeconds(10)); + } +} diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingServiceTests.cs b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingServiceTests.cs new file mode 100644 index 00000000..883a9d8d --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/RateLimitingServiceTests.cs @@ -0,0 +1,146 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using SimpleModule.Core.RateLimiting; +using SimpleModule.Database; +using SimpleModule.RateLimiting.Contracts; + +namespace SimpleModule.RateLimiting.Tests; + +public sealed class RateLimitingServiceTests : IDisposable +{ + private readonly RateLimitingDbContext _db; + private readonly RateLimitingService _service; + + public RateLimitingServiceTests() + { + var dbOptions = new DbContextOptionsBuilder() + .UseSqlite("Data Source=:memory:") + .Options; + var databaseOptions = Options.Create( + new DatabaseOptions + { + ModuleConnections = new Dictionary + { + ["RateLimiting"] = "Data Source=:memory:", + }, + } + ); + _db = new RateLimitingDbContext(dbOptions, databaseOptions); + _db.Database.OpenConnection(); + _db.Database.EnsureCreated(); + _service = new RateLimitingService(_db, NullLogger.Instance); + } + + [Fact] + public async Task CreateRuleAsync_ShouldPersistRule() + { + var request = new CreateRateLimitRuleRequest + { + PolicyName = "test-fixed", + PolicyType = RateLimitPolicyType.FixedWindow, + Target = RateLimitTarget.Ip, + PermitLimit = 100, + WindowSeconds = 60, + }; + + var result = await _service.CreateRuleAsync(request); + + result.PolicyName.Should().Be("test-fixed"); + result.PermitLimit.Should().Be(100); + result.Id.Value.Should().BeGreaterThan(0); + } + + [Fact] + public async Task GetAllRulesAsync_ShouldReturnAllRules() + { + await _service.CreateRuleAsync(new CreateRateLimitRuleRequest { PolicyName = "rule-1" }); + await _service.CreateRuleAsync(new CreateRateLimitRuleRequest { PolicyName = "rule-2" }); + + var rules = await _service.GetAllRulesAsync(); + + rules.Should().HaveCount(2); + } + + [Fact] + public async Task GetRuleByIdAsync_ShouldReturnRule_WhenExists() + { + var created = await _service.CreateRuleAsync( + new CreateRateLimitRuleRequest { PolicyName = "find-me" } + ); + + var found = await _service.GetRuleByIdAsync(created.Id); + + found.Should().NotBeNull(); + found!.PolicyName.Should().Be("find-me"); + } + + [Fact] + public async Task GetRuleByIdAsync_ShouldReturnNull_WhenNotFound() + { + var found = await _service.GetRuleByIdAsync(RateLimitRuleId.From(999)); + + found.Should().BeNull(); + } + + [Fact] + public async Task UpdateRuleAsync_ShouldModifyRule() + { + var created = await _service.CreateRuleAsync( + new CreateRateLimitRuleRequest { PolicyName = "update-me", PermitLimit = 60 } + ); + + var updated = await _service.UpdateRuleAsync( + created.Id, + new UpdateRateLimitRuleRequest + { + PermitLimit = 200, + PolicyType = RateLimitPolicyType.SlidingWindow, + Target = RateLimitTarget.User, + } + ); + + updated.PermitLimit.Should().Be(200); + updated.PolicyType.Should().Be(RateLimitPolicyType.SlidingWindow); + updated.Target.Should().Be(RateLimitTarget.User); + updated.UpdatedAt.Should().NotBeNull(); + } + + [Fact] + public async Task DeleteRuleAsync_ShouldRemoveRule() + { + var created = await _service.CreateRuleAsync( + new CreateRateLimitRuleRequest { PolicyName = "delete-me" } + ); + + await _service.DeleteRuleAsync(created.Id); + + var found = await _service.GetRuleByIdAsync(created.Id); + found.Should().BeNull(); + } + + [Fact] + public async Task DeleteRuleAsync_ShouldThrow_WhenNotFound() + { + var act = () => _service.DeleteRuleAsync(RateLimitRuleId.From(999)); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task UpdateRuleAsync_ShouldThrow_WhenNotFound() + { + var act = () => + _service.UpdateRuleAsync(RateLimitRuleId.From(999), new UpdateRateLimitRuleRequest()); + + await act.Should().ThrowAsync(); + } + + public void Dispose() + { + _db.Database.CloseConnection(); + _db.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/SimpleModule.RateLimiting.Tests.csproj b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/SimpleModule.RateLimiting.Tests.csproj new file mode 100644 index 00000000..37878918 --- /dev/null +++ b/modules/RateLimiting/tests/SimpleModule.RateLimiting.Tests/SimpleModule.RateLimiting.Tests.csproj @@ -0,0 +1,22 @@ + + + net10.0 + false + Exe + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index aa64ad8f..9c39925f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -143,6 +143,14 @@ "react-dom": "^19.0.0" } }, + "modules/RateLimiting/src/SimpleModule.RateLimiting": { + "name": "@simplemodule/ratelimiting", + "version": "0.0.0", + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "modules/Settings/src/SimpleModule.Settings": { "name": "@simplemodule/settings", "version": "0.0.0", @@ -4112,6 +4120,10 @@ "resolved": "modules/Products/src/SimpleModule.Products", "link": true }, + "node_modules/@simplemodule/ratelimiting": { + "resolved": "modules/RateLimiting/src/SimpleModule.RateLimiting", + "link": true + }, "node_modules/@simplemodule/settings": { "resolved": "modules/Settings/src/SimpleModule.Settings", "link": true diff --git a/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.Designer.cs b/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.Designer.cs new file mode 100644 index 00000000..ba45e522 --- /dev/null +++ b/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.Designer.cs @@ -0,0 +1,1830 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SimpleModule.Host; + +#nullable disable + +namespace SimpleModule.Host.Migrations +{ + [DbContext(typeof(HostDbContext))] + [Migration("20260403145434_AddRateLimitingModule")] + partial class AddRateLimitingModule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users_AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Users_AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("Users_AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("Users_AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("Users_AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("JsonWebKeySet") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedirectUris") + .HasColumnType("TEXT"); + + b.Property("Requirements") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddict_OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Scopes") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddict_OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Descriptions") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Resources") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddict_OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("AuthorizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Payload") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedemptionDate") + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddict_OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Agents.Sessions.AgentMessage", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("TokenCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.HasIndex("SessionId", "Timestamp"); + + b.ToTable("Agents_Messages", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Agents.Sessions.AgentSession", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LastMessageAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("UserId"); + + b.ToTable("Agents_Sessions", (string)null); + }); + + modelBuilder.Entity("SimpleModule.AuditLogs.Contracts.AuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("Changes") + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("DurationMs") + .HasColumnType("INTEGER"); + + b.Property("EntityId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EntityType") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("HttpMethod") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Module") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Path") + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("QueryString") + .HasColumnType("TEXT"); + + b.Property("RequestBody") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("UserAgent") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("Path"); + + b.HasIndex("Source"); + + b.HasIndex("StatusCode"); + + b.HasIndex("Timestamp") + .IsDescending(); + + b.HasIndex("EntityType", "EntityId"); + + b.HasIndex("Module", "Timestamp") + .IsDescending(false, true); + + b.HasIndex("UserId", "Timestamp") + .IsDescending(false, true); + + b.ToTable("AuditLogs_AuditEntries", (string)null); + }); + + modelBuilder.Entity("SimpleModule.BackgroundJobs.Entities.JobProgress", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("JobTypeName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Logs") + .HasColumnType("TEXT"); + + b.Property("ModuleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProgressMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ModuleName"); + + b.ToTable("BackgroundJobs_JobProgress", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Email.Contracts.EmailMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bcc") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Cc") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsHtml") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ReplyTo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SentAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TemplateSlug") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("To") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("Email_EmailMessages", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Email.Contracts.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DefaultReplyTo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsHtml") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Email_EmailTemplates", (string)null); + }); + + modelBuilder.Entity("SimpleModule.FeatureFlags.Entities.FeatureFlagEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeprecated") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FeatureFlags_FeatureFlags", (string)null); + }); + + modelBuilder.Entity("SimpleModule.FeatureFlags.Entities.FeatureFlagOverrideEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FlagName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("OverrideType") + .HasColumnType("INTEGER"); + + b.Property("OverrideValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FlagName", "OverrideType", "OverrideValue") + .IsUnique(); + + b.ToTable("FeatureFlags_FeatureFlagOverrides", (string)null); + }); + + modelBuilder.Entity("SimpleModule.FileStorage.Contracts.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Folder") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Folder"); + + b.HasIndex("Folder", "FileName") + .IsUnique(); + + b.ToTable("FileStorage_StoredFiles", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Orders.Contracts.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Orders_Orders", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Orders.Contracts.OrderItem", b => + { + b.Property("OrderId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("OrderId", "ProductId"); + + b.ToTable("Orders_OrderItems", (string)null); + }); + + modelBuilder.Entity("SimpleModule.PageBuilder.Contracts.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DraftContent") + .HasColumnType("TEXT"); + + b.Property("IsPublished") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("MetaDescription") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MetaKeywords") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OgImage") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Order") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletedAt"); + + b.HasIndex("IsPublished"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("PageBuilder_Pages", (string)null); + }); + + modelBuilder.Entity("SimpleModule.PageBuilder.Contracts.PageTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PageId"); + + b.ToTable("PageBuilder_Tags", (string)null); + }); + + modelBuilder.Entity("SimpleModule.PageBuilder.Contracts.PageTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PageBuilder_Templates", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Permissions.Entities.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasColumnType("TEXT"); + + b.HasKey("RoleId", "Permission"); + + b.ToTable("Permissions_RolePermissions", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Permissions.Entities.UserPermission", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "Permission"); + + b.ToTable("Permissions_UserPermissions", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Products.Contracts.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Products_Products", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Fantastic Rubber Shoes", + Price = 991.68m + }, + new + { + Id = 2, + Name = "Fantastic Rubber Bacon", + Price = 446.22m + }, + new + { + Id = 3, + Name = "Fantastic Concrete Bike", + Price = 660.12m + }, + new + { + Id = 4, + Name = "Handcrafted Concrete Keyboard", + Price = 633.67m + }, + new + { + Id = 5, + Name = "Intelligent Frozen Mouse", + Price = 674.30m + }, + new + { + Id = 6, + Name = "Sleek Soft Hat", + Price = 851.63m + }, + new + { + Id = 7, + Name = "Practical Fresh Bike", + Price = 417.48m + }, + new + { + Id = 8, + Name = "Handmade Steel Ball", + Price = 975.56m + }, + new + { + Id = 9, + Name = "Ergonomic Fresh Pants", + Price = 928.09m + }, + new + { + Id = 10, + Name = "Licensed Steel Sausages", + Price = 592.60m + }); + }); + + modelBuilder.Entity("SimpleModule.Rag.StructuredRag.Data.CachedStructuredKnowledge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DocumentHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("SourceTitle") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("StructureType") + .HasColumnType("INTEGER"); + + b.Property("StructuredContent") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("CollectionName", "DocumentHash", "StructureType") + .IsUnique(); + + b.ToTable("Rag_CachedStructuredKnowledge", (string)null); + }); + + modelBuilder.Entity("SimpleModule.RateLimiting.Contracts.RateLimitRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EndpointPattern") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PermitLimit") + .HasColumnType("INTEGER"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PolicyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("QueueLimit") + .HasColumnType("INTEGER"); + + b.Property("ReplenishmentPeriodSeconds") + .HasColumnType("INTEGER"); + + b.Property("SegmentsPerWindow") + .HasColumnType("INTEGER"); + + b.Property("Target") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TokenLimit") + .HasColumnType("INTEGER"); + + b.Property("TokensPerPeriod") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WindowSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PolicyName") + .IsUnique(); + + b.ToTable("RateLimiting_Rules", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Settings.Entities.PublicMenuItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CssClass") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsHomePage") + .HasColumnType("INTEGER"); + + b.Property("IsVisible") + .HasColumnType("INTEGER"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OpenInNewTab") + .HasColumnType("INTEGER"); + + b.Property("PageRoute") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId", "SortOrder"); + + b.ToTable("Settings_PublicMenuItems", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Settings.Entities.SettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Scope", "UserId") + .IsUnique(); + + b.ToTable("Settings_Settings", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Tenants.Entities.TenantEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdminEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConnectionString") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EditionName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("ValidUpTo") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Tenants_Tenants", (string)null); + + b.HasData( + new + { + Id = 1, + AdminEmail = "admin@acme.com", + ConcurrencyStamp = "seed-acme", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EditionName = "Enterprise", + Name = "Acme Corporation", + Slug = "acme", + Status = 0, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }, + new + { + Id = 2, + AdminEmail = "admin@contoso.com", + ConcurrencyStamp = "seed-contoso", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + EditionName = "Standard", + Name = "Contoso Ltd", + Slug = "contoso", + Status = 0, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }, + new + { + Id = 3, + AdminEmail = "admin@suspended.com", + ConcurrencyStamp = "seed-suspended", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Suspended Corp", + Slug = "suspended-corp", + Status = 1, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }); + }); + + modelBuilder.Entity("SimpleModule.Tenants.Entities.TenantHostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("HostName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("TenantId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("HostName") + .IsUnique(); + + b.HasIndex("TenantId"); + + b.ToTable("Tenants_TenantHosts", (string)null); + + b.HasData( + new + { + Id = 1, + ConcurrencyStamp = "seed-host-1", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HostName = "acme.localhost", + IsActive = true, + TenantId = 1, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }, + new + { + Id = 2, + ConcurrencyStamp = "seed-host-2", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HostName = "acme.local", + IsActive = true, + TenantId = 1, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }, + new + { + Id = 3, + ConcurrencyStamp = "seed-host-3", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HostName = "contoso.localhost", + IsActive = true, + TenantId = 2, + UpdatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }); + }); + + modelBuilder.Entity("SimpleModule.Users.Contracts.ApplicationRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Users_AspNetRoles", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Users.Contracts.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeactivatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastLoginAt") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users_AspNetUsers", (string)null); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackgroundJobs_CronTickers", (string)null); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId"); + + b.ToTable("BackgroundJobs_CronTickerOccurrences", (string)null); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("BackgroundJobs_TimeTickers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("SimpleModule.Users.Contracts.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SimpleModule.Users.Contracts.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SimpleModule.Users.Contracts.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("SimpleModule.Users.Contracts.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SimpleModule.Users.Contracts.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SimpleModule.Users.Contracts.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("SimpleModule.Orders.Contracts.OrderItem", b => + { + b.HasOne("SimpleModule.Orders.Contracts.Order", null) + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SimpleModule.PageBuilder.Contracts.PageTag", b => + { + b.HasOne("SimpleModule.PageBuilder.Contracts.Page", null) + .WithMany("Tags") + .HasForeignKey("PageId"); + }); + + modelBuilder.Entity("SimpleModule.Settings.Entities.PublicMenuItemEntity", b => + { + b.HasOne("SimpleModule.Settings.Entities.PublicMenuItemEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("SimpleModule.Tenants.Entities.TenantHostEntity", b => + { + b.HasOne("SimpleModule.Tenants.Entities.TenantEntity", "Tenant") + .WithMany("Hosts") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("SimpleModule.Orders.Contracts.Order", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("SimpleModule.PageBuilder.Contracts.Page", b => + { + b.Navigation("Tags"); + }); + + modelBuilder.Entity("SimpleModule.Settings.Entities.PublicMenuItemEntity", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("SimpleModule.Tenants.Entities.TenantEntity", b => + { + b.Navigation("Hosts"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.cs b/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.cs new file mode 100644 index 00000000..d678fa2e --- /dev/null +++ b/template/SimpleModule.Host/Migrations/20260403145434_AddRateLimitingModule.cs @@ -0,0 +1,491 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SimpleModule.Host.Migrations +{ + /// + public partial class AddRateLimitingModule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder + .AlterColumn( + name: "Id", + table: "Tenants_Tenants", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Tenants_TenantHosts", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Products_Products", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Templates", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Tags", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Pages", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Orders_Orders", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "FileStorage_StoredFiles", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "AuditLogs_AuditEntries", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .OldAnnotation("Sqlite:Autoincrement", true); + + migrationBuilder.CreateTable( + name: "BackgroundJobs_CronTickers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Expression = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_BackgroundJobs_CronTickers", x => x.Id); + } + ); + + migrationBuilder.CreateTable( + name: "BackgroundJobs_JobProgress", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + JobTypeName = table.Column( + type: "TEXT", + maxLength: 500, + nullable: false + ), + ModuleName = table.Column( + type: "TEXT", + maxLength: 100, + nullable: false + ), + ProgressPercentage = table.Column(type: "INTEGER", nullable: false), + ProgressMessage = table.Column( + type: "TEXT", + maxLength: 1000, + nullable: true + ), + Data = table.Column(type: "TEXT", nullable: true), + Logs = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_BackgroundJobs_JobProgress", x => x.Id); + } + ); + + migrationBuilder.CreateTable( + name: "BackgroundJobs_TimeTickers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: true), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + ParentId = table.Column(type: "TEXT", nullable: true), + RunCondition = table.Column(type: "INTEGER", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_BackgroundJobs_TimeTickers", x => x.Id); + table.ForeignKey( + name: "FK_BackgroundJobs_TimeTickers_BackgroundJobs_TimeTickers_ParentId", + column: x => x.ParentId, + principalTable: "BackgroundJobs_TimeTickers", + principalColumn: "Id" + ); + } + ); + + migrationBuilder.CreateTable( + name: "Email_EmailMessages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + To = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Cc = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Bcc = table.Column(type: "TEXT", maxLength: 500, nullable: true), + ReplyTo = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Subject = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Body = table.Column(type: "TEXT", nullable: false), + IsHtml = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ErrorMessage = table.Column( + type: "TEXT", + maxLength: 2000, + nullable: true + ), + RetryCount = table.Column(type: "INTEGER", nullable: false), + TemplateSlug = table.Column( + type: "TEXT", + maxLength: 200, + nullable: true + ), + Provider = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + SentAt = table.Column(type: "TEXT", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_Email_EmailMessages", x => x.Id); + } + ); + + migrationBuilder.CreateTable( + name: "Email_EmailTemplates", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Slug = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Subject = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Body = table.Column(type: "TEXT", nullable: false), + IsHtml = table.Column(type: "INTEGER", nullable: false), + DefaultReplyTo = table.Column( + type: "TEXT", + maxLength: 500, + nullable: true + ), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_Email_EmailTemplates", x => x.Id); + } + ); + + migrationBuilder.CreateTable( + name: "RateLimiting_Rules", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + PolicyName = table.Column( + type: "TEXT", + maxLength: 100, + nullable: false + ), + PolicyType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Target = table.Column(type: "TEXT", maxLength: 50, nullable: false), + PermitLimit = table.Column(type: "INTEGER", nullable: false), + WindowSeconds = table.Column(type: "INTEGER", nullable: false), + SegmentsPerWindow = table.Column(type: "INTEGER", nullable: false), + TokenLimit = table.Column(type: "INTEGER", nullable: false), + TokensPerPeriod = table.Column(type: "INTEGER", nullable: false), + ReplenishmentPeriodSeconds = table.Column( + type: "INTEGER", + nullable: false + ), + QueueLimit = table.Column(type: "INTEGER", nullable: false), + EndpointPattern = table.Column( + type: "TEXT", + maxLength: 500, + nullable: true + ), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_RateLimiting_Rules", x => x.Id); + } + ); + + migrationBuilder.CreateTable( + name: "BackgroundJobs_CronTickerOccurrences", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: false), + CronTickerId = table.Column(type: "TEXT", nullable: false), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_BackgroundJobs_CronTickerOccurrences", x => x.Id); + table.ForeignKey( + name: "FK_BackgroundJobs_CronTickerOccurrences_BackgroundJobs_CronTickers_CronTickerId", + column: x => x.CronTickerId, + principalTable: "BackgroundJobs_CronTickers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_BackgroundJobs_CronTickerOccurrences_CronTickerId", + table: "BackgroundJobs_CronTickerOccurrences", + column: "CronTickerId" + ); + + migrationBuilder.CreateIndex( + name: "IX_BackgroundJobs_JobProgress_ModuleName", + table: "BackgroundJobs_JobProgress", + column: "ModuleName" + ); + + migrationBuilder.CreateIndex( + name: "IX_BackgroundJobs_TimeTickers_ParentId", + table: "BackgroundJobs_TimeTickers", + column: "ParentId" + ); + + migrationBuilder.CreateIndex( + name: "IX_Email_EmailMessages_CreatedAt", + table: "Email_EmailMessages", + column: "CreatedAt" + ); + + migrationBuilder.CreateIndex( + name: "IX_Email_EmailMessages_Status", + table: "Email_EmailMessages", + column: "Status" + ); + + migrationBuilder.CreateIndex( + name: "IX_Email_EmailTemplates_Slug", + table: "Email_EmailTemplates", + column: "Slug", + unique: true + ); + + migrationBuilder.CreateIndex( + name: "IX_RateLimiting_Rules_PolicyName", + table: "RateLimiting_Rules", + column: "PolicyName", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "BackgroundJobs_CronTickerOccurrences"); + + migrationBuilder.DropTable(name: "BackgroundJobs_JobProgress"); + + migrationBuilder.DropTable(name: "BackgroundJobs_TimeTickers"); + + migrationBuilder.DropTable(name: "Email_EmailMessages"); + + migrationBuilder.DropTable(name: "Email_EmailTemplates"); + + migrationBuilder.DropTable(name: "RateLimiting_Rules"); + + migrationBuilder.DropTable(name: "BackgroundJobs_CronTickers"); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Tenants_Tenants", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Tenants_TenantHosts", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Products_Products", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Templates", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Tags", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "PageBuilder_Pages", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "Orders_Orders", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "FileStorage_StoredFiles", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder + .AlterColumn( + name: "Id", + table: "AuditLogs_AuditEntries", + type: "INTEGER", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER" + ) + .Annotation("Sqlite:Autoincrement", true); + } + } +} diff --git a/template/SimpleModule.Host/Migrations/HostDbContextModelSnapshot.cs b/template/SimpleModule.Host/Migrations/HostDbContextModelSnapshot.cs index 9ac274a6..021e9121 100644 --- a/template/SimpleModule.Host/Migrations/HostDbContextModelSnapshot.cs +++ b/template/SimpleModule.Host/Migrations/HostDbContextModelSnapshot.cs @@ -489,6 +489,160 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AuditLogs_AuditEntries", (string)null); }); + modelBuilder.Entity("SimpleModule.BackgroundJobs.Entities.JobProgress", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("JobTypeName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Logs") + .HasColumnType("TEXT"); + + b.Property("ModuleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProgressMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ModuleName"); + + b.ToTable("BackgroundJobs_JobProgress", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Email.Contracts.EmailMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bcc") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Cc") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsHtml") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ReplyTo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SentAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TemplateSlug") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("To") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("Email_EmailMessages", (string)null); + }); + + modelBuilder.Entity("SimpleModule.Email.Contracts.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DefaultReplyTo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsHtml") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Email_EmailTemplates", (string)null); + }); + modelBuilder.Entity("SimpleModule.FeatureFlags.Entities.FeatureFlagEntity", b => { b.Property("Id") @@ -915,6 +1069,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Rag_CachedStructuredKnowledge", (string)null); }); + modelBuilder.Entity("SimpleModule.RateLimiting.Contracts.RateLimitRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EndpointPattern") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PermitLimit") + .HasColumnType("INTEGER"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PolicyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("QueueLimit") + .HasColumnType("INTEGER"); + + b.Property("ReplenishmentPeriodSeconds") + .HasColumnType("INTEGER"); + + b.Property("SegmentsPerWindow") + .HasColumnType("INTEGER"); + + b.Property("Target") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TokenLimit") + .HasColumnType("INTEGER"); + + b.Property("TokensPerPeriod") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WindowSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PolicyName") + .IsUnique(); + + b.ToTable("RateLimiting_Rules", (string)null); + }); + modelBuilder.Entity("SimpleModule.Settings.Entities.PublicMenuItemEntity", b => { b.Property("Id") @@ -1276,6 +1493,166 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users_AspNetUsers", (string)null); }); + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackgroundJobs_CronTickers", (string)null); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId"); + + b.ToTable("BackgroundJobs_CronTickerOccurrences", (string)null); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("BackgroundJobs_TimeTickers", (string)null); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("SimpleModule.Users.Contracts.ApplicationRole", null) @@ -1388,6 +1765,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tenant"); }); + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => { b.Navigation("Authorizations"); @@ -1419,6 +1816,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Hosts"); }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); #pragma warning restore 612, 618 } } diff --git a/template/SimpleModule.Host/SimpleModule.Host.csproj b/template/SimpleModule.Host/SimpleModule.Host.csproj index 03813df5..c20744cf 100644 --- a/template/SimpleModule.Host/SimpleModule.Host.csproj +++ b/template/SimpleModule.Host/SimpleModule.Host.csproj @@ -37,6 +37,7 @@ + diff --git a/template/SimpleModule.Host/wwwroot/css/app.css b/template/SimpleModule.Host/wwwroot/css/app.css index 52504882..90263311 100644 --- a/template/SimpleModule.Host/wwwroot/css/app.css +++ b/template/SimpleModule.Host/wwwroot/css/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-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-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-green-600:oklch(62.7% .194 149.214);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-3xl:48rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--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;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#059669;--color-primary-hover:#047857;--color-primary-light:#34d399;--color-primary-subtle:#05966914;--color-primary-ring:#05966940;--color-accent:#0f766e;--color-success:#059669;--color-success-light:#34d399;--color-success-bg:#0596691a;--color-success-text:#047857;--color-danger:#e11d48;--color-danger-hover:#be123c;--color-danger-bg:#e11d4814;--color-danger-text:#9f1239;--color-warning:#d97706;--color-warning-bg:#d9770614;--color-warning-border:#d9770633;--color-warning-text:#92400e;--color-info:#0ea5e9;--color-info-bg:#0ea5e914;--color-surface:#fff;--color-surface-raised:#f8fafc;--color-surface-sunken:#f1f5f9;--color-surface-overlay:#fffc;--color-text:#0f172a;--color-text-secondary:#475569;--color-text-muted:#94a3b8;--color-text-inverse:#fff;--color-border:#e2e8f0;--color-border-strong:#cbd5e1;--color-code-bg:#1e293b;--color-code-text:#e2e8f0;--color-dark:#0f172a;--color-muted:#94a3b8;--color-ring:#05966966;--shadow-primary:0 4px 14px #05966959;--shadow-primary-hover:0 6px 20px #05966980;--shadow-danger:0 4px 14px #e11d4840;--shadow-danger-hover:0 6px 20px #e11d4866}}@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}html{scroll-behavior:smooth;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background-color:var(--color-surface-sunken);color:var(--color-text);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));--tw-duration:.3s;font-family:DM Sans,system-ui,sans-serif;transition-duration:.3s}h1,h2,h3,h4,h5,h6{--tw-font-weight:var(--font-weight-bold);font-family:Sora,DM Sans,system-ui,sans-serif;font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--color-text);margin-top:0}h1{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}@media (min-width:40rem){h1{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}h2{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}@media (min-width:40rem){h2{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}h3{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media (min-width:40rem){h3{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}label{margin-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-text-secondary);display:block}input[type=email],input[type=password],input[type=text],input[type=tel]{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text)}:is(input[type=email],input[type=password],input[type=text],input[type=tel])::placeholder{color:var(--color-text-muted)}input[type=email],input[type=password],input[type=text],input[type=tel]{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){input[type=email],input[type=password],input[type=text],input[type=tel]{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}}input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,input[type=tel]:focus{border-color:var(--color-primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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);--tw-ring-color:var(--color-primary-ring);--tw-outline-style:none;outline-style:none}a{color:var(--color-primary);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));--tw-duration:.15s;transition-duration:.15s}a:hover{color:var(--color-primary-hover)}hr{margin-block:calc(var(--spacing) * 6);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border)}::selection{background:#05966933}table{width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}table th{padding-bottom:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-text-muted);text-transform:uppercase}table td{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border);padding-block:calc(var(--spacing) * 2.5);color:var(--color-text-secondary)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background-color:var(--color-border-strong);border-radius:3.40282e38px}::-webkit-scrollbar-thumb:hover{background-color:var(--color-text-muted)}}@layer components{.glass-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface-overlay);--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);-webkit-backdrop-filter:blur(20px)saturate(180%)}.dark .glass-card{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);box-shadow:0 0 0 1px #94a3b80d,0 25px 50px -12px #00000080}.btn-primary,.btn-secondary,.btn-ghost,.btn-danger,.btn-outline{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);--tw-border-style:none;padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:var(--ease-out);transition-duration:.2s;transition-timing-function:var(--ease-out);border-style:none;text-decoration-line:none;display:inline-flex}.btn-primary:active,.btn-secondary:active,.btn-ghost:active,.btn-danger:active,.btn-outline:active{transform:scale(.97)}.btn-primary{color:var(--color-white);background:linear-gradient(135deg, var(--color-primary), var(--color-accent));box-shadow:0 4px 14px #05966959}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 20px #05966980}.btn-secondary{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);color:var(--color-text)}.btn-secondary:hover{border-color:var(--color-border-strong);background-color:var(--color-surface-raised)}.btn-ghost{color:var(--color-text-secondary);background-color:#0000}.btn-ghost:hover{background-color:var(--color-primary-subtle);color:var(--color-primary)}.btn-danger{background-color:var(--color-danger);color:var(--color-white);box-shadow:0 4px 14px #e11d4840}.btn-danger:hover{background-color:var(--color-danger-hover);transform:translateY(-1px);box-shadow:0 6px 20px #e11d4866}.btn-outline{border-style:var(--tw-border-style);border-width:2px;border-color:#0596694d}@supports (color:color-mix(in lab, red, red)){.btn-outline{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.btn-outline{color:var(--color-primary);background-color:#0000}.btn-outline:hover{border-color:var(--color-primary);background-color:var(--color-primary-subtle)}.btn-sm{border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.btn-lg{padding-inline:calc(var(--spacing) * 8);padding-block:calc(var(--spacing) * 3.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.form-group{margin-bottom:calc(var(--spacing) * 5)}.badge-success,.badge-danger,.badge-warning,.badge-info{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-success{background-color:var(--color-success-bg);color:var(--color-success-text)}.badge-danger{background-color:var(--color-danger-bg);color:var(--color-danger-text)}.badge-warning{background-color:var(--color-warning-bg);color:var(--color-warning-text)}.badge-info{background-color:var(--color-info-bg);color:var(--color-primary)}.alert-success,.alert-danger,.alert-warning,.alert-info{border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px}.alert-success{border-color:#05966933}@supports (color:color-mix(in lab, red, red)){.alert-success{border-color:color-mix(in oklab, var(--color-success) 20%, transparent)}}.alert-success{background-color:var(--color-success-bg);color:var(--color-success-text)}.alert-danger{border-color:#e11d4833}@supports (color:color-mix(in lab, red, red)){.alert-danger{border-color:color-mix(in oklab, var(--color-danger) 20%, transparent)}}.alert-danger{background-color:var(--color-danger-bg);color:var(--color-danger-text)}.alert-warning{border-color:var(--color-warning-border);background-color:var(--color-warning-bg);color:var(--color-warning-text)}.alert-info{border-color:#0ea5e933}@supports (color:color-mix(in lab, red, red)){.alert-info{border-color:color-mix(in oklab, var(--color-info) 20%, transparent)}}.alert-info{background-color:var(--color-info-bg);color:var(--color-primary)}.code-block{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-code-bg);padding:calc(var(--spacing) * 4);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-code-text);font-family:JetBrains Mono,Fira Code,monospace;overflow:auto}.panel{margin-bottom:calc(var(--spacing) * 8)}.panel-title{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);font-family:Sora,system-ui,sans-serif;display:flex}.panel-title:before{content:"";height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 1);background:linear-gradient(180deg, var(--color-primary), var(--color-accent));border-radius:3.40282e38px}.card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding:calc(var(--spacing) * 4);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){.card{border-radius:var(--radius-2xl);padding:calc(var(--spacing) * 5)}}.card:hover{border-color:var(--color-border-strong)}.table-responsive{margin-inline:calc(var(--spacing) * -4);overflow-x:auto}@media (min-width:40rem){.table-responsive{margin-inline:calc(var(--spacing) * 0)}}.table-responsive>table{min-width:600px}@media (min-width:40rem){.table-responsive>table{min-width:calc(var(--spacing) * 0)}}.scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-hide::-webkit-scrollbar{display:none}.manage-tab{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:nowrap;color:var(--color-text-muted);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));--tw-duration:.15s;border-color:#0000;border-bottom-width:2px;flex-shrink:0;text-decoration-line:none;transition-duration:.15s;display:inline-flex}.manage-tab:hover{color:var(--color-text)}.manage-tab-active{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-primary);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);white-space:nowrap;color:var(--color-primary);flex-shrink:0;text-decoration-line:none;display:inline-flex}.public-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--color-surface);opacity:0;pointer-events:none;transition:opacity .25s;position:fixed}.public-overlay.open{opacity:1;pointer-events:auto}.nav-link-active,.nav-link-inactive{border-radius:var(--radius-xl);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;text-decoration-line:none;transition-duration:.15s;display:block}.nav-link-active{background-color:var(--color-primary-subtle);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-primary)}.nav-link-inactive{color:var(--color-text-secondary)}.nav-link-inactive:hover{background-color:var(--color-surface-raised);color:var(--color-text)}.bg-mesh{z-index:-1;background:var(--color-surface-sunken);position:fixed;inset:0;overflow:hidden}.bg-mesh:before,.bg-mesh:after{content:"";filter:blur(100px);opacity:.15;border-radius:50%;animation:20s ease-in-out infinite mesh-float;position:absolute}.bg-mesh:before{background:var(--color-primary);width:600px;height:600px;top:-10%;right:-10%}.bg-mesh:after{background:var(--color-accent);width:500px;height:500px;animation-direction:reverse;animation-delay:-10s;bottom:-10%;left:-10%}.dark .bg-mesh:before,.dark .bg-mesh:after{opacity:.08}@keyframes mesh-float{0%,to{transform:translate(0)scale(1)}33%{transform:translate(40px,-30px)scale(1.05)}66%{transform:translate(-20px,20px)scale(.95)}}.spinner{margin-left:calc(var(--spacing) * 2);height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);animation:var(--animate-spin);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-border);border-top-color:var(--color-primary);vertical-align:middle;border-radius:3.40282e38px;display:inline-block}.user-dropdown-wrap{position:relative}.user-dropdown-trigger{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);--tw-border-style:none;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;background-color:#0000;border-style:none;transition-duration:.2s;display:flex}.user-dropdown-trigger:hover,.user-dropdown-trigger[aria-expanded=true]{background-color:var(--color-surface-raised)}.user-dropdown{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:240px;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);opacity:0;pointer-events:none;z-index:100;transition:opacity .18s,transform .18s;position:absolute;top:calc(100% + 6px);right:0;transform:translateY(-4px)scale(.97)}.user-dropdown.open{opacity:1;pointer-events:auto;transform:translateY(0)scale(1)}.app-sidebar .user-dropdown{inset:auto auto calc(100% + 6px) 0;transform:translateY(4px)scale(.97)}.app-sidebar .user-dropdown.open{transform:translateY(0)scale(1)}.dark .user-dropdown{box-shadow:0 0 0 1px #94a3b80f,0 20px 40px -8px #0009}.user-dropdown-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.user-dropdown-body{padding-block:calc(var(--spacing) * 1.5)}.user-dropdown-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2.5);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text-secondary);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));--tw-duration:.15s;text-decoration-line:none;transition-duration:.15s;display:flex}.user-dropdown-item:hover{background-color:var(--color-surface-raised);color:var(--color-text)}.user-dropdown-divider{margin-block:calc(var(--spacing) * 1.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border)}.user-dropdown-item.danger{color:var(--color-danger)}.user-dropdown-item.danger:hover{background-color:var(--color-danger-bg);color:var(--color-danger)}.dash-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding:calc(var(--spacing) * 4);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){.dash-card{border-radius:var(--radius-2xl);padding:calc(var(--spacing) * 5)}}.dash-card:hover{border-color:var(--color-border-strong);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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)}.dash-stat{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}@media (min-width:40rem){.dash-stat{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.dash-stat{font-family:Sora,system-ui,sans-serif}.dash-label{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-text-muted);text-transform:uppercase}.validation-message{margin-top:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-danger)}.app-layout{flex-direction:column;min-height:100vh;display:flex}.app-sidebar{border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--color-border);background-color:var(--color-surface);z-index:40;flex-shrink:0;width:16rem;transition:width .2s,transform .25s;position:fixed;top:0;bottom:0;left:0;transform:translate(-100%)}.app-sidebar.app-sidebar-open{transform:translate(0)}.app-sidebar.app-sidebar-collapsed{width:4rem}.app-sidebar.app-sidebar-collapsed .sidebar-label{display:none}.app-sidebar.app-sidebar-collapsed .sidebar-icon{margin:0 auto}.app-content{flex:1;margin-left:0;transition:margin-left .2s}.app-sidebar-backdrop{inset:calc(var(--spacing) * 0);z-index:30;background-color:#00000080;position:fixed}@supports (color:color-mix(in lab, red, red)){.app-sidebar-backdrop{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.app-sidebar-backdrop{opacity:0;pointer-events:none;transition:opacity .25s}.app-sidebar-backdrop.visible{opacity:1;pointer-events:auto}.app-sidebar-toggle{bottom:calc(var(--spacing) * 4);z-index:50;height:calc(var(--spacing) * 7);width:calc(var(--spacing) * 7);cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);color:var(--color-text-muted);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;border-radius:3.40282e38px;justify-content:center;align-items:center;transition-duration:.2s;position:fixed}@media (hover:hover){.app-sidebar-toggle:hover{background-color:var(--color-surface-raised);color:var(--color-text)}}.app-sidebar-toggle{display:none;left:1rem}.app-mobile-header{top:calc(var(--spacing) * 0);z-index:20;align-items:center;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex;position:sticky}@media (min-width:768px){.app-sidebar{transform:translate(0)}.app-content{margin-left:16rem}.app-sidebar-collapsed~.app-content{margin-left:4rem}.app-sidebar-backdrop{display:none}.app-sidebar-toggle{display:flex;left:15rem}.app-sidebar-collapsed~.app-sidebar-toggle{left:3rem}.app-mobile-header,.public-overlay{display:none}}.markdown-content h1{margin-top:calc(var(--spacing) * 6);margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h2{margin-top:calc(var(--spacing) * 5);margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h3{margin-top:calc(var(--spacing) * 4);margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h4{margin-top:calc(var(--spacing) * 3);margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text)}.markdown-content p{margin-bottom:calc(var(--spacing) * 3);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--color-text-secondary)}.markdown-content ul{margin-bottom:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 6);color:var(--color-text-secondary);list-style-type:disc}.markdown-content ol{margin-bottom:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 6);color:var(--color-text-secondary);list-style-type:decimal}.markdown-content li{margin-bottom:calc(var(--spacing) * 1)}.markdown-content code{background-color:var(--color-surface-sunken);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-text);border-radius:.25rem}.markdown-content pre{margin-bottom:calc(var(--spacing) * 3);border-radius:var(--radius-xl);background-color:var(--color-code-bg);padding:calc(var(--spacing) * 4);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-code-text);overflow:auto}.markdown-content pre code{padding:calc(var(--spacing) * 0);color:var(--color-code-text);background-color:#0000}.markdown-content a{color:var(--color-primary)}@media (hover:hover){.markdown-content a:hover{text-decoration-line:underline}}.markdown-content blockquote{margin-block:calc(var(--spacing) * 3);border-left-style:var(--tw-border-style);border-color:#0596694d;border-left-width:4px}@supports (color:color-mix(in lab, red, red)){.markdown-content blockquote{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.markdown-content blockquote{padding-left:calc(var(--spacing) * 4);color:var(--color-text-muted);font-style:italic}.markdown-content img{margin-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);max-width:100%}.markdown-content table{margin-block:calc(var(--spacing) * 3);border-collapse:collapse;width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.markdown-content th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding:calc(var(--spacing) * 2);text-align:left;--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text)}.markdown-content td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding:calc(var(--spacing) * 2);color:var(--color-text-secondary)}.markdown-content hr{margin-block:calc(var(--spacing) * 6);border-color:var(--color-border)}.markdown-content>:first-child{margin-top:calc(var(--spacing) * 0)}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-1{top:calc(var(--spacing) * 1)}.top-1\/2{top:50%}.top-4{top:calc(var(--spacing) * 4)}.top-20{top:calc(var(--spacing) * 20)}.top-\[50\%\]{top:50%}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-1{right:calc(var(--spacing) * 1)}.right-4{right:calc(var(--spacing) * 4)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.left-1{left:calc(var(--spacing) * 1)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing) * 2)}.left-\[50\%\]{left:50%}.left-full{left:100%}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[9999\]{z-index:9999}.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}}.m-0{margin:calc(var(--spacing) * 0)}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.mx-auto{margin-inline:auto}.my-0\.5{margin-block:calc(var(--spacing) * .5)}.my-1{margin-block:calc(var(--spacing) * 1)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-6{margin-block:calc(var(--spacing) * 6)}.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-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.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-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.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-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.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-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-20{height:calc(var(--spacing) * 20)}.h-36{height:calc(var(--spacing) * 36)}.h-\[18px\]{height:18px}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-30{max-height:calc(var(--spacing) * 30)}.max-h-50{max-height:calc(var(--spacing) * 50)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[300px\]{max-height:300px}.max-h-\[500px\]{max-height:500px}.max-h-screen{max-height:100vh}.min-h-\[80px\]{min-height:80px}.min-h-\[180px\]{min-height:180px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[400px\]{min-height:400px}.min-h-\[calc\(100vh-12rem\)\]{min-height:calc(100vh - 12rem)}.min-h-\[calc\(100vh-16rem\)\]{min-height:calc(100vh - 16rem)}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing) * 0)}.w-1{width:calc(var(--spacing) * 1)}.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-3\/4{width:75%}.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-11{width:calc(var(--spacing) * 11)}.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-36{width:calc(var(--spacing) * 36)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-\[18px\]{width:18px}.w-\[70px\]{width:70px}.w-\[160px\]{width:160px}.w-auto{width:auto}.w-full{width:100%}.w-px{width:1px}.max-w-3xl{max-width:var(--container-3xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[80\%\]{max-width:80%}.max-w-\[140px\]{max-width:140px}.max-w-\[200px\]{max-width:200px}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[160px\]{min-width:160px}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.resize-y{resize:vertical}.list-decimal{list-style-type:decimal}.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))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing) * 0)}.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-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0\.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-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-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-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)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-\[2px\]{border-radius:2px}.rounded-\[inherit\]{border-radius:inherit}.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)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-\[--color-border\]{border-color:--color-border}.border-border{border-color:var(--color-border)}.border-border\/50{border-color:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.border-border\/50{border-color:color-mix(in oklab, var(--color-border) 50%, transparent)}}.border-danger{border-color:var(--color-danger)}.border-danger\/20{border-color:#e11d4833}@supports (color:color-mix(in lab, red, red)){.border-danger\/20{border-color:color-mix(in oklab, var(--color-danger) 20%, transparent)}}.border-danger\/30{border-color:#e11d484d}@supports (color:color-mix(in lab, red, red)){.border-danger\/30{border-color:color-mix(in oklab, var(--color-danger) 30%, transparent)}}.border-info\/20{border-color:#0ea5e933}@supports (color:color-mix(in lab, red, red)){.border-info\/20{border-color:color-mix(in oklab, var(--color-info) 20%, transparent)}}.border-primary{border-color:var(--color-primary)}.border-primary\/30{border-color:#0596694d}@supports (color:color-mix(in lab, red, red)){.border-primary\/30{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-success\/20{border-color:#05966933}@supports (color:color-mix(in lab, red, red)){.border-success\/20{border-color:color-mix(in oklab, var(--color-success) 20%, transparent)}}.border-transparent{border-color:#0000}.border-warning-border{border-color:var(--color-warning-border)}.border-t-primary{border-top-color:var(--color-primary)}.border-t-transparent{border-top-color:#0000}.border-l-primary{border-left-color:var(--color-primary)}.border-l-transparent{border-left-color:#0000}.bg-\[--color-bg\]{background-color:--color-bg}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-border{background-color:var(--color-border)}.bg-border-strong{background-color:var(--color-border-strong)}.bg-danger{background-color:var(--color-danger)}.bg-danger-bg{background-color:var(--color-danger-bg)}.bg-danger\/10{background-color:#e11d481a}@supports (color:color-mix(in lab, red, red)){.bg-danger\/10{background-color:color-mix(in oklab, var(--color-danger) 10%, transparent)}}.bg-dark{background-color:var(--color-dark)}.bg-dark\/60{background-color:#0f172a99}@supports (color:color-mix(in lab, red, red)){.bg-dark\/60{background-color:color-mix(in oklab, var(--color-dark) 60%, transparent)}}.bg-info-bg{background-color:var(--color-info-bg)}.bg-muted{background-color:var(--color-muted)}.bg-primary{background-color:var(--color-primary)}.bg-primary-subtle{background-color:var(--color-primary-subtle)}.bg-primary\/5{background-color:#0596690d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-success-bg{background-color:var(--color-success-bg)}.bg-success\/10{background-color:#0596691a}@supports (color:color-mix(in lab, red, red)){.bg-success\/10{background-color:color-mix(in oklab, var(--color-success) 10%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-overlay{background-color:var(--color-surface-overlay)}.bg-surface-raised{background-color:var(--color-surface-raised)}.bg-surface-sunken{background-color:var(--color-surface-sunken)}.bg-transparent{background-color:#0000}.bg-warning-bg{background-color:var(--color-warning-bg)}.bg-white{background-color:var(--color-white)}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary{--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-accent{--tw-gradient-to:var(--color-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.fill-current{fill:currentColor}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.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-\[1px\]{padding:1px}.px-0{padding-inline:calc(var(--spacing) * 0)}.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-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.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-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.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-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.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-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.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)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.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-accent{color:var(--color-accent)}.text-border{color:var(--color-border)}.text-current{color:currentColor}.text-danger{color:var(--color-danger)}.text-danger-text{color:var(--color-danger-text)}.text-danger-text\/60{color:#9f123999}@supports (color:color-mix(in lab, red, red)){.text-danger-text\/60{color:color-mix(in oklab, var(--color-danger-text) 60%, transparent)}}.text-green-600{color:var(--color-green-600)}.text-info{color:var(--color-info)}.text-muted{color:var(--color-muted)}.text-primary{color:var(--color-primary)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-success{color:var(--color-success)}.text-success-text{color:var(--color-success-text)}.text-text{color:var(--color-text)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-muted{color:var(--color-text-muted)}.text-text-muted\/50{color:#94a3b880}@supports (color:color-mix(in lab, red, red)){.text-text-muted\/50{color:color-mix(in oklab, var(--color-text-muted) 50%, transparent)}}.text-text-secondary{color:var(--color-text-secondary)}.text-warning{color:var(--color-warning)}.text-warning-text{color:var(--color-warning-text)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.opacity-100{opacity:1}.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-\(--shadow-danger\){--tw-shadow:var(--shadow-danger);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\(--shadow-primary\){--tw-shadow:var(--shadow-primary);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-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px 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)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + 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)}.ring-offset-surface{--tw-ring-offset-color:var(--color-surface)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-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-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/sub\:block:is(:where(.group\/sub):hover *){display:block}}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}.before\:h-5:before{content:var(--tw-content);height:calc(var(--spacing) * 5)}.before\:w-1:before{content:var(--tw-content);width:calc(var(--spacing) * 1)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-gradient-to-b:before{content:var(--tw-content);--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.before\:from-primary:before{content:var(--tw-content);--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.before\:to-accent:before{content:var(--tw-content);--tw-gradient-to:var(--color-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--spacing) * 0)}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-1:after{content:var(--tw-content);width:calc(var(--spacing) * 1)}.after\:-translate-x-1\/2:after{content:var(--tw-content);--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.focus-within\:relative:focus-within{position:relative}.focus-within\:z-20:focus-within{z-index:20}@media (hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:-translate-y-px:hover{--tw-translate-y:-1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-border-strong:hover{border-color:var(--color-border-strong)}.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:border-primary\/50:hover{border-color:#05966980}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.hover\:bg-danger-hover:hover{background-color:var(--color-danger-hover)}.hover\:bg-muted\/50:hover{background-color:#94a3b880}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted) 50%, transparent)}}.hover\:bg-primary:hover{background-color:var(--color-primary)}.hover\:bg-primary-subtle:hover{background-color:var(--color-primary-subtle)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-surface-raised:hover{background-color:var(--color-surface-raised)}.hover\:bg-surface-sunken:hover{background-color:var(--color-surface-sunken)}.hover\:text-danger-text:hover{color:var(--color-danger-text)}.hover\:text-danger\/80:hover{color:#e11d48cc}@supports (color:color-mix(in lab, red, red)){.hover\:text-danger\/80:hover{color:color-mix(in oklab, var(--color-danger) 80%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:text-text:hover{color:var(--color-text)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-\(--shadow-danger-hover\):hover{--tw-shadow:var(--shadow-danger-hover);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-\(--shadow-primary-hover\):hover{--tw-shadow:var(--shadow-primary-hover);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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-danger:focus{border-color:var(--color-danger)}.focus\:border-primary:focus{border-color:var(--color-primary)}.focus\:bg-primary:focus{background-color:var(--color-primary)}.focus\:bg-primary-subtle:focus{background-color:var(--color-primary-subtle)}.focus\:bg-surface-raised:focus{background-color:var(--color-surface-raised)}.focus\:text-primary:focus{color:var(--color-primary)}.focus\:text-text:focus{color:var(--color-text)}.focus\:text-text-inverse:focus{color:var(--color-text-inverse)}.focus\:opacity-100:focus{opacity:1}.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-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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-danger-bg:focus{--tw-ring-color:var(--color-danger-bg)}.focus\:ring-primary-ring:focus{--tw-ring-color:var(--color-primary-ring)}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--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-visible\:ring-4:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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-visible\:ring-primary-ring:focus-visible{--tw-ring-color:var(--color-primary-ring)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-\[0\.97\]:active{scale:.97}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.aria-selected\:bg-primary-subtle[aria-selected=true]{background-color:var(--color-primary-subtle)}.aria-selected\:bg-primary-subtle\/50[aria-selected=true]{background-color:#0596690a}@supports (color:color-mix(in lab, red, red)){.aria-selected\:bg-primary-subtle\/50[aria-selected=true]{background-color:color-mix(in oklab, var(--color-primary-subtle) 50%, transparent)}}.aria-selected\:text-text[aria-selected=true]{color:var(--color-text)}.aria-selected\:text-text-muted[aria-selected=true]{color:var(--color-text-muted)}.aria-selected\:opacity-100[aria-selected=true]{opacity:1}.data-\[active\]\:bg-primary-subtle[data-active]{background-color:var(--color-primary-subtle)}.data-\[active\]\:font-semibold[data-active]{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.data-\[active\]\:text-primary[data-active]{color:var(--color-primary)}.data-\[direction\=vertical\]\:flex-col[data-direction=vertical]{flex-direction:column}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.data-\[selected\=true\]\:bg-primary-subtle[data-selected=true]{background-color:var(--color-primary-subtle)}.data-\[selected\=true\]\:text-primary[data-selected=true]{color:var(--color-primary)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=active\]\:bg-surface[data-state=active]{background-color:var(--color-surface)}.data-\[state\=active\]\:text-text[data-state=active]{color:var(--color-text)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--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)}.data-\[state\=checked\]\:translate-x-5[data-state=checked]{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--color-primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--color-primary)}.data-\[state\=checked\]\:text-text-inverse[data-state=checked]{color:var(--color-text-inverse)}.data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.data-\[state\=on\]\:bg-primary-subtle[data-state=on]{background-color:var(--color-primary-subtle)}.data-\[state\=on\]\:text-primary[data-state=on]{color:var(--color-primary)}.data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=unchecked\]\:bg-border-strong[data-state=unchecked]{background-color:var(--color-border-strong)}.data-\[swipe\=cancel\]\:translate-x-0[data-swipe=cancel]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=end\]\:translate-x-\[var\(--radix-toast-swipe-end-x\)\][data-swipe=end]{--tw-translate-x:var(--radix-toast-swipe-end-x);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=move\]\:translate-x-\[var\(--radix-toast-swipe-move-x\)\][data-swipe=move]{--tw-translate-x:var(--radix-toast-swipe-move-x);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=move\]\:transition-none[data-swipe=move]{transition-property:none}@media (min-width:40rem){.sm\:sticky{position:sticky}.sm\:top-20{top:calc(var(--spacing) * 20)}.sm\:top-auto{top:auto}.sm\:right-0{right:calc(var(--spacing) * 0)}.sm\:bottom-0{bottom:calc(var(--spacing) * 0)}.sm\:mx-0{margin-inline:calc(var(--spacing) * 0)}.sm\:mt-6{margin-top:calc(var(--spacing) * 6)}.sm\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.sm\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.sm\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.sm\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.sm\:h-16{height:calc(var(--spacing) * 16)}.sm\:h-48{height:calc(var(--spacing) * 48)}.sm\:min-h-\[200px\]{min-height:200px}.sm\:min-h-\[220px\]{min-height:220px}.sm\:min-h-\[250px\]{min-height:250px}.sm\:min-h-\[280px\]{min-height:280px}.sm\:w-16{width:calc(var(--spacing) * 16)}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-56{width:calc(var(--spacing) * 56)}.sm\:w-\[160px\]{width:160px}.sm\:w-\[180px\]{width:180px}.sm\:w-auto{width:auto}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:gap-0{gap:calc(var(--spacing) * 0)}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-5{gap:calc(var(--spacing) * 5)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\: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(.sm\: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(.sm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.sm\:overflow-x-visible{overflow-x:visible}.sm\:p-3{padding:calc(var(--spacing) * 3)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-0{padding-inline:calc(var(--spacing) * 0)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:py-6{padding-block:calc(var(--spacing) * 6)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:pt-4{padding-top:calc(var(--spacing) * 4)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}@media (min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:w-56{width:calc(var(--spacing) * 56)}.md\:max-w-\[420px\]{max-width:420px}.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))}.md\:grid-cols-\[2fr_3fr\]{grid-template-columns:2fr 3fr}.md\:flex-row{flex-direction:row}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:gap-6{gap:calc(var(--spacing) * 6)}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:block{display:block}.dark\:hidden{display:none}}.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-text-muted .recharts-cartesian-axis-tick text{fill:var(--color-text-muted)}.\[\&_\.recharts-cartesian-grid_line\[stroke\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke]{stroke:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.\[\&_\.recharts-cartesian-grid_line\[stroke\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke]{stroke:color-mix(in oklab, var(--color-border) 50%, transparent)}}.\[\&_\.recharts-curve\.recharts-tooltip-cursor\]\:stroke-border .recharts-curve.recharts-tooltip-cursor{stroke:var(--color-border)}.\[\&_\.recharts-dot\[stroke\]\]\:stroke-transparent .recharts-dot[stroke]{stroke:#0000}.\[\&_\.recharts-layer\]\:outline-none .recharts-layer{--tw-outline-style:none;outline-style:none}.\[\&_\.recharts-polar-grid_\[stroke\]\]\:stroke-border .recharts-polar-grid [stroke]{stroke:var(--color-border)}.\[\&_\.recharts-radial-bar-background-sector\]\:fill-surface-sunken .recharts-radial-bar-background-sector,.\[\&_\.recharts-rectangle\.recharts-tooltip-cursor\]\:fill-surface-sunken .recharts-rectangle.recharts-tooltip-cursor{fill:var(--color-surface-sunken)}.\[\&_\.recharts-reference-line_\[stroke\]\]\:stroke-border .recharts-reference-line [stroke]{stroke:var(--color-border)}.\[\&_\.recharts-sector\]\:outline-none .recharts-sector{--tw-outline-style:none;outline-style:none}.\[\&_\.recharts-sector\[stroke\]\]\:stroke-transparent .recharts-sector[stroke]{stroke:#0000}.\[\&_\.recharts-surface\]\:outline-none .recharts-surface{--tw-outline-style:none;outline-style:none}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-inline:calc(var(--spacing) * 2)}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-block:calc(var(--spacing) * 1.5)}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&_\[cmdk-group-heading\]\]\:text-text-muted [cmdk-group-heading]{color:var(--color-text-muted)}.\[\&_p\]\:leading-relaxed p{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[aria-selected\]\)\]\:bg-primary-subtle:has([aria-selected]){background-color:var(--color-primary-subtle)}.first\:\[\&\:has\(\[aria-selected\]\)\]\:rounded-l-md:first-child:has([aria-selected]){border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.last\:\[\&\:has\(\[aria-selected\]\)\]\:rounded-r-md:last-child:has([aria-selected]){border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.\[\&\:has\(\[aria-selected\]\.day-outside\)\]\:bg-primary-subtle\/50:has([aria-selected].day-outside){background-color:#0596690a}@supports (color:color-mix(in lab, red, red)){.\[\&\:has\(\[aria-selected\]\.day-outside\)\]\:bg-primary-subtle\/50:has([aria-selected].day-outside){background-color:color-mix(in oklab, var(--color-primary-subtle) 50%, transparent)}}.\[\&\:has\(\[aria-selected\]\.day-range-end\)\]\:rounded-r-md:has([aria-selected].day-range-end){border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing) * 0)}.\[\&\>span\]\:line-clamp-1>span{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[\&\>svg\]\:h-2\.5>svg{height:calc(var(--spacing) * 2.5)}.\[\&\>svg\]\:h-3>svg{height:calc(var(--spacing) * 3)}.\[\&\>svg\]\:h-3\.5>svg{height:calc(var(--spacing) * 3.5)}.\[\&\>svg\]\:w-2\.5>svg{width:calc(var(--spacing) * 2.5)}.\[\&\>svg\]\:w-3>svg{width:calc(var(--spacing) * 3)}.\[\&\>svg\]\:w-3\.5>svg{width:calc(var(--spacing) * 3.5)}.\[\&\>svg\]\:text-text-muted>svg{color:var(--color-text-muted)}.\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:h-px{height:1px}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:w-full{width:100%}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:left-0:after{content:var(--tw-content);left:calc(var(--spacing) * 0)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:h-1:after{content:var(--tw-content);height:calc(var(--spacing) * 1)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:w-full:after{content:var(--tw-content);width:100%}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:translate-x-0:after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:-translate-y-1\/2:after{content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.gradient-text{background:linear-gradient(135deg, var(--color-primary), var(--color-accent));-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text}.gradient-border{position:relative}.gradient-border:before{content:"";border-radius:inherit;background:linear-gradient(135deg, var(--color-primary), var(--color-accent));-webkit-mask-composite:xor;pointer-events:none;-webkit-mask-composite:xor;-webkit-mask-source-type:auto,auto;padding:1px;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-size:auto,auto;mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}}.dark{--color-primary-subtle:#05966926;--color-primary-ring:#05966959;--color-success-bg:#34d3991f;--color-success-text:#6ee7b7;--color-danger-bg:#e11d481f;--color-danger-text:#fda4af;--color-warning-bg:#d977061f;--color-warning-border:#d977064d;--color-warning-text:#fcd34d;--color-info-bg:#0ea5e91f;--color-surface:#0f172a;--color-surface-raised:#1e293b;--color-surface-sunken:#0b1120;--color-surface-overlay:#0f172ad9;--color-text:#f1f5f9;--color-text-secondary:#94a3b8;--color-text-muted:#64748b;--color-text-inverse:#0f172a;--color-border:#1e293b;--color-border-strong:#334155;--color-code-bg:#020617;--color-code-text:#e2e8f0}@media (prefers-reduced-motion:reduce){.app-sidebar,.app-sidebar-backdrop,.app-sidebar-toggle,.public-overlay{transition:none!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-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-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-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-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-green-600:oklch(62.7% .194 149.214);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-3xl:48rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--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;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#059669;--color-primary-hover:#047857;--color-primary-light:#34d399;--color-primary-subtle:#05966914;--color-primary-ring:#05966940;--color-accent:#0f766e;--color-success:#059669;--color-success-light:#34d399;--color-success-bg:#0596691a;--color-success-text:#047857;--color-danger:#e11d48;--color-danger-hover:#be123c;--color-danger-bg:#e11d4814;--color-danger-text:#9f1239;--color-warning:#d97706;--color-warning-bg:#d9770614;--color-warning-border:#d9770633;--color-warning-text:#92400e;--color-info:#0ea5e9;--color-info-bg:#0ea5e914;--color-surface:#fff;--color-surface-raised:#f8fafc;--color-surface-sunken:#f1f5f9;--color-surface-overlay:#fffc;--color-text:#0f172a;--color-text-secondary:#475569;--color-text-muted:#94a3b8;--color-text-inverse:#fff;--color-border:#e2e8f0;--color-border-strong:#cbd5e1;--color-code-bg:#1e293b;--color-code-text:#e2e8f0;--color-dark:#0f172a;--color-muted:#94a3b8;--color-ring:#05966966;--shadow-primary:0 4px 14px #05966959;--shadow-primary-hover:0 6px 20px #05966980;--shadow-danger:0 4px 14px #e11d4840;--shadow-danger-hover:0 6px 20px #e11d4866}}@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}html{scroll-behavior:smooth;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background-color:var(--color-surface-sunken);color:var(--color-text);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));--tw-duration:.3s;font-family:DM Sans,system-ui,sans-serif;transition-duration:.3s}h1,h2,h3,h4,h5,h6{--tw-font-weight:var(--font-weight-bold);font-family:Sora,DM Sans,system-ui,sans-serif;font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--color-text);margin-top:0}h1{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}@media (min-width:40rem){h1{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}h2{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}@media (min-width:40rem){h2{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}h3{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}@media (min-width:40rem){h3{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}label{margin-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-text-secondary);display:block}input[type=email],input[type=password],input[type=text],input[type=tel]{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text)}:is(input[type=email],input[type=password],input[type=text],input[type=tel])::placeholder{color:var(--color-text-muted)}input[type=email],input[type=password],input[type=text],input[type=tel]{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){input[type=email],input[type=password],input[type=text],input[type=tel]{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}}input[type=email]:focus,input[type=password]:focus,input[type=text]:focus,input[type=tel]:focus{border-color:var(--color-primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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);--tw-ring-color:var(--color-primary-ring);--tw-outline-style:none;outline-style:none}a{color:var(--color-primary);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));--tw-duration:.15s;transition-duration:.15s}a:hover{color:var(--color-primary-hover)}hr{margin-block:calc(var(--spacing) * 6);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border)}::selection{background:#05966933}table{width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}table th{padding-bottom:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-text-muted);text-transform:uppercase}table td{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border);padding-block:calc(var(--spacing) * 2.5);color:var(--color-text-secondary)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background-color:var(--color-border-strong);border-radius:3.40282e38px}::-webkit-scrollbar-thumb:hover{background-color:var(--color-text-muted)}}@layer components{.glass-card{border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface-overlay);--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);-webkit-backdrop-filter:blur(20px)saturate(180%)}.dark .glass-card{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);box-shadow:0 0 0 1px #94a3b80d,0 25px 50px -12px #00000080}.btn-primary,.btn-secondary,.btn-ghost,.btn-danger,.btn-outline{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);--tw-border-style:none;padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:var(--ease-out);transition-duration:.2s;transition-timing-function:var(--ease-out);border-style:none;text-decoration-line:none;display:inline-flex}.btn-primary:active,.btn-secondary:active,.btn-ghost:active,.btn-danger:active,.btn-outline:active{transform:scale(.97)}.btn-primary{color:var(--color-white);background:linear-gradient(135deg, var(--color-primary), var(--color-accent));box-shadow:0 4px 14px #05966959}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 20px #05966980}.btn-secondary{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);color:var(--color-text)}.btn-secondary:hover{border-color:var(--color-border-strong);background-color:var(--color-surface-raised)}.btn-ghost{color:var(--color-text-secondary);background-color:#0000}.btn-ghost:hover{background-color:var(--color-primary-subtle);color:var(--color-primary)}.btn-danger{background-color:var(--color-danger);color:var(--color-white);box-shadow:0 4px 14px #e11d4840}.btn-danger:hover{background-color:var(--color-danger-hover);transform:translateY(-1px);box-shadow:0 6px 20px #e11d4866}.btn-outline{border-style:var(--tw-border-style);border-width:2px;border-color:#0596694d}@supports (color:color-mix(in lab, red, red)){.btn-outline{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.btn-outline{color:var(--color-primary);background-color:#0000}.btn-outline:hover{border-color:var(--color-primary);background-color:var(--color-primary-subtle)}.btn-sm{border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.btn-lg{padding-inline:calc(var(--spacing) * 8);padding-block:calc(var(--spacing) * 3.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.form-group{margin-bottom:calc(var(--spacing) * 5)}.badge-success,.badge-danger,.badge-warning,.badge-info{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-success{background-color:var(--color-success-bg);color:var(--color-success-text)}.badge-danger{background-color:var(--color-danger-bg);color:var(--color-danger-text)}.badge-warning{background-color:var(--color-warning-bg);color:var(--color-warning-text)}.badge-info{background-color:var(--color-info-bg);color:var(--color-primary)}.alert-success,.alert-danger,.alert-warning,.alert-info{border-radius:var(--radius-xl);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px}.alert-success{border-color:#05966933}@supports (color:color-mix(in lab, red, red)){.alert-success{border-color:color-mix(in oklab, var(--color-success) 20%, transparent)}}.alert-success{background-color:var(--color-success-bg);color:var(--color-success-text)}.alert-danger{border-color:#e11d4833}@supports (color:color-mix(in lab, red, red)){.alert-danger{border-color:color-mix(in oklab, var(--color-danger) 20%, transparent)}}.alert-danger{background-color:var(--color-danger-bg);color:var(--color-danger-text)}.alert-warning{border-color:var(--color-warning-border);background-color:var(--color-warning-bg);color:var(--color-warning-text)}.alert-info{border-color:#0ea5e933}@supports (color:color-mix(in lab, red, red)){.alert-info{border-color:color-mix(in oklab, var(--color-info) 20%, transparent)}}.alert-info{background-color:var(--color-info-bg);color:var(--color-primary)}.code-block{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-code-bg);padding:calc(var(--spacing) * 4);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-code-text);font-family:JetBrains Mono,Fira Code,monospace;overflow:auto}.panel{margin-bottom:calc(var(--spacing) * 8)}.panel-title{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);font-family:Sora,system-ui,sans-serif;display:flex}.panel-title:before{content:"";height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 1);background:linear-gradient(180deg, var(--color-primary), var(--color-accent));border-radius:3.40282e38px}.card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding:calc(var(--spacing) * 4);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){.card{border-radius:var(--radius-2xl);padding:calc(var(--spacing) * 5)}}.card:hover{border-color:var(--color-border-strong)}.table-responsive{margin-inline:calc(var(--spacing) * -4);overflow-x:auto}@media (min-width:40rem){.table-responsive{margin-inline:calc(var(--spacing) * 0)}}.table-responsive>table{min-width:600px}@media (min-width:40rem){.table-responsive>table{min-width:calc(var(--spacing) * 0)}}.scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-hide::-webkit-scrollbar{display:none}.manage-tab{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:nowrap;color:var(--color-text-muted);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));--tw-duration:.15s;border-color:#0000;border-bottom-width:2px;flex-shrink:0;text-decoration-line:none;transition-duration:.15s;display:inline-flex}.manage-tab:hover{color:var(--color-text)}.manage-tab-active{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-primary);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);white-space:nowrap;color:var(--color-primary);flex-shrink:0;text-decoration-line:none;display:inline-flex}.public-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--color-surface);opacity:0;pointer-events:none;transition:opacity .25s;position:fixed}.public-overlay.open{opacity:1;pointer-events:auto}.nav-link-active,.nav-link-inactive{border-radius:var(--radius-xl);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;text-decoration-line:none;transition-duration:.15s;display:block}.nav-link-active{background-color:var(--color-primary-subtle);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-primary)}.nav-link-inactive{color:var(--color-text-secondary)}.nav-link-inactive:hover{background-color:var(--color-surface-raised);color:var(--color-text)}.bg-mesh{z-index:-1;background:var(--color-surface-sunken);position:fixed;inset:0;overflow:hidden}.bg-mesh:before,.bg-mesh:after{content:"";filter:blur(100px);opacity:.15;border-radius:50%;animation:20s ease-in-out infinite mesh-float;position:absolute}.bg-mesh:before{background:var(--color-primary);width:600px;height:600px;top:-10%;right:-10%}.bg-mesh:after{background:var(--color-accent);width:500px;height:500px;animation-direction:reverse;animation-delay:-10s;bottom:-10%;left:-10%}.dark .bg-mesh:before,.dark .bg-mesh:after{opacity:.08}@keyframes mesh-float{0%,to{transform:translate(0)scale(1)}33%{transform:translate(40px,-30px)scale(1.05)}66%{transform:translate(-20px,20px)scale(.95)}}.spinner{margin-left:calc(var(--spacing) * 2);height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);animation:var(--animate-spin);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-border);border-top-color:var(--color-primary);vertical-align:middle;border-radius:3.40282e38px;display:inline-block}.user-dropdown-wrap{position:relative}.user-dropdown-trigger{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-xl);--tw-border-style:none;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;background-color:#0000;border-style:none;transition-duration:.2s;display:flex}.user-dropdown-trigger:hover,.user-dropdown-trigger[aria-expanded=true]{background-color:var(--color-surface-raised)}.user-dropdown{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:240px;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);opacity:0;pointer-events:none;z-index:100;transition:opacity .18s,transform .18s;position:absolute;top:calc(100% + 6px);right:0;transform:translateY(-4px)scale(.97)}.user-dropdown.open{opacity:1;pointer-events:auto;transform:translateY(0)scale(1)}.app-sidebar .user-dropdown{inset:auto auto calc(100% + 6px) 0;transform:translateY(4px)scale(.97)}.app-sidebar .user-dropdown.open{transform:translateY(0)scale(1)}.dark .user-dropdown{box-shadow:0 0 0 1px #94a3b80f,0 20px 40px -8px #0009}.user-dropdown-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.user-dropdown-body{padding-block:calc(var(--spacing) * 1.5)}.user-dropdown-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2.5);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-text-secondary);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));--tw-duration:.15s;text-decoration-line:none;transition-duration:.15s;display:flex}.user-dropdown-item:hover{background-color:var(--color-surface-raised);color:var(--color-text)}.user-dropdown-divider{margin-block:calc(var(--spacing) * 1.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-border)}.user-dropdown-item.danger{color:var(--color-danger)}.user-dropdown-item.danger:hover{background-color:var(--color-danger-bg);color:var(--color-danger)}.dash-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding:calc(var(--spacing) * 4);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s}@media (min-width:40rem){.dash-card{border-radius:var(--radius-2xl);padding:calc(var(--spacing) * 5)}}.dash-card:hover{border-color:var(--color-border-strong);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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)}.dash-stat{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}@media (min-width:40rem){.dash-stat{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.dash-stat{font-family:Sora,system-ui,sans-serif}.dash-label{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-text-muted);text-transform:uppercase}.validation-message{margin-top:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-danger)}.app-layout{flex-direction:column;min-height:100vh;display:flex}.app-sidebar{border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--color-border);background-color:var(--color-surface);z-index:40;flex-shrink:0;width:16rem;transition:width .2s,transform .25s;position:fixed;top:0;bottom:0;left:0;transform:translate(-100%)}.app-sidebar.app-sidebar-open{transform:translate(0)}.app-sidebar.app-sidebar-collapsed{width:4rem}.app-sidebar.app-sidebar-collapsed .sidebar-label{display:none}.app-sidebar.app-sidebar-collapsed .sidebar-icon{margin:0 auto}.app-content{flex:1;margin-left:0;transition:margin-left .2s}.app-sidebar-backdrop{inset:calc(var(--spacing) * 0);z-index:30;background-color:#00000080;position:fixed}@supports (color:color-mix(in lab, red, red)){.app-sidebar-backdrop{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.app-sidebar-backdrop{opacity:0;pointer-events:none;transition:opacity .25s}.app-sidebar-backdrop.visible{opacity:1;pointer-events:auto}.app-sidebar-toggle{bottom:calc(var(--spacing) * 4);z-index:50;height:calc(var(--spacing) * 7);width:calc(var(--spacing) * 7);cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);color:var(--color-text-muted);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;border-radius:3.40282e38px;justify-content:center;align-items:center;transition-duration:.2s;position:fixed}@media (hover:hover){.app-sidebar-toggle:hover{background-color:var(--color-surface-raised);color:var(--color-text)}}.app-sidebar-toggle{display:none;left:1rem}.app-mobile-header{top:calc(var(--spacing) * 0);z-index:20;align-items:center;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex;position:sticky}@media (min-width:768px){.app-sidebar{transform:translate(0)}.app-content{margin-left:16rem}.app-sidebar-collapsed~.app-content{margin-left:4rem}.app-sidebar-backdrop{display:none}.app-sidebar-toggle{display:flex;left:15rem}.app-sidebar-collapsed~.app-sidebar-toggle{left:3rem}.app-mobile-header,.public-overlay{display:none}}.markdown-content h1{margin-top:calc(var(--spacing) * 6);margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h2{margin-top:calc(var(--spacing) * 5);margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h3{margin-top:calc(var(--spacing) * 4);margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text);font-family:Sora,sans-serif}.markdown-content h4{margin-top:calc(var(--spacing) * 3);margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text)}.markdown-content p{margin-bottom:calc(var(--spacing) * 3);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--color-text-secondary)}.markdown-content ul{margin-bottom:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 6);color:var(--color-text-secondary);list-style-type:disc}.markdown-content ol{margin-bottom:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 6);color:var(--color-text-secondary);list-style-type:decimal}.markdown-content li{margin-bottom:calc(var(--spacing) * 1)}.markdown-content code{background-color:var(--color-surface-sunken);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-text);border-radius:.25rem}.markdown-content pre{margin-bottom:calc(var(--spacing) * 3);border-radius:var(--radius-xl);background-color:var(--color-code-bg);padding:calc(var(--spacing) * 4);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-code-text);overflow:auto}.markdown-content pre code{padding:calc(var(--spacing) * 0);color:var(--color-code-text);background-color:#0000}.markdown-content a{color:var(--color-primary)}@media (hover:hover){.markdown-content a:hover{text-decoration-line:underline}}.markdown-content blockquote{margin-block:calc(var(--spacing) * 3);border-left-style:var(--tw-border-style);border-color:#0596694d;border-left-width:4px}@supports (color:color-mix(in lab, red, red)){.markdown-content blockquote{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.markdown-content blockquote{padding-left:calc(var(--spacing) * 4);color:var(--color-text-muted);font-style:italic}.markdown-content img{margin-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);max-width:100%}.markdown-content table{margin-block:calc(var(--spacing) * 3);border-collapse:collapse;width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.markdown-content th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding:calc(var(--spacing) * 2);text-align:left;--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-text)}.markdown-content td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border);padding:calc(var(--spacing) * 2);color:var(--color-text-secondary)}.markdown-content hr{margin-block:calc(var(--spacing) * 6);border-color:var(--color-border)}.markdown-content>:first-child{margin-top:calc(var(--spacing) * 0)}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-1{top:calc(var(--spacing) * 1)}.top-1\/2{top:50%}.top-4{top:calc(var(--spacing) * 4)}.top-20{top:calc(var(--spacing) * 20)}.top-\[50\%\]{top:50%}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-1{right:calc(var(--spacing) * 1)}.right-4{right:calc(var(--spacing) * 4)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.left-1{left:calc(var(--spacing) * 1)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing) * 2)}.left-\[50\%\]{left:50%}.left-full{left:100%}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[9999\]{z-index:9999}.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}}.m-0{margin:calc(var(--spacing) * 0)}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.mx-auto{margin-inline:auto}.my-0\.5{margin-block:calc(var(--spacing) * .5)}.my-1{margin-block:calc(var(--spacing) * 1)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-6{margin-block:calc(var(--spacing) * 6)}.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-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.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-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.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-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.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-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-20{height:calc(var(--spacing) * 20)}.h-36{height:calc(var(--spacing) * 36)}.h-\[18px\]{height:18px}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-30{max-height:calc(var(--spacing) * 30)}.max-h-50{max-height:calc(var(--spacing) * 50)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[300px\]{max-height:300px}.max-h-\[500px\]{max-height:500px}.max-h-screen{max-height:100vh}.min-h-\[80px\]{min-height:80px}.min-h-\[180px\]{min-height:180px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[400px\]{min-height:400px}.min-h-\[calc\(100vh-12rem\)\]{min-height:calc(100vh - 12rem)}.min-h-\[calc\(100vh-16rem\)\]{min-height:calc(100vh - 16rem)}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing) * 0)}.w-1{width:calc(var(--spacing) * 1)}.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-3\/4{width:75%}.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-11{width:calc(var(--spacing) * 11)}.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-36{width:calc(var(--spacing) * 36)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-\[18px\]{width:18px}.w-\[70px\]{width:70px}.w-\[100px\]{width:100px}.w-\[160px\]{width:160px}.w-auto{width:auto}.w-full{width:100%}.w-px{width:1px}.max-w-3xl{max-width:var(--container-3xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[80\%\]{max-width:80%}.max-w-\[140px\]{max-width:140px}.max-w-\[200px\]{max-width:200px}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[160px\]{min-width:160px}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.resize-y{resize:vertical}.list-decimal{list-style-type:decimal}.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))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing) * 0)}.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-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0\.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-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-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-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)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-\[2px\]{border-radius:2px}.rounded-\[inherit\]{border-radius:inherit}.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)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-\[--color-border\]{border-color:--color-border}.border-border{border-color:var(--color-border)}.border-border\/50{border-color:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.border-border\/50{border-color:color-mix(in oklab, var(--color-border) 50%, transparent)}}.border-danger{border-color:var(--color-danger)}.border-danger\/20{border-color:#e11d4833}@supports (color:color-mix(in lab, red, red)){.border-danger\/20{border-color:color-mix(in oklab, var(--color-danger) 20%, transparent)}}.border-danger\/30{border-color:#e11d484d}@supports (color:color-mix(in lab, red, red)){.border-danger\/30{border-color:color-mix(in oklab, var(--color-danger) 30%, transparent)}}.border-info\/20{border-color:#0ea5e933}@supports (color:color-mix(in lab, red, red)){.border-info\/20{border-color:color-mix(in oklab, var(--color-info) 20%, transparent)}}.border-primary{border-color:var(--color-primary)}.border-primary\/30{border-color:#0596694d}@supports (color:color-mix(in lab, red, red)){.border-primary\/30{border-color:color-mix(in oklab, var(--color-primary) 30%, transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-success\/20{border-color:#05966933}@supports (color:color-mix(in lab, red, red)){.border-success\/20{border-color:color-mix(in oklab, var(--color-success) 20%, transparent)}}.border-transparent{border-color:#0000}.border-warning-border{border-color:var(--color-warning-border)}.border-t-primary{border-top-color:var(--color-primary)}.border-t-transparent{border-top-color:#0000}.border-l-primary{border-left-color:var(--color-primary)}.border-l-transparent{border-left-color:#0000}.bg-\[--color-bg\]{background-color:--color-bg}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-border{background-color:var(--color-border)}.bg-border-strong{background-color:var(--color-border-strong)}.bg-danger{background-color:var(--color-danger)}.bg-danger-bg{background-color:var(--color-danger-bg)}.bg-danger\/10{background-color:#e11d481a}@supports (color:color-mix(in lab, red, red)){.bg-danger\/10{background-color:color-mix(in oklab, var(--color-danger) 10%, transparent)}}.bg-dark{background-color:var(--color-dark)}.bg-dark\/60{background-color:#0f172a99}@supports (color:color-mix(in lab, red, red)){.bg-dark\/60{background-color:color-mix(in oklab, var(--color-dark) 60%, transparent)}}.bg-info-bg{background-color:var(--color-info-bg)}.bg-muted{background-color:var(--color-muted)}.bg-primary{background-color:var(--color-primary)}.bg-primary-subtle{background-color:var(--color-primary-subtle)}.bg-primary\/5{background-color:#0596690d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary) 5%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-success-bg{background-color:var(--color-success-bg)}.bg-success\/10{background-color:#0596691a}@supports (color:color-mix(in lab, red, red)){.bg-success\/10{background-color:color-mix(in oklab, var(--color-success) 10%, transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-overlay{background-color:var(--color-surface-overlay)}.bg-surface-raised{background-color:var(--color-surface-raised)}.bg-surface-sunken{background-color:var(--color-surface-sunken)}.bg-transparent{background-color:#0000}.bg-warning-bg{background-color:var(--color-warning-bg)}.bg-white{background-color:var(--color-white)}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary{--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-accent{--tw-gradient-to:var(--color-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.fill-current{fill:currentColor}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.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-\[1px\]{padding:1px}.px-0{padding-inline:calc(var(--spacing) * 0)}.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-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.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-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top: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-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.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-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.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)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.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-accent{color:var(--color-accent)}.text-border{color:var(--color-border)}.text-current{color:currentColor}.text-danger{color:var(--color-danger)}.text-danger-text{color:var(--color-danger-text)}.text-danger-text\/60{color:#9f123999}@supports (color:color-mix(in lab, red, red)){.text-danger-text\/60{color:color-mix(in oklab, var(--color-danger-text) 60%, transparent)}}.text-green-600{color:var(--color-green-600)}.text-info{color:var(--color-info)}.text-muted{color:var(--color-muted)}.text-primary{color:var(--color-primary)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-success{color:var(--color-success)}.text-success-text{color:var(--color-success-text)}.text-text{color:var(--color-text)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-muted{color:var(--color-text-muted)}.text-text-muted\/50{color:#94a3b880}@supports (color:color-mix(in lab, red, red)){.text-text-muted\/50{color:color-mix(in oklab, var(--color-text-muted) 50%, transparent)}}.text-text-secondary{color:var(--color-text-secondary)}.text-warning{color:var(--color-warning)}.text-warning-text{color:var(--color-warning-text)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.opacity-100{opacity:1}.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-\(--shadow-danger\){--tw-shadow:var(--shadow-danger);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\(--shadow-primary\){--tw-shadow:var(--shadow-primary);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-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px 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)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + 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)}.ring-offset-surface{--tw-ring-offset-color:var(--color-surface)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-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-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/sub\:block:is(:where(.group\/sub):hover *){display:block}}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}.before\:h-5:before{content:var(--tw-content);height:calc(var(--spacing) * 5)}.before\:w-1:before{content:var(--tw-content);width:calc(var(--spacing) * 1)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-gradient-to-b:before{content:var(--tw-content);--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.before\:from-primary:before{content:var(--tw-content);--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.before\:to-accent:before{content:var(--tw-content);--tw-gradient-to:var(--color-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--spacing) * 0)}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-1:after{content:var(--tw-content);width:calc(var(--spacing) * 1)}.after\:-translate-x-1\/2:after{content:var(--tw-content);--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.focus-within\:relative:focus-within{position:relative}.focus-within\:z-20:focus-within{z-index:20}@media (hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:-translate-y-px:hover{--tw-translate-y:-1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-border-strong:hover{border-color:var(--color-border-strong)}.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:border-primary\/50:hover{border-color:#05966980}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary) 50%, transparent)}}.hover\:bg-danger-hover:hover{background-color:var(--color-danger-hover)}.hover\:bg-muted\/50:hover{background-color:#94a3b880}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted) 50%, transparent)}}.hover\:bg-primary:hover{background-color:var(--color-primary)}.hover\:bg-primary-subtle:hover{background-color:var(--color-primary-subtle)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-surface-raised:hover{background-color:var(--color-surface-raised)}.hover\:bg-surface-sunken:hover{background-color:var(--color-surface-sunken)}.hover\:text-danger-text:hover{color:var(--color-danger-text)}.hover\:text-danger\/80:hover{color:#e11d48cc}@supports (color:color-mix(in lab, red, red)){.hover\:text-danger\/80:hover{color:color-mix(in oklab, var(--color-danger) 80%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:text-text:hover{color:var(--color-text)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-\(--shadow-danger-hover\):hover{--tw-shadow:var(--shadow-danger-hover);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-\(--shadow-primary-hover\):hover{--tw-shadow:var(--shadow-primary-hover);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px 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-danger:focus{border-color:var(--color-danger)}.focus\:border-primary:focus{border-color:var(--color-primary)}.focus\:bg-primary:focus{background-color:var(--color-primary)}.focus\:bg-primary-subtle:focus{background-color:var(--color-primary-subtle)}.focus\:bg-surface-raised:focus{background-color:var(--color-surface-raised)}.focus\:text-primary:focus{color:var(--color-primary)}.focus\:text-text:focus{color:var(--color-text)}.focus\:text-text-inverse:focus{color:var(--color-text-inverse)}.focus\:opacity-100:focus{opacity:1}.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-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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-danger-bg:focus{--tw-ring-color:var(--color-danger-bg)}.focus\:ring-primary-ring:focus{--tw-ring-color:var(--color-primary-ring)}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--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-visible\:ring-4:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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-visible\:ring-primary-ring:focus-visible{--tw-ring-color:var(--color-primary-ring)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-\[0\.97\]:active{scale:.97}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.aria-selected\:bg-primary-subtle[aria-selected=true]{background-color:var(--color-primary-subtle)}.aria-selected\:bg-primary-subtle\/50[aria-selected=true]{background-color:#0596690a}@supports (color:color-mix(in lab, red, red)){.aria-selected\:bg-primary-subtle\/50[aria-selected=true]{background-color:color-mix(in oklab, var(--color-primary-subtle) 50%, transparent)}}.aria-selected\:text-text[aria-selected=true]{color:var(--color-text)}.aria-selected\:text-text-muted[aria-selected=true]{color:var(--color-text-muted)}.aria-selected\:opacity-100[aria-selected=true]{opacity:1}.data-\[active\]\:bg-primary-subtle[data-active]{background-color:var(--color-primary-subtle)}.data-\[active\]\:font-semibold[data-active]{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.data-\[active\]\:text-primary[data-active]{color:var(--color-primary)}.data-\[direction\=vertical\]\:flex-col[data-direction=vertical]{flex-direction:column}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.data-\[selected\=true\]\:bg-primary-subtle[data-selected=true]{background-color:var(--color-primary-subtle)}.data-\[selected\=true\]\:text-primary[data-selected=true]{color:var(--color-primary)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=active\]\:bg-surface[data-state=active]{background-color:var(--color-surface)}.data-\[state\=active\]\:text-text[data-state=active]{color:var(--color-text)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--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)}.data-\[state\=checked\]\:translate-x-5[data-state=checked]{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--color-primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--color-primary)}.data-\[state\=checked\]\:text-text-inverse[data-state=checked]{color:var(--color-text-inverse)}.data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.data-\[state\=on\]\:bg-primary-subtle[data-state=on]{background-color:var(--color-primary-subtle)}.data-\[state\=on\]\:text-primary[data-state=on]{color:var(--color-primary)}.data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=unchecked\]\:bg-border-strong[data-state=unchecked]{background-color:var(--color-border-strong)}.data-\[swipe\=cancel\]\:translate-x-0[data-swipe=cancel]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=end\]\:translate-x-\[var\(--radix-toast-swipe-end-x\)\][data-swipe=end]{--tw-translate-x:var(--radix-toast-swipe-end-x);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=move\]\:translate-x-\[var\(--radix-toast-swipe-move-x\)\][data-swipe=move]{--tw-translate-x:var(--radix-toast-swipe-move-x);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[swipe\=move\]\:transition-none[data-swipe=move]{transition-property:none}@media (min-width:40rem){.sm\:sticky{position:sticky}.sm\:top-20{top:calc(var(--spacing) * 20)}.sm\:top-auto{top:auto}.sm\:right-0{right:calc(var(--spacing) * 0)}.sm\:bottom-0{bottom:calc(var(--spacing) * 0)}.sm\:mx-0{margin-inline:calc(var(--spacing) * 0)}.sm\:mt-6{margin-top:calc(var(--spacing) * 6)}.sm\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.sm\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.sm\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.sm\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.sm\:h-16{height:calc(var(--spacing) * 16)}.sm\:h-48{height:calc(var(--spacing) * 48)}.sm\:min-h-\[200px\]{min-height:200px}.sm\:min-h-\[220px\]{min-height:220px}.sm\:min-h-\[250px\]{min-height:250px}.sm\:min-h-\[280px\]{min-height:280px}.sm\:w-16{width:calc(var(--spacing) * 16)}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-56{width:calc(var(--spacing) * 56)}.sm\:w-\[160px\]{width:160px}.sm\:w-\[180px\]{width:180px}.sm\:w-auto{width:auto}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:gap-0{gap:calc(var(--spacing) * 0)}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-5{gap:calc(var(--spacing) * 5)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\: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(.sm\: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(.sm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.sm\:overflow-x-visible{overflow-x:visible}.sm\:p-3{padding:calc(var(--spacing) * 3)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-0{padding-inline:calc(var(--spacing) * 0)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:py-6{padding-block:calc(var(--spacing) * 6)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:pt-4{padding-top:calc(var(--spacing) * 4)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}@media (min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:w-56{width:calc(var(--spacing) * 56)}.md\:max-w-\[420px\]{max-width:420px}.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))}.md\:grid-cols-\[2fr_3fr\]{grid-template-columns:2fr 3fr}.md\:flex-row{flex-direction:row}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:gap-6{gap:calc(var(--spacing) * 6)}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:block{display:block}.dark\:hidden{display:none}}.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-text-muted .recharts-cartesian-axis-tick text{fill:var(--color-text-muted)}.\[\&_\.recharts-cartesian-grid_line\[stroke\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke]{stroke:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.\[\&_\.recharts-cartesian-grid_line\[stroke\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke]{stroke:color-mix(in oklab, var(--color-border) 50%, transparent)}}.\[\&_\.recharts-curve\.recharts-tooltip-cursor\]\:stroke-border .recharts-curve.recharts-tooltip-cursor{stroke:var(--color-border)}.\[\&_\.recharts-dot\[stroke\]\]\:stroke-transparent .recharts-dot[stroke]{stroke:#0000}.\[\&_\.recharts-layer\]\:outline-none .recharts-layer{--tw-outline-style:none;outline-style:none}.\[\&_\.recharts-polar-grid_\[stroke\]\]\:stroke-border .recharts-polar-grid [stroke]{stroke:var(--color-border)}.\[\&_\.recharts-radial-bar-background-sector\]\:fill-surface-sunken .recharts-radial-bar-background-sector,.\[\&_\.recharts-rectangle\.recharts-tooltip-cursor\]\:fill-surface-sunken .recharts-rectangle.recharts-tooltip-cursor{fill:var(--color-surface-sunken)}.\[\&_\.recharts-reference-line_\[stroke\]\]\:stroke-border .recharts-reference-line [stroke]{stroke:var(--color-border)}.\[\&_\.recharts-sector\]\:outline-none .recharts-sector{--tw-outline-style:none;outline-style:none}.\[\&_\.recharts-sector\[stroke\]\]\:stroke-transparent .recharts-sector[stroke]{stroke:#0000}.\[\&_\.recharts-surface\]\:outline-none .recharts-surface{--tw-outline-style:none;outline-style:none}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-inline:calc(var(--spacing) * 2)}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-block:calc(var(--spacing) * 1.5)}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&_\[cmdk-group-heading\]\]\:text-text-muted [cmdk-group-heading]{color:var(--color-text-muted)}.\[\&_p\]\:leading-relaxed p{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[aria-selected\]\)\]\:bg-primary-subtle:has([aria-selected]){background-color:var(--color-primary-subtle)}.first\:\[\&\:has\(\[aria-selected\]\)\]\:rounded-l-md:first-child:has([aria-selected]){border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.last\:\[\&\:has\(\[aria-selected\]\)\]\:rounded-r-md:last-child:has([aria-selected]){border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.\[\&\:has\(\[aria-selected\]\.day-outside\)\]\:bg-primary-subtle\/50:has([aria-selected].day-outside){background-color:#0596690a}@supports (color:color-mix(in lab, red, red)){.\[\&\:has\(\[aria-selected\]\.day-outside\)\]\:bg-primary-subtle\/50:has([aria-selected].day-outside){background-color:color-mix(in oklab, var(--color-primary-subtle) 50%, transparent)}}.\[\&\:has\(\[aria-selected\]\.day-range-end\)\]\:rounded-r-md:has([aria-selected].day-range-end){border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing) * 0)}.\[\&\>span\]\:line-clamp-1>span{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[\&\>svg\]\:h-2\.5>svg{height:calc(var(--spacing) * 2.5)}.\[\&\>svg\]\:h-3>svg{height:calc(var(--spacing) * 3)}.\[\&\>svg\]\:h-3\.5>svg{height:calc(var(--spacing) * 3.5)}.\[\&\>svg\]\:w-2\.5>svg{width:calc(var(--spacing) * 2.5)}.\[\&\>svg\]\:w-3>svg{width:calc(var(--spacing) * 3)}.\[\&\>svg\]\:w-3\.5>svg{width:calc(var(--spacing) * 3.5)}.\[\&\>svg\]\:text-text-muted>svg{color:var(--color-text-muted)}.\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:h-px{height:1px}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:w-full{width:100%}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:left-0:after{content:var(--tw-content);left:calc(var(--spacing) * 0)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:h-1:after{content:var(--tw-content);height:calc(var(--spacing) * 1)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:w-full:after{content:var(--tw-content);width:100%}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:translate-x-0:after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}[data-direction=vertical] .\[\[data-direction\=vertical\]_\&\]\:after\:-translate-y-1\/2:after{content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.gradient-text{background:linear-gradient(135deg, var(--color-primary), var(--color-accent));-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text}.gradient-border{position:relative}.gradient-border:before{content:"";border-radius:inherit;background:linear-gradient(135deg, var(--color-primary), var(--color-accent));-webkit-mask-composite:xor;pointer-events:none;-webkit-mask-composite:xor;-webkit-mask-source-type:auto,auto;padding:1px;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-size:auto,auto;mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}}.dark{--color-primary-subtle:#05966926;--color-primary-ring:#05966959;--color-success-bg:#34d3991f;--color-success-text:#6ee7b7;--color-danger-bg:#e11d481f;--color-danger-text:#fda4af;--color-warning-bg:#d977061f;--color-warning-border:#d977064d;--color-warning-text:#fcd34d;--color-info-bg:#0ea5e91f;--color-surface:#0f172a;--color-surface-raised:#1e293b;--color-surface-sunken:#0b1120;--color-surface-overlay:#0f172ad9;--color-text:#f1f5f9;--color-text-secondary:#94a3b8;--color-text-muted:#64748b;--color-text-inverse:#0f172a;--color-border:#1e293b;--color-border-strong:#334155;--color-code-bg:#020617;--color-code-text:#e2e8f0}@media (prefers-reduced-motion:reduce){.app-sidebar,.app-sidebar-backdrop,.app-sidebar-toggle,.public-overlay{transition:none!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-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-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/tests/SimpleModule.Generator.Tests/TopologicalSortTests.cs b/tests/SimpleModule.Generator.Tests/TopologicalSortTests.cs index d5d06392..3292b703 100644 --- a/tests/SimpleModule.Generator.Tests/TopologicalSortTests.cs +++ b/tests/SimpleModule.Generator.Tests/TopologicalSortTests.cs @@ -205,6 +205,7 @@ public void SortModules_WithDependencies_ReordersByDependency() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -223,6 +224,7 @@ public void SortModules_WithDependencies_ReordersByDependency() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -273,6 +275,7 @@ public void SortModules_WithCycle_ReturnsOriginalOrder() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -291,6 +294,7 @@ public void SortModules_WithCycle_ReturnsOriginalOrder() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -344,6 +348,7 @@ public void SortModules_NoDependencies_PreservesOriginalOrder() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -362,6 +367,7 @@ public void SortModules_NoDependencies_PreservesOriginalOrder() false, false, false, + false, "", "", ImmutableArray.Empty, @@ -380,6 +386,7 @@ public void SortModules_NoDependencies_PreservesOriginalOrder() false, false, false, + false, "", "", ImmutableArray.Empty, diff --git a/tests/SimpleModule.Tests.Shared/Fixtures/SimpleModuleWebApplicationFactory.cs b/tests/SimpleModule.Tests.Shared/Fixtures/SimpleModuleWebApplicationFactory.cs index ecc4d53f..7cbcd34e 100644 --- a/tests/SimpleModule.Tests.Shared/Fixtures/SimpleModuleWebApplicationFactory.cs +++ b/tests/SimpleModule.Tests.Shared/Fixtures/SimpleModuleWebApplicationFactory.cs @@ -25,6 +25,7 @@ using SimpleModule.Permissions; using SimpleModule.Products; using SimpleModule.Rag.Module; +using SimpleModule.RateLimiting; using SimpleModule.Settings; using SimpleModule.Tenants; using SimpleModule.Users; @@ -69,6 +70,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) ReplaceDbContext(services); ReplaceDbContext(services); ReplaceDbContext(services); + ReplaceDbContext(services); ReplaceDbContext(services); ReplaceDbContext(services, useOpenIddict: true); @@ -170,6 +172,7 @@ private void EnsureModuleDatabasesCreated() EnsureTablesCreated(sp); EnsureTablesCreated(sp); EnsureTablesCreated(sp); + EnsureTablesCreated(sp); EnsureTablesCreated(sp); } diff --git a/tests/SimpleModule.Tests.Shared/SimpleModule.Tests.Shared.csproj b/tests/SimpleModule.Tests.Shared/SimpleModule.Tests.Shared.csproj index 406837ba..23a317f3 100644 --- a/tests/SimpleModule.Tests.Shared/SimpleModule.Tests.Shared.csproj +++ b/tests/SimpleModule.Tests.Shared/SimpleModule.Tests.Shared.csproj @@ -30,5 +30,6 @@ + diff --git a/tests/e2e/pages/rate-limiting/admin.page.ts b/tests/e2e/pages/rate-limiting/admin.page.ts new file mode 100644 index 00000000..ad44a588 --- /dev/null +++ b/tests/e2e/pages/rate-limiting/admin.page.ts @@ -0,0 +1,61 @@ +import type { Page } from '@playwright/test'; + +export class RateLimitingAdminPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/rate-limiting'); + } + + get heading() { + return this.page.getByRole('heading', { name: /rate limiting/i }); + } + + get storedRulesTab() { + return this.page.getByRole('tab', { name: /stored rules/i }); + } + + get activePoliciesTab() { + return this.page.getByRole('tab', { name: /active policies/i }); + } + + get createRuleButton() { + return this.page.getByRole('button', { name: /create rule/i }); + } + + get createDialog() { + return this.page.getByRole('dialog'); + } + + get policyNameInput() { + return this.createDialog.getByLabel(/policy name/i); + } + + get permitLimitInput() { + return this.createDialog.getByLabel(/permit limit/i); + } + + get createSubmitButton() { + return this.createDialog.getByRole('button', { name: /^create$/i }); + } + + get rulesTable() { + return this.page.getByRole('table'); + } + + get activePoliciesTable() { + return this.page.getByRole('table'); + } + + ruleRow(name: string) { + return this.page.getByRole('row', { name: new RegExp(name, 'i') }); + } + + ruleToggle(name: string) { + return this.ruleRow(name).getByRole('switch'); + } + + ruleDeleteButton(name: string) { + return this.ruleRow(name).getByRole('button', { name: /delete/i }); + } +} diff --git a/tests/e2e/tests/flows/rate-limiting-crud.spec.ts b/tests/e2e/tests/flows/rate-limiting-crud.spec.ts new file mode 100644 index 00000000..9b0a3c8c --- /dev/null +++ b/tests/e2e/tests/flows/rate-limiting-crud.spec.ts @@ -0,0 +1,123 @@ +import { expect, test } from '../../fixtures/base'; +import { RateLimitingAdminPage } from '../../pages/rate-limiting/admin.page'; + +test.describe('Rate Limiting CRUD flows', () => { + const testPolicyName = `e2e-test-${Date.now()}`; + + test('create, verify, and delete a rate limit rule via API', async ({ request }) => { + // API: create a rule + const createRes = await request.post('/api/rate-limiting', { + data: { + policyName: testPolicyName, + policyType: 'FixedWindow', + target: 'Ip', + permitLimit: 100, + windowSeconds: 60, + segmentsPerWindow: 4, + tokenLimit: 100, + tokensPerPeriod: 10, + replenishmentPeriodSeconds: 10, + queueLimit: 0, + isEnabled: true, + }, + }); + expect(createRes.ok()).toBeTruthy(); + const created = await createRes.json(); + expect(created.policyName).toBe(testPolicyName); + expect(created.permitLimit).toBe(100); + + // API: get all rules and verify our rule is present + const allRes = await request.get('/api/rate-limiting'); + expect(allRes.ok()).toBeTruthy(); + const rules = await allRes.json(); + expect(rules.some((r: { policyName: string }) => r.policyName === testPolicyName)).toBeTruthy(); + + // API: get rule by ID + const getRes = await request.get(`/api/rate-limiting/${created.id}`); + expect(getRes.ok()).toBeTruthy(); + const fetched = await getRes.json(); + expect(fetched.policyName).toBe(testPolicyName); + + // API: update the rule + const updateRes = await request.put(`/api/rate-limiting/${created.id}`, { + data: { + policyType: 'SlidingWindow', + target: 'User', + permitLimit: 200, + windowSeconds: 120, + segmentsPerWindow: 6, + tokenLimit: 100, + tokensPerPeriod: 10, + replenishmentPeriodSeconds: 10, + queueLimit: 0, + isEnabled: true, + }, + }); + expect(updateRes.ok()).toBeTruthy(); + const updated = await updateRes.json(); + expect(updated.permitLimit).toBe(200); + expect(updated.policyType).toBe('SlidingWindow'); + expect(updated.target).toBe('User'); + + // API: delete the rule + const deleteRes = await request.delete(`/api/rate-limiting/${created.id}`); + expect(deleteRes.status()).toBe(204); + + // API: verify deletion + const afterDeleteRes = await request.get(`/api/rate-limiting/${created.id}`); + expect(afterDeleteRes.status()).toBe(404); + }); + + test('active policies endpoint returns registered policies', async ({ request }) => { + const res = await request.get('/api/rate-limiting/active'); + expect(res.ok()).toBeTruthy(); + const policies = await res.json(); + expect(policies.length).toBeGreaterThan(0); + + // Verify built-in policies exist + const names = policies.map((p: { name: string }) => p.name); + expect(names).toContain('fixed-default'); + expect(names).toContain('sliding-strict'); + expect(names).toContain('token-bucket'); + expect(names).toContain('auth-strict'); + }); + + test('admin page shows created rule in UI', async ({ page, request }) => { + const admin = new RateLimitingAdminPage(page); + const ruleName = `e2e-ui-${Date.now()}`; + + // API: create a rule + const createRes = await request.post('/api/rate-limiting', { + data: { + policyName: ruleName, + policyType: 'FixedWindow', + target: 'Ip', + permitLimit: 50, + windowSeconds: 30, + segmentsPerWindow: 4, + tokenLimit: 100, + tokensPerPeriod: 10, + replenishmentPeriodSeconds: 10, + queueLimit: 0, + isEnabled: true, + }, + }); + expect(createRes.ok()).toBeTruthy(); + const created = await createRes.json(); + + // UI: verify the rule appears on the admin page + await admin.goto(); + await expect(admin.ruleRow(ruleName)).toBeVisible(); + + // Cleanup: delete via API + await request.delete(`/api/rate-limiting/${created.id}`); + }); + + test('create rule dialog opens from admin page', async ({ page }) => { + const admin = new RateLimitingAdminPage(page); + await admin.goto(); + await admin.createRuleButton.click(); + await expect(admin.createDialog).toBeVisible(); + await expect(admin.policyNameInput).toBeVisible(); + }); +}); diff --git a/tests/e2e/tests/smoke/rate-limiting.spec.ts b/tests/e2e/tests/smoke/rate-limiting.spec.ts new file mode 100644 index 00000000..7b9845ba --- /dev/null +++ b/tests/e2e/tests/smoke/rate-limiting.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '../../fixtures/base'; +import { RateLimitingAdminPage } from '../../pages/rate-limiting/admin.page'; + +test.describe('Rate Limiting pages', () => { + test('admin page loads', async ({ page }) => { + const admin = new RateLimitingAdminPage(page); + await admin.goto(); + await expect(admin.heading).toBeVisible(); + }); + + test('admin page has stored rules and active policies tabs', async ({ page }) => { + const admin = new RateLimitingAdminPage(page); + await admin.goto(); + await expect(admin.storedRulesTab).toBeVisible(); + await expect(admin.activePoliciesTab).toBeVisible(); + }); + + test('active policies tab shows policy table', async ({ page }) => { + const admin = new RateLimitingAdminPage(page); + await admin.goto(); + await admin.activePoliciesTab.click(); + await expect(admin.activePoliciesTable).toBeVisible(); + }); +});