From 8db1a6a5569d641c9e510c6404ab6f3ebd782c28 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Mon, 6 Apr 2026 08:15:54 +0200 Subject: [PATCH] Initial version --- .gitignore | 27 +- ConsoleApp1/ConsoleApp1.csproj | 14 + ConsoleApp1/Program.cs | 34 + .../EonaCat.LogStack.LogClient.csproj | 39 + .../LogCentralClient.cs | 186 + .../LogCentralEonaCatAdapter.cs | 64 + .../LogCentralOptions.cs | 21 + EonaCat.LogStack.LogClient/LogLevel.cs | 18 + EonaCat.LogStack.LogClient/Models/LogEntry.cs | 21 + EonaCat.LogStack.LogClient/readme.md | 389 + .../EonaCat.LogStack.OpenTelemetryFlow.csproj | 45 + .../LogBuilder.cs | 33 + .../OpenTelemetryFlow.cs | 162 + .../EonaCat.LogStack.SerilogTest.csproj | 18 + EonaCat.LogStack.SerilogTest/Program.cs | 169 + .../Properties/launchSettings.json | 12 + .../EonaCat.Logger.Server.csproj | 28 + EonaCat.LogStack.Server/Server.cs | 287 + .../Controllers/ApiController.cs | 554 + .../Data/DatabaseContext.cs | 67 + .../EonaCat.LogStack.Status.csproj | 14 + .../EonaCat.LogStack.Status.db | Bin 0 -> 4096 bytes .../EonaCat.LogStack.Status.db-shm | Bin 0 -> 32768 bytes .../EonaCat.LogStack.Status.db-wal | Bin 0 -> 317272 bytes EonaCat.LogStack.Status/Models/Models.cs | 206 + .../Pages/Admin/AlertRules.cshtml | 118 + .../Pages/Admin/AlertRules.cshtml.cs | 89 + .../Pages/Admin/Certificates.cshtml | 126 + .../Pages/Admin/Certificates.cshtml.cs | 90 + .../Pages/Admin/Incidents.cshtml | 165 + .../Pages/Admin/Incidents.cshtml.cs | 138 + .../Pages/Admin/Ingest.cshtml | 133 + .../Pages/Admin/Ingest.cshtml.cs | 20 + .../Pages/Admin/Login.cshtml | 52 + .../Pages/Admin/Login.cshtml.cs | 30 + .../Pages/Admin/Logout.cshtml | 2 + .../Pages/Admin/Logout.cshtml.cs | 16 + .../Pages/Admin/Monitors.cshtml | 229 + .../Pages/Admin/Monitors.cshtml.cs | 121 + .../Pages/Admin/Settings.cshtml | 129 + .../Pages/Admin/Settings.cshtml.cs | 86 + .../Pages/Analytics.cshtml | 80 + .../Pages/Analytics.cshtml.cs | 56 + .../Pages/Certificates.cshtml | 72 + .../Pages/Certificates.cshtml.cs | 20 + .../Pages/Incidents.cshtml | 70 + .../Pages/Incidents.cshtml.cs | 48 + EonaCat.LogStack.Status/Pages/Index.cshtml | 198 + EonaCat.LogStack.Status/Pages/Index.cshtml.cs | 78 + EonaCat.LogStack.Status/Pages/Logs.cshtml | 133 + EonaCat.LogStack.Status/Pages/Logs.cshtml.cs | 78 + EonaCat.LogStack.Status/Pages/Monitors.cshtml | 83 + .../Pages/Monitors.cshtml.cs | 37 + .../Pages/Shared/_Layout.cshtml | 162 + .../Pages/_ViewImports.cshtml | 5 + .../Pages/_ViewStart.cshtml | 3 + EonaCat.LogStack.Status/Program.cs | 45 + .../Properties/launchSettings.json | 12 + EonaCat.LogStack.Status/README.md | 119 + .../Services/AuthenticationService.cs | 77 + .../Services/MonitoringService.cs | 537 + .../Services/SyslogService.cs | 241 + EonaCat.LogStack.Status/appsettings.json | 9 + EonaCat.LogStack.Status/wwwroot/css/site.css | 803 ++ EonaCat.LogStack.Status/wwwroot/js/site.js | 317 + ...aCat.LogStack.Flows.WindowsEventLog.csproj | 46 + .../LogBuilder.cs | 32 + .../WindowsEventLogFlow.cs | 256 + EonaCat.LogStack.sln | 140 + EonaCat.LogStack/Constants.cs | 11 + EonaCat.LogStack/DllInfo.cs | 27 + EonaCat.LogStack/Enums.cs | 199 + EonaCat.LogStack/EonaCat.LogStack.csproj | 92 + EonaCat.LogStack/EonaCatLogger.cs | 291 + .../EonaCatLoggerCore/BoosterBase.cs | 22 + .../EonaCatLoggerCore/Boosters/AppBooster.cs | 25 + .../Boosters/ApplicationBooster.cs | 34 + .../Boosters/CallbackBooster.cs | 41 + .../Boosters/CorrelationIdBooster.cs | 27 + .../Boosters/CustomTextBooster.cs | 37 + .../EonaCatLoggerCore/Boosters/DateBooster.cs | 21 + .../Boosters/EnvironmentBooster.cs | 27 + .../Boosters/FrameworkBooster.cs | 23 + .../Boosters/LevelFilterBooster.cs | 28 + .../Boosters/MachineNameBooster.cs | 25 + .../Boosters/MemoryBooster.cs | 22 + .../EonaCatLoggerCore/Boosters/OSBooster.cs | 23 + .../Boosters/ProcStartBooster.cs | 24 + .../Boosters/ProcessIdBooster.cs | 25 + .../Boosters/ThreadIdBooster.cs | 21 + .../Boosters/ThreadNameBooster.cs | 21 + .../Boosters/TicksBooster.cs | 21 + .../EonaCatLoggerCore/Boosters/TimeBooster.cs | 21 + .../Boosters/TimestampBooster.cs | 36 + .../Boosters/UptimeBooster.cs | 25 + .../EonaCatLoggerCore/Boosters/UserBooster.cs | 23 + .../EonaCatLoggerCore/ColorSchema.cs | 74 + .../EonaCatLoggerCore/CompressionFormat.cs | 11 + EonaCat.LogStack/EonaCatLoggerCore/Enums.cs | 59 + .../EonaCatLoggerCore/FileOutputFormat.cs | 14 + .../EonaCatLoggerCore/Flows/AuditFlow.cs | 400 + .../EonaCatLoggerCore/Flows/ConsoleFlow.cs | 285 + .../EonaCatLoggerCore/Flows/DatabaseFlow.cs | 233 + .../Flows/DiagnosticsFlow.cs | 300 + .../EonaCatLoggerCore/Flows/DiscordFlow.cs | 199 + .../Flows/ElasticSearchFlow.cs | 190 + .../EonaCatLoggerCore/Flows/EmailFlow.cs | 331 + .../Flows/EncryptedFileFlow.cs | 499 + .../EonaCatLoggerCore/Flows/EventLogFlow.cs | 214 + .../EonaCatLoggerCore/Flows/FailoverFlow.cs | 50 + .../EonaCatLoggerCore/Flows/FileFlow.cs | 1638 +++ .../EonaCatLoggerCore/Flows/GrayLogFlow.cs | 287 + .../EonaCatLoggerCore/Flows/HttpFlow.cs | 285 + .../EonaCatLoggerCore/Flows/MemoryFlow.cs | 180 + .../Flows/MicrosoftTeamsFlow.cs | 180 + .../EonaCatLoggerCore/Flows/RedisFlow.cs | 401 + .../EonaCatLoggerCore/Flows/RetryFlow.cs | 67 + .../Flows/RollingBufferFlow.cs | 239 + .../EonaCatLoggerCore/Flows/SignalRFlow.cs | 317 + .../EonaCatLoggerCore/Flows/SlackFlow.cs | 176 + .../EonaCatLoggerCore/Flows/SnmpTrapFlow.cs | 69 + .../EonaCatLoggerCore/Flows/SplunkFlow.cs | 203 + .../EonaCatLoggerCore/Flows/StatusFlow.cs | 320 + .../EonaCatLoggerCore/Flows/SyslogTcpFlow.cs | 270 + .../EonaCatLoggerCore/Flows/SyslogUdpFlow.cs | 238 + .../EonaCatLoggerCore/Flows/TcpFlow.cs | 289 + .../EonaCatLoggerCore/Flows/TelegramFlow.cs | 183 + .../EonaCatLoggerCore/Flows/ThrottledFlow.cs | 303 + .../EonaCatLoggerCore/Flows/UdpFlow.cs | 207 + .../EonaCatLoggerCore/Flows/WebhookFlow.cs | 85 + .../EonaCatLoggerCore/Flows/ZabbixFlow.cs | 235 + .../EonaCatLoggerCore/IBooster.cs | 26 + EonaCat.LogStack/EonaCatLoggerCore/IFlow.cs | 126 + .../EonaCatLoggerCore/LogEvent.cs | 169 + .../EonaCatLoggerCore/LogStats.cs | 23 + .../Policies/FileRetentionPolicy.cs | 25 + .../Policies/SamplingPolicy.cs | 39 + .../EonaCatLoggerCore/StringBuilderPool.cs | 35 + .../Extensions/DateTimeExtensions.cs | 14 + .../Extensions/DictionaryExtensions.cs | 19 + .../Extensions/ExceptionExtensions.cs | 101 + .../Extensions/ObjectExtensions.cs | 843 ++ EonaCat.LogStack/Extensions/OffsetStream.cs | 260 + EonaCat.LogStack/Helpers/ColorHelper.cs | 140 + EonaCat.LogStack/Helpers/EnumHelper.cs | 52 + EonaCat.LogStack/LogBuilder.cs | 1076 ++ EonaCat.LogStack/LogMessage.cs | 17 + EonaCat.LogStack/LoggerDiagnostics.cs | 22 + EonaCat.LogStack/Server.cs | 602 + EonaCat.LogStack/icon.ico | Bin 0 -> 254014 bytes EonaCat.LogStack/icon.png | Bin 0 -> 89562 bytes EonaCat.LogStack/libman.json | 5 + LICENSE | 213 +- README.md | 476 +- .../EonaCat.LogStack.Test.Web.csproj | 24 + .../Pages/Error.cshtml | 26 + .../Pages/Error.cshtml.cs | 26 + .../Pages/Index.cshtml | 10 + .../Pages/Index.cshtml.cs | 17 + .../Pages/Privacy.cshtml | 8 + .../Pages/Privacy.cshtml.cs | 17 + .../Pages/Shared/_Layout.cshtml | 51 + .../Pages/Shared/_Layout.cshtml.css | 48 + .../Shared/_ValidationScriptsPartial.cshtml | 2 + .../Pages/_ViewImports.cshtml | 3 + .../Pages/_ViewStart.cshtml | 3 + Testers/EonaCat.LogStack.Test.Web/Program.cs | 483 + .../Properties/launchSettings.json | 28 + .../appsettings.Development.json | 9 + .../appsettings.json | 9 + Testers/EonaCat.LogStack.Test.Web/libman.json | 5 + .../wwwroot/css/site.css | 18 + .../wwwroot/favicon.ico | Bin 0 -> 5430 bytes .../wwwroot/js/site.js | 4 + .../wwwroot/lib/bootstrap/LICENSE | 22 + .../lib/bootstrap/dist/css/bootstrap-grid.css | 4997 +++++++ .../bootstrap/dist/css/bootstrap-grid.css.map | 1 + .../bootstrap/dist/css/bootstrap-grid.min.css | 7 + .../dist/css/bootstrap-grid.min.css.map | 1 + .../bootstrap/dist/css/bootstrap-grid.rtl.css | 4996 +++++++ .../dist/css/bootstrap-grid.rtl.css.map | 1 + .../dist/css/bootstrap-grid.rtl.min.css | 7 + .../dist/css/bootstrap-grid.rtl.min.css.map | 1 + .../bootstrap/dist/css/bootstrap-reboot.css | 427 + .../dist/css/bootstrap-reboot.css.map | 1 + .../dist/css/bootstrap-reboot.min.css | 8 + .../dist/css/bootstrap-reboot.min.css.map | 1 + .../dist/css/bootstrap-reboot.rtl.css | 424 + .../dist/css/bootstrap-reboot.rtl.css.map | 1 + .../dist/css/bootstrap-reboot.rtl.min.css | 8 + .../dist/css/bootstrap-reboot.rtl.min.css.map | 1 + .../dist/css/bootstrap-utilities.css | 4866 +++++++ .../dist/css/bootstrap-utilities.css.map | 1 + .../dist/css/bootstrap-utilities.min.css | 7 + .../dist/css/bootstrap-utilities.min.css.map | 1 + .../dist/css/bootstrap-utilities.rtl.css | 4857 +++++++ .../dist/css/bootstrap-utilities.rtl.css.map | 1 + .../dist/css/bootstrap-utilities.rtl.min.css | 7 + .../css/bootstrap-utilities.rtl.min.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.css | 11221 ++++++++++++++++ .../lib/bootstrap/dist/css/bootstrap.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.min.css | 7 + .../bootstrap/dist/css/bootstrap.min.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.rtl.css | 11197 +++++++++++++++ .../bootstrap/dist/css/bootstrap.rtl.css.map | 1 + .../bootstrap/dist/css/bootstrap.rtl.min.css | 7 + .../dist/css/bootstrap.rtl.min.css.map | 1 + .../lib/bootstrap/dist/js/bootstrap.bundle.js | 6780 ++++++++++ .../bootstrap/dist/js/bootstrap.bundle.js.map | 1 + .../bootstrap/dist/js/bootstrap.bundle.min.js | 7 + .../dist/js/bootstrap.bundle.min.js.map | 1 + .../lib/bootstrap/dist/js/bootstrap.esm.js | 4977 +++++++ .../bootstrap/dist/js/bootstrap.esm.js.map | 1 + .../bootstrap/dist/js/bootstrap.esm.min.js | 7 + .../dist/js/bootstrap.esm.min.js.map | 1 + .../lib/bootstrap/dist/js/bootstrap.js | 5026 +++++++ .../lib/bootstrap/dist/js/bootstrap.js.map | 1 + .../lib/bootstrap/dist/js/bootstrap.min.js | 7 + .../bootstrap/dist/js/bootstrap.min.js.map | 1 + .../jquery-validation-unobtrusive/LICENSE.txt | 12 + .../jquery.validate.unobtrusive.js | 432 + .../jquery.validate.unobtrusive.min.js | 5 + .../wwwroot/lib/jquery-validation/LICENSE.md | 22 + .../dist/additional-methods.js | 1158 ++ .../dist/additional-methods.min.js | 4 + .../jquery-validation/dist/jquery.validate.js | 1601 +++ .../dist/jquery.validate.min.js | 4 + .../wwwroot/lib/jquery/LICENSE.txt | 36 + .../wwwroot/lib/jquery/dist/jquery.js | 10872 +++++++++++++++ .../wwwroot/lib/jquery/dist/jquery.min.js | 2 + .../wwwroot/lib/jquery/dist/jquery.min.map | 1 + 231 files changed, 96983 insertions(+), 63 deletions(-) create mode 100644 ConsoleApp1/ConsoleApp1.csproj create mode 100644 ConsoleApp1/Program.cs create mode 100644 EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj create mode 100644 EonaCat.LogStack.LogClient/LogCentralClient.cs create mode 100644 EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs create mode 100644 EonaCat.LogStack.LogClient/LogCentralOptions.cs create mode 100644 EonaCat.LogStack.LogClient/LogLevel.cs create mode 100644 EonaCat.LogStack.LogClient/Models/LogEntry.cs create mode 100644 EonaCat.LogStack.LogClient/readme.md create mode 100644 EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj create mode 100644 EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs create mode 100644 EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs create mode 100644 EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj create mode 100644 EonaCat.LogStack.SerilogTest/Program.cs create mode 100644 EonaCat.LogStack.SerilogTest/Properties/launchSettings.json create mode 100644 EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj create mode 100644 EonaCat.LogStack.Server/Server.cs create mode 100644 EonaCat.LogStack.Status/Controllers/ApiController.cs create mode 100644 EonaCat.LogStack.Status/Data/DatabaseContext.cs create mode 100644 EonaCat.LogStack.Status/EonaCat.LogStack.Status.csproj create mode 100644 EonaCat.LogStack.Status/EonaCat.LogStack.Status.db create mode 100644 EonaCat.LogStack.Status/EonaCat.LogStack.Status.db-shm create mode 100644 EonaCat.LogStack.Status/EonaCat.LogStack.Status.db-wal create mode 100644 EonaCat.LogStack.Status/Models/Models.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Login.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Login.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Analytics.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Analytics.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Certificates.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Certificates.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Incidents.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Incidents.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Index.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Index.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Logs.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Logs.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Monitors.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/Monitors.cshtml.cs create mode 100644 EonaCat.LogStack.Status/Pages/Shared/_Layout.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/_ViewImports.cshtml create mode 100644 EonaCat.LogStack.Status/Pages/_ViewStart.cshtml create mode 100644 EonaCat.LogStack.Status/Program.cs create mode 100644 EonaCat.LogStack.Status/Properties/launchSettings.json create mode 100644 EonaCat.LogStack.Status/README.md create mode 100644 EonaCat.LogStack.Status/Services/AuthenticationService.cs create mode 100644 EonaCat.LogStack.Status/Services/MonitoringService.cs create mode 100644 EonaCat.LogStack.Status/Services/SyslogService.cs create mode 100644 EonaCat.LogStack.Status/appsettings.json create mode 100644 EonaCat.LogStack.Status/wwwroot/css/site.css create mode 100644 EonaCat.LogStack.Status/wwwroot/js/site.js create mode 100644 EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj create mode 100644 EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs create mode 100644 EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs create mode 100644 EonaCat.LogStack.sln create mode 100644 EonaCat.LogStack/Constants.cs create mode 100644 EonaCat.LogStack/DllInfo.cs create mode 100644 EonaCat.LogStack/Enums.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack.csproj create mode 100644 EonaCat.LogStack/EonaCatLogger.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Enums.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/ElasticSearchFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/EmailFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/EventLogFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/GrayLogFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/HttpFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/MemoryFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/MicrosoftTeamsFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/RedisFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/RetryFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/RollingBufferFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SignalRFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SlackFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SnmpTrapFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SplunkFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogTcpFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogUdpFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/TcpFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/TelegramFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/ThrottledFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/UdpFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/WebhookFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/ZabbixFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/IBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/IFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/LogEvent.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/LogStats.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Policies/FileRetentionPolicy.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Policies/SamplingPolicy.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/StringBuilderPool.cs create mode 100644 EonaCat.LogStack/Extensions/DateTimeExtensions.cs create mode 100644 EonaCat.LogStack/Extensions/DictionaryExtensions.cs create mode 100644 EonaCat.LogStack/Extensions/ExceptionExtensions.cs create mode 100644 EonaCat.LogStack/Extensions/ObjectExtensions.cs create mode 100644 EonaCat.LogStack/Extensions/OffsetStream.cs create mode 100644 EonaCat.LogStack/Helpers/ColorHelper.cs create mode 100644 EonaCat.LogStack/Helpers/EnumHelper.cs create mode 100644 EonaCat.LogStack/LogBuilder.cs create mode 100644 EonaCat.LogStack/LogMessage.cs create mode 100644 EonaCat.LogStack/LoggerDiagnostics.cs create mode 100644 EonaCat.LogStack/Server.cs create mode 100644 EonaCat.LogStack/icon.ico create mode 100644 EonaCat.LogStack/icon.png create mode 100644 EonaCat.LogStack/libman.json create mode 100644 Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml.cs create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml.cs create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml.cs create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_ValidationScriptsPartial.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/_ViewImports.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Pages/_ViewStart.cshtml create mode 100644 Testers/EonaCat.LogStack.Test.Web/Program.cs create mode 100644 Testers/EonaCat.LogStack.Test.Web/Properties/launchSettings.json create mode 100644 Testers/EonaCat.LogStack.Test.Web/appsettings.Development.json create mode 100644 Testers/EonaCat.LogStack.Test.Web/appsettings.json create mode 100644 Testers/EonaCat.LogStack.Test.Web/libman.json create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/css/site.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/favicon.ico create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/js/site.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/LICENSE create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation/LICENSE.md create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation/dist/additional-methods.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation/dist/additional-methods.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation/dist/jquery.validate.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery/LICENSE.txt create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery/dist/jquery.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery/dist/jquery.min.js create mode 100644 Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/.gitignore b/.gitignore index 7e2e97c..1998960 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser @@ -83,8 +83,6 @@ StyleCopReport.xml *.pgc *.pgd *.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp *.sbr *.tlb *.tli @@ -209,6 +207,9 @@ PublishScripts/ *.nuget.props *.nuget.targets +# Nuget personal access tokens and Credentials +nuget.config + # Microsoft Azure Build Output csx/ *.build.csdef @@ -297,17 +298,6 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -364,9 +354,6 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ -# Visual Studio History (VSHistory) files -.vshistory/ - # BeatPulse healthcheck temp database healthchecksdb @@ -398,6 +385,7 @@ FodyWeavers.xsd *.msp # JetBrains Rider +.idea/ *.sln.iml # ---> VisualStudioCode @@ -406,11 +394,8 @@ FodyWeavers.xsd !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -!.vscode/*.code-snippets +*.code-workspace # Local History for Visual Studio Code .history/ -# Built Visual Studio Code Extensions -*.vsix - diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..22bf901 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9 + enable + enable + + + + + + + diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..a2dba27 --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,34 @@ +using EonaCat.LogStack.Configuration; +using EonaCat.LogStack.Core; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +var logger = new LogBuilder("MyApp") + .WithMinimumLevel(LogLevel.Information) + .WriteToConsole() + .WriteToFile("C:\\tesss", maxFileSize: 50 * 1024 * 1024) + //.WriteToJsonFile("./logs", maxFileSize: 50 * 1024 * 1024) + //.WriteToHttp("https://127.0.0.1") + //.WriteToUdp("127.0.0.1", 514) + //.WriteToTcp("127.0.0.1", 514) + //.WriteToDatabase(null) + //.WriteToDiscord("https://discord.com/api/webhooks/...") + //.WriteToMicrosoftTeams("https://outlook.office.com/webhook/...") + //.WriteToElasticSearch("http://localhost:9200/logs") + //.WriteToGraylogFlow(null) + //.WriteToZabbixFlow(null) + .BoostWithCorrelationId() + .BoostWithProcessId() + .Build(); + + + +while (true) +{ + logger.Information("Application started"); + logger.Error(new Exception("Nerd!"), "Something went wrong"); + await Task.Delay(1); +} + +await logger.DisposeAsync(); // Flushes all logs \ No newline at end of file diff --git a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj new file mode 100644 index 0000000..3bfd282 --- /dev/null +++ b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj @@ -0,0 +1,39 @@ + + + netstandard2.1 + true + EonaCat.LogStack.LogClient + 0.0.1 + EonaCat (Jeroen Saey) + Logging client for the EonaCat Logger LogServer LogStack + logging;monitoring;analytics;diagnostics + EonaCat (Jeroen Saey) + icon.png + readme.md + https://git.saey.me/EonaCat/EonaCat.LogStack.LogClient + git + LICENSE + + + + True + \ + + + True + \ + + + + + + + + + + + True + \ + + + \ No newline at end of file diff --git a/EonaCat.LogStack.LogClient/LogCentralClient.cs b/EonaCat.LogStack.LogClient/LogCentralClient.cs new file mode 100644 index 0000000..e4d5ba6 --- /dev/null +++ b/EonaCat.LogStack.LogClient/LogCentralClient.cs @@ -0,0 +1,186 @@ +using EonaCat.Json; +using EonaCat.LogStack.Extensions; +using EonaCat.LogStack.LogClient.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.LogClient +{ + public class EonaCatPayLoad + { + public List? Events { get; set; } + } + + public class EonaCatLogEvent + { + public string Timestamp { get; set; } = default!; + public string Level { get; set; } = default!; + public string Message { get; set; } = default!; + public string Category { get; set; } = default!; + public ExceptionDto? Exception { get; set; } + public Dictionary? Properties { get; set; } + } + + public class ExceptionDto + { + public string Type { get; set; } = default!; + public string Message { get; set; } = default!; + public string? StackTrace { get; set; } + } + + public class LogCentralClient : IDisposable + { + private readonly HttpClient _httpClient; + private readonly LogCentralOptions _options; + private readonly ConcurrentQueue _logQueue; + private readonly Timer _flushTimer; + private readonly SemaphoreSlim _flushSemaphore; + private bool _disposed; + + public LogCentralClient(LogCentralOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _httpClient = new HttpClient { BaseAddress = new Uri(_options.ServerUrl) }; + _httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.ApiKey); + _logQueue = new ConcurrentQueue(); + _flushSemaphore = new SemaphoreSlim(1, 1); + _flushTimer = new Timer(async _ => await FlushAsync(), null, + TimeSpan.FromSeconds(_options.FlushIntervalSeconds), + TimeSpan.FromSeconds(_options.FlushIntervalSeconds)); + } + + public async Task LogAsync(LogEntry entry) + { + entry.Source = _options.ApplicationName; + entry.Timestamp = DateTime.UtcNow; + entry.Message ??= ""; + + var properties = new Dictionary(); + if (_options.ApplicationName != null) + { + properties.Add("ApplicationName", _options.ApplicationName); + } + + if (_options.ApplicationVersion != null) + { + properties.Add("ApplicationVersion", _options.ApplicationVersion); + } + + if (_options.Environment != null) + { + properties.Add("Environment", _options.Environment); + } + + if (!string.IsNullOrEmpty(entry.TraceId)) + { + properties.Add("TraceId", entry.TraceId); + } + + entry.Properties = JsonHelper.ToJson(properties); + + _logQueue.Enqueue(entry); + + if (_logQueue.Count >= _options.BatchSize) + { + await FlushAsync(); + } + } + + private async Task FlushAsync() + { + if (_logQueue.IsEmpty) + { + return; + } + + await _flushSemaphore.WaitAsync(); + try + { + var batch = new List(); + while (batch.Count < _options.BatchSize && _logQueue.TryDequeue(out var entry)) + { + batch.Add(entry); + } + + if (batch.Count > 0) + { + await SendBatchToEonaCatAsync(batch); + } + } + finally + { + _flushSemaphore.Release(); + } + } + + private async Task SendBatchToEonaCatAsync(List batch) + { + try + { + var eventsArray = batch.Select(e => new + { + timestamp = e.Timestamp.ToString("O"), + level = e.Level, + message = e.Message ?? "", // empty message is fine + exception = string.IsNullOrEmpty(e.Exception) ? null : new + { + type = "Exception", + message = e.Exception, + stackTrace = e.StackTrace + }, + properties = string.IsNullOrEmpty(e.Properties) + ? new Dictionary() // <-- same type now + : JsonHelper.ToObject>(e.Properties) + }).ToArray(); + + var json = JsonHelper.ToJson(eventsArray); + + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync("api/logs/eonacat", content); + + var responseContent = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + if (_options.EnableFallbackLogging) + { + Console.WriteLine($"[LogCentral] Failed to send logs to EonaCat: {ex.Message}"); + } + + foreach (var entry in batch) + { + _logQueue.Enqueue(entry); + } + } + } + + public async Task FlushAndDisposeAsync() + { + await FlushAsync(); + Dispose(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _flushTimer?.Dispose(); + FlushAsync().GetAwaiter().GetResult(); + _httpClient?.Dispose(); + _flushSemaphore?.Dispose(); + _disposed = true; + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs b/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs new file mode 100644 index 0000000..6801ef9 --- /dev/null +++ b/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs @@ -0,0 +1,64 @@ +using EonaCat.LogStack.Configuration; +using EonaCat.LogStack.Extensions; +using EonaCat.LogStack.LogClient.Models; +using System; +using System.Collections.Generic; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.LogClient +{ + public class LogCentralEonaCatAdapter : IDisposable + { + private readonly LogCentralClient _client; + private LogBuilder _logBuilder; + + public LogCentralEonaCatAdapter(LogBuilder logBuilder, LogCentralClient client) + { + _client = client; + _logBuilder.OnLog += LogSettings_OnLog; + } + + private void LogSettings_OnLog(object sender, LogMessage e) + { + var entry = new LogEntry + { + Level = e.Level.ToString().ToLower(), + Message = e.Message, + Properties = new Dictionary + { + { "Source", e.Origin ?? "Unknown" }, + { "Category", e.Category ?? "General" } + }.ToJson() + }; + + if (e.Exception != null) + { + entry.Exception = e.Exception.ToString(); + entry.StackTrace = e.Exception.StackTrace; + } + + _client.LogAsync(entry).ConfigureAwait(false); + } + private static LogLevel MapLogLevel(Core.LogLevel logType) + { + return logType switch + { + Core.LogLevel.Trace => LogLevel.Trace, + Core.LogLevel.Debug => LogLevel.Debug, + Core.LogLevel.Information => LogLevel.Information, + Core.LogLevel.Warning => LogLevel.Warning, + Core.LogLevel.Error => LogLevel.Error, + Core.LogLevel.Critical => LogLevel.Critical, + _ => LogLevel.Information + }; + } + + public void Dispose() + { + _logBuilder.OnLog -= LogSettings_OnLog; + GC.SuppressFinalize(this); + } + } +} diff --git a/EonaCat.LogStack.LogClient/LogCentralOptions.cs b/EonaCat.LogStack.LogClient/LogCentralOptions.cs new file mode 100644 index 0000000..46464d7 --- /dev/null +++ b/EonaCat.LogStack.LogClient/LogCentralOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.LogClient +{ + public class LogCentralOptions + { + public string ServerUrl { get; set; } = "https://localhost:62299"; + public string ApiKey { get; set; } = string.Empty; + public string ApplicationName { get; set; } = string.Empty; + public string ApplicationVersion { get; set; } = "1.0.0"; + public string Environment { get; set; } = "Production"; + public int BatchSize { get; set; } = 1; + public int FlushIntervalSeconds { get; set; } = 5; + public bool EnableFallbackLogging { get; set; } = true; + } +} diff --git a/EonaCat.LogStack.LogClient/LogLevel.cs b/EonaCat.LogStack.LogClient/LogLevel.cs new file mode 100644 index 0000000..18c0720 --- /dev/null +++ b/EonaCat.LogStack.LogClient/LogLevel.cs @@ -0,0 +1,18 @@ +namespace EonaCat.LogStack.LogClient +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public enum LogLevel + { + Trace = 0, + Debug = 1, + Information = 2, + Warning = 3, + Error = 4, + Critical = 5, + Traffic = 6, + Security = 7, + Analytics = 8 + } +} diff --git a/EonaCat.LogStack.LogClient/Models/LogEntry.cs b/EonaCat.LogStack.LogClient/Models/LogEntry.cs new file mode 100644 index 0000000..c2fc597 --- /dev/null +++ b/EonaCat.LogStack.LogClient/Models/LogEntry.cs @@ -0,0 +1,21 @@ +using System; + +namespace EonaCat.LogStack.LogClient.Models +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class LogEntry + { + public int Id { get; set; } + public string Source { get; set; } = "system"; + public string Level { get; set; } = "info"; + public string Message { get; set; } = ""; + public string? Properties { get; set; } + public string? Exception { get; set; } + public string? StackTrace { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public string? TraceId { get; set; } + public string? Host { get; set; } + } +} diff --git a/EonaCat.LogStack.LogClient/readme.md b/EonaCat.LogStack.LogClient/readme.md new file mode 100644 index 0000000..24d3a5d --- /dev/null +++ b/EonaCat.LogStack.LogClient/readme.md @@ -0,0 +1,389 @@ +# EonaCat.LogStack.LogClient + +### Client Installation + +#### Via NuGet Package Manager: +```bash +dotnet add package EonaCat.LogStack.LogClient +``` + +#### Via Package Manager Console: +```powershell +Install-Package EonaCat.LogStack.LogClient +``` + +## 📖 Usage Examples + +### Basic Setup + +```csharp +using EonaCat.LogStack.LogClient; +using EonaCat.LogStack.LogClient.Models; + +// Configure the client +var options = new LogCentralOptions +{ + ServerUrl = "https://your-logcentral-server.com", + ApiKey = "your-api-key-here", + ApplicationName = "MyAwesomeApp", + ApplicationVersion = "1.0.0", + Environment = "Production", + BatchSize = 50, + FlushIntervalSeconds = 5 +}; + +var logClient = new LogCentralClient(options); +``` + +### Integration with EonaCat.LogStack + +```csharp +using EonaCat.LogStack; +using EonaCat.LogStack.LogClient.Integration; + +var loggerSettings = new LoggerSettings(); +loggerSettings.UseLocalTime = true; +loggerSettings.Id = "TEST"; +var logger = new LogManager(loggerSettings); + +// Create the adapter +var adapter = new LogCentralEonaCatAdapter(loggerSettings, logClient); + +// Now all EonaCat.LogStack logs will be sent to LogCentral automatically +logger.Log("Application started", LogLevel.Info); +logger.Log("User logged in", LogLevel.Info, "Authentication"); +``` + +### Manual Logging + +```csharp +// Simple log +await logClient.LogAsync(new LogEntry +{ + Level = LogLevel.Information, + Category = "Startup", + Message = "Application started successfully" +}); + +// Log with properties +await logClient.LogAsync(new LogEntry +{ + Level = LogLevel.Information, + Category = "UserAction", + Message = "User performed action", + UserId = "user123", + Properties = new Dictionary + { + ["Action"] = "Purchase", + ["Amount"] = 99.99, + ["ProductId"] = "prod-456" + } +}); + +// Log exception +try +{ + // Your code + throw new Exception("Something went wrong"); +} +catch (Exception ex) +{ + await logClient.LogExceptionAsync(ex, "Error processing order", + new Dictionary + { + ["OrderId"] = "12345", + ["CustomerId"] = "cust-789" + }); +} +``` + +### Security Event Logging + +```csharp +await logClient.LogSecurityEventAsync( + "LoginAttempt", + "Failed login attempt detected", + new Dictionary + { + ["Username"] = "admin", + ["IPAddress"] = "192.168.1.100", + ["Attempts"] = 5 + } +); + +await logClient.LogSecurityEventAsync( + "UnauthorizedAccess", + "Unauthorized API access attempt", + new Dictionary + { + ["Endpoint"] = "/api/admin/users", + ["Method"] = "DELETE", + ["UserId"] = "user456" + } +); +``` + +### Analytics Logging + +```csharp +// Track user events +await logClient.LogAnalyticsAsync("PageView", + new Dictionary + { + ["Page"] = "/products/electronics", + ["Duration"] = 45.2, + ["Source"] = "Google" + } +); + +await logClient.LogAnalyticsAsync("Purchase", + new Dictionary + { + ["ProductId"] = "prod-123", + ["Price"] = 299.99, + ["Category"] = "Electronics", + ["PaymentMethod"] = "CreditCard" + } +); + +await logClient.LogAnalyticsAsync("FeatureUsage", + new Dictionary + { + ["Feature"] = "DarkMode", + ["Enabled"] = true, + ["Platform"] = "iOS" + } +); +``` + +### ASP.NET Core Integration + +```csharp +// Program.cs or Startup.cs +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Register LogCentral + var logCentralOptions = new LogCentralOptions + { + ServerUrl = builder.Configuration["LogCentral:ServerUrl"], + ApiKey = builder.Configuration["LogCentral:ApiKey"], + ApplicationName = "MyWebApp", + ApplicationVersion = "1.0.0", + Environment = builder.Environment.EnvironmentName + }; + + var logClient = new LogCentralClient(logCentralOptions); + builder.Services.AddSingleton(logClient); + + var app = builder.Build(); + + // Use middleware to log requests + app.Use(async (context, next) => + { + var requestId = Guid.NewGuid().ToString(); + + await logClient.LogAsync(new LogEntry + { + Level = LogLevel.Information, + Category = "HTTP", + Message = $"{context.Request.Method} {context.Request.Path}", + RequestId = requestId, + Properties = new Dictionary + { + ["Method"] = context.Request.Method, + ["Path"] = context.Request.Path.Value, + ["QueryString"] = context.Request.QueryString.Value + } + }); + + await next(); + }); + + app.Run(); + } +} +``` + +### Windows Service / Console App + +```csharp +using EonaCat.LogStack.LogClient; +using Microsoft.Extensions.Hosting; + +public class Worker : BackgroundService +{ + private readonly LogCentralClient _logClient; + + public Worker(LogCentralClient logClient) + { + _logClient = logClient; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _logClient.LogAsync(new LogEntry + { + Level = LogLevel.Information, + Category = "Service", + Message = "Worker service started" + }); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + // Your work here + await Task.Delay(1000, stoppingToken); + } + catch (Exception ex) + { + await _logClient.LogExceptionAsync(ex, "Error in worker"); + } + } + + await _logClient.FlushAndDisposeAsync(); + } +} +``` + +### WPF / WinForms Application + +```csharp +public partial class MainWindow : Window +{ + private readonly LogCentralClient _logClient; + + public MainWindow() + { + InitializeComponent(); + + _logClient = new LogCentralClient(new LogCentralOptions + { + ServerUrl = "https://logs.mycompany.com", + ApiKey = "your-api-key", + ApplicationName = "MyDesktopApp", + ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(), + Environment = "Production" + }); + + Application.Current.DispatcherUnhandledException += OnUnhandledException; + } + + private async void OnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + await _logClient.LogExceptionAsync(e.Exception, "Unhandled exception in UI"); + e.Handled = true; + } + + protected override async void OnClosing(CancelEventArgs e) + { + await _logClient.FlushAndDisposeAsync(); + base.OnClosing(e); + } +} +``` + +## 🎯 Advanced Features + +### Correlation IDs for Distributed Tracing + +```csharp +var correlationId = Guid.NewGuid().ToString(); + +await logClient.LogAsync(new LogEntry +{ + Level = LogLevel.Information, + Category = "OrderProcessing", + Message = "Order created", + CorrelationId = correlationId, + Properties = new Dictionary { ["OrderId"] = "12345" } +}); + +// In another service +await logClient.LogAsync(new LogEntry +{ + Level = LogLevel.Information, + Category = "PaymentProcessing", + Message = "Payment processed", + CorrelationId = correlationId, // Same ID + Properties = new Dictionary { ["Amount"] = 99.99 } +}); +``` + +### Performance Monitoring + +```csharp +var stopwatch = Stopwatch.StartNew(); + +try +{ + // Your operation + await SomeSlowOperation(); +} +finally +{ + stopwatch.Stop(); + + await logClient.LogAsync(new LogEntry + { + Level = LogLevel.Information, + Category = "Performance", + Message = "Operation completed", + Properties = new Dictionary + { + ["Operation"] = "DatabaseQuery", + ["DurationMs"] = stopwatch.ElapsedMilliseconds, + ["Status"] = "Success" + } + }); +} +``` + +## 📊 Dashboard Features + +- **Real-time monitoring**: Auto-refreshes every 30 seconds +- **Advanced search**: Full-text search across all log fields +- **Filtering**: By application, environment, level, date range +- **Charts**: Visual representation of log levels and trends +- **Export**: Download logs as CSV or JSON +- **Alerts**: Configure notifications for critical events (planned) + +## 🔒 Security Best Practices + +1. **Use HTTPS** for production deployments +2. **Rotate API keys** regularly +3. **Limit API key permissions** by application +4. **Store API keys** in secure configuration (Azure Key Vault, AWS Secrets Manager) +5. **Enable authentication** for dashboard access (add authentication middleware) + +## 🚀 Deployment + +### Docker Deployment + +```dockerfile +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "EonaCat.LogStack.LogServer.dll"] +``` + +### Azure Deployment + +```bash +az webapp create --resource-group MyResourceGroup --plan MyPlan --name logcentral --runtime "DOTNETCORE:8.0" +az webapp deployment source config-zip --resource-group MyResourceGroup --name logcentral --src logcentral.zip +``` + +## 📈 Scalability + +For high-volume applications: + +1. Use **Redis** for caching +2. Implement **Elasticsearch** for faster searches +3. Use **message queues** (RabbitMQ, Azure Service Bus) for async processing +4. Partition database by date ranges +5. Implement log archival and retention policies \ No newline at end of file diff --git a/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj b/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj new file mode 100644 index 0000000..5a2fcd7 --- /dev/null +++ b/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.1 + enable + True + EonaCat.LogStack.OpenTelemetryFlow + EonaCat.LogStack.OpenTelemetryFlow + 0.0.1 + EonaCat.LogStack.OpenTelemetryFlow + EonaCat (Jeroen Saey) + EonaCat OpenTelemetry Flow for LogStack + EonaCat (Jeroen Saey) + https://git.saey.me/EonaCat/EonaCat.LogStack + icon.png + README.md + https://git.saey.me/EonaCat/EonaCat.LogStack + git + EonaCat; OpenTelemetry; Flow; LogStack; Logging; Jeroen;Saey + README.md + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + + diff --git a/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs b/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs new file mode 100644 index 0000000..0cbe295 --- /dev/null +++ b/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs @@ -0,0 +1,33 @@ +using EonaCat.LogStack.Configuration; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using OpenTelemetry.Exporter; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EonaCat.LogStack.Flows.WindowsEventLog +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public static class EonaCatLogStackExtensions + { + /// + /// Write to OpenTelemetry + /// + public static LogBuilder WriteToOpenTelemetry(this LogBuilder logBuilder, + string serviceName, + Uri endpoint, + OtlpExportProtocol protocol = OtlpExportProtocol.Grpc, + LogLevel minimumLevel = LogLevel.Trace) + { + logBuilder.AddFlow(new OpenTelemetryFlow( + serviceName, + endpoint, + protocol, + minimumLevel)); + return logBuilder; + } + } +} diff --git a/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs b/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs new file mode 100644 index 0000000..b1519d9 --- /dev/null +++ b/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs @@ -0,0 +1,162 @@ +using EonaCat.LogStack.Core; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using LogLevel = EonaCat.LogStack.Core.LogLevel; + +namespace EonaCat.LogStack.Flows +{ + public sealed class OpenTelemetryFlow : FlowBase + { + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public OpenTelemetryFlow(string serviceName, Uri endpoint, OtlpExportProtocol protocol = OtlpExportProtocol.Grpc, LogLevel minimumLevel = LogLevel.Trace) : base("OpenTelemetry:" + serviceName, minimumLevel) + { + if (string.IsNullOrWhiteSpace(serviceName)) + { + throw new ArgumentNullException(nameof(serviceName)); + } + + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + + _loggerFactory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + + builder.AddOpenTelemetry(options => + { + options.SetResourceBuilder( + ResourceBuilder.CreateDefault() + .AddService(serviceName) + .AddAttributes(new Dictionary + { + ["host.name"] = Environment.MachineName, + ["process.id"] = Process.GetCurrentProcess().Id + })); + + options.AddOtlpExporter(otlp => + { + otlp.Endpoint = endpoint; + otlp.Protocol = protocol; + }); + + options.IncludeScopes = true; + options.IncludeFormattedMessage = true; + options.ParseStateValues = true; + }); + }); + + _logger = _loggerFactory.CreateLogger(serviceName); + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + WriteLog(logEvent); + Interlocked.Increment(ref BlastedCount); + + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (var e in logEvents.Span) + { + if (e.Level < MinimumLevel) + { + continue; + } + + WriteLog(e); + Interlocked.Increment(ref BlastedCount); + } + + return Task.FromResult(WriteResult.Success); + } + + private void WriteLog(LogEvent log) + { + var state = new List>(); + + if (!string.IsNullOrEmpty(log.Category)) + { + state.Add(new KeyValuePair("category", log.Category)); + } + + foreach (var prop in log.Properties) + { + state.Add(new KeyValuePair(prop.Key, prop.Value ?? "null")); + } + + if (log.Exception != null) + { + state.Add(new KeyValuePair("exception.type", log.Exception.GetType().FullName)); + state.Add(new KeyValuePair("exception.message", log.Exception.Message)); + state.Add(new KeyValuePair("exception.stacktrace", log.Exception.StackTrace)); + } + + _logger.Log( + MapLevel(log.Level), + new EventId(0, log.Category), + state, + log.Exception, + (s, e) => log.Message.ToString()); + } + + private static Microsoft.Extensions.Logging.LogLevel MapLevel(LogLevel level) + { + return level switch + { + LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace, + LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, + LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, + LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, + LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, + _ => Microsoft.Extensions.Logging.LogLevel.Information + }; + } + + public override async ValueTask DisposeAsync() + { + if (!IsEnabled) + { + return; + } + + IsEnabled = false; + + _loggerFactory?.Dispose(); + + await base.DisposeAsync().ConfigureAwait(false); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj b/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj new file mode 100644 index 0000000..af014cb --- /dev/null +++ b/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/EonaCat.LogStack.SerilogTest/Program.cs b/EonaCat.LogStack.SerilogTest/Program.cs new file mode 100644 index 0000000..3ecb053 --- /dev/null +++ b/EonaCat.LogStack.SerilogTest/Program.cs @@ -0,0 +1,169 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Json; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +var builder = WebApplication.CreateBuilder(args); + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("Id", "TEST") + .Enrich.WithProperty("AppName", "[ALL YOUR BASE ARE BELONG TO US!]") + .WriteTo.Async(a => a.Console()) + .WriteTo.Async(a => a.File( + path: "logs/web-.log", + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 1_000_000, + rollOnFileSizeLimit: true, + retainedFileCountLimit: 5, + shared: true)) + .WriteTo.Async(a => a.File( + new JsonFormatter(), + path: "logs/test.json", + rollingInterval: RollingInterval.Day)) + //.WriteTo.Seq("http://localhost:5341") // central logging + .CreateLogger(); + +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "keys"))) + .SetApplicationName("SerilogStressTest"); + +builder.Services.AddRazorPages(); + +builder.WebHost.ConfigureKestrel(options => +{ + options.ListenAnyIP(6000); +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthorization(); +app.MapRazorPages(); + +// Run tests +_ = Task.Run(RunLoggingTestsAsync); +_ = Task.Run(RunWebLoggingTestsAsync); +_ = Task.Run(RunLoggingExceptionTests); +_ = Task.Run(RunWebLoggingExceptionTests); +//_ = Task.Run(RunMemoryLeakTest); +_ = Task.Run(RunTcpLoggerTest); + +app.Run(); + + +async Task RunLoggingTestsAsync() +{ + for (var i = 0; i < 9_000_000; i++) + { + Log.Information("test to file {i} INFO", i); + Log.Fatal("test to file {i} CRITICAL", i); + Log.Debug("test to file {i} DEBUG", i); + Log.Error("test to file {i} ERROR", i); + Log.Verbose("test to file {i} TRACE", i); + Log.Warning("test to file {i} WARNING", i); + + Console.WriteLine($"Logged: {i}"); + await Task.Delay(1); + } +} + +async Task RunWebLoggingTestsAsync() +{ + int i = 0; + + while (true) + { + i++; + + Log.Information("web-test {i}", i); + Log.Debug("web-test {i}", i); + Log.Warning("web-test {i}", i); + Log.Error("web-test {i}", i); + Log.Verbose("web-test {i}", i); + + await Task.Delay(1); + } +} + +void RunLoggingExceptionTests() +{ + for (int i = 0; i < 10; i++) + { + try + { + throw new Exception($"Normal Exception {i}"); + } + catch (Exception ex) + { + Log.Error(ex, "Exception {Index}", i); + Console.WriteLine($"Normal ExceptionLogged: {i}"); + } + } +} + +void RunWebLoggingExceptionTests() +{ + for (int i = 0; i < 10; i++) + { + try + { + throw new Exception($"WebException {i}"); + } + catch (Exception ex) + { + Log.Fatal(ex, "CRITICAL"); + Log.Debug(ex, "DEBUG"); + Log.Error(ex, "ERROR"); + Log.Verbose(ex, "TRACE"); + Log.Warning(ex, "WARNING"); + Log.Information(ex, "INFORMATION"); + + Console.WriteLine($"WebExceptionLogged: {i}"); + } + } +} +async Task RunMemoryLeakTest() +{ + var managedLeak = new List(); + + while (true) + { + managedLeak.Add(new byte[5_000_000]); // 5MB + Marshal.AllocHGlobal(10_000_000); // 10MB unmanaged + + await Task.Delay(500); + } +} + +async Task RunTcpLoggerTest() +{ + using var client = new TcpClient(); + + try + { + await client.ConnectAsync("192.168.1.1", 12345); + + int i = 0; + while (true) + { + var message = Encoding.UTF8.GetBytes($"TCP log {++i}\n"); + await client.GetStream().WriteAsync(message); + await Task.Delay(1000); + } + } + catch + { + Log.Warning("TCP server not reachable"); + } +} diff --git a/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json b/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json new file mode 100644 index 0000000..4837432 --- /dev/null +++ b/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "EonaCat.LogStack.SerilogTest": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56815;http://localhost:56816" + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj b/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj new file mode 100644 index 0000000..a347705 --- /dev/null +++ b/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.1 + enable + True + EonaCat.LogStack.Server + EonaCat (Jeroen Saey) + EonaCat.LogStack.Server is a server for the logging library + EonaCat (Jeroen Saey) + https://www.nuget.org/packages/EonaCat.LogStack.Server/ + icon.png + README.md + EonaCat;Logger;EonaCatLogStack;server;Log;Writer;Jeroen;Saey + + + + + True + \ + + + True + \ + + + + diff --git a/EonaCat.LogStack.Server/Server.cs b/EonaCat.LogStack.Server/Server.cs new file mode 100644 index 0000000..c84aa32 --- /dev/null +++ b/EonaCat.LogStack.Server/Server.cs @@ -0,0 +1,287 @@ +using System.IO; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System; +using System.Linq; + +namespace EonaCat.LogStack.Server +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class Server + { + private TcpListener _tcpListener; + private UdpClient _udpListener; + private CancellationTokenSource _cts; + private bool _isRunning; + private readonly bool _useUdp; + private const long MaxLogFileSize = 200 * 1024 * 1024; // 200MB log rollover limit + private readonly int _logRetentionDays; // Number of days to retain logs + private readonly long _maxLogDirectorySize; // Maximum allowed size of the logs directory + private const int UdpBufferSize = 65507; // Maximum UDP packet size (65507 bytes for UDP payload) + + /// + /// EonaCat Log Server + /// + /// Determine if we need to start a udp server (default: true) + /// Max log retention days (default: 30) + /// Max log directory size (default: 10GB) + public Server(bool useUdp = true, int logRetentionDays = 30, long maxLogDirectorySize = 10L * 1024 * 1024 * 1024) // Default 10GB max directory size + { + _useUdp = useUdp; + _logRetentionDays = logRetentionDays; + _maxLogDirectorySize = maxLogDirectorySize; + } + + protected virtual Task ProcessLogAsync(string logData) + { + string logsRootDirectory = "logs"; + + // Create root log directory if it doesn't exist + if (!Directory.Exists(logsRootDirectory)) + { + Directory.CreateDirectory(logsRootDirectory); + } + + // Create a daily directory for logs + string dailyLogsDirectory = Path.Combine(logsRootDirectory, DateTime.Now.ToString("yyyyMMdd")); + if (!Directory.Exists(dailyLogsDirectory)) + { + Directory.CreateDirectory(dailyLogsDirectory); + } + + // Base log file name + string baseLogFilePath = Path.Combine(dailyLogsDirectory, "EonaCatLogs"); + string logFilePath = baseLogFilePath + ".log"; + + int fileIndex = 1; + while (File.Exists(logFilePath) && new FileInfo(logFilePath).Length > MaxLogFileSize) + { + logFilePath = baseLogFilePath + $"_{fileIndex}.log"; + fileIndex++; + } + + // After processing log, check directory size and clean up if needed + CleanUpOldLogs(); + + return File.AppendAllTextAsync(logFilePath, logData + Environment.NewLine); + } + + private void CleanUpOldLogs() + { + string logsRootDirectory = "logs"; + if (!Directory.Exists(logsRootDirectory)) + { + return; + } + + // Delete old directories + foreach (var directory in Directory.GetDirectories(logsRootDirectory)) + { + try + { + DirectoryInfo dirInfo = new DirectoryInfo(directory); + if (dirInfo.CreationTime < DateTime.Now.AddDays(-_logRetentionDays)) + { + Console.WriteLine($"Deleting old log directory: {directory}"); + Directory.Delete(directory, true); // Delete directory and its contents + } + } + catch (Exception ex) + { + Console.WriteLine($"Error deleting old directory {directory}: {ex.Message}"); + } + } + + // Ensure total size of log directory doesn't exceed max limit + long totalDirectorySize = GetDirectorySize(logsRootDirectory); + if (totalDirectorySize > _maxLogDirectorySize) + { + Console.WriteLine("Log directory size exceeded limit, cleaning up..."); + + // Delete the oldest directories until the size limit is met + foreach (var directory in Directory.GetDirectories(logsRootDirectory).OrderBy(d => new DirectoryInfo(d).CreationTime)) + { + try + { + DirectoryInfo dirInfo = new DirectoryInfo(directory); + long dirSize = GetDirectorySize(directory); + totalDirectorySize -= dirSize; + + // Delete the directory if the total size exceeds the limit + Directory.Delete(directory, true); + Console.WriteLine($"Deleted directory: {directory}"); + + // Stop deleting if we are under the size limit + if (totalDirectorySize <= _maxLogDirectorySize) + { + break; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error deleting directory {directory}: {ex.Message}"); + } + } + } + } + + private long GetDirectorySize(string directory) + { + long size = 0; + try + { + // Add size of files in the directory + size += Directory.GetFiles(directory).Sum(file => new FileInfo(file).Length); + + // Add size of files in subdirectories + foreach (var subdirectory in Directory.GetDirectories(directory)) + { + size += GetDirectorySize(subdirectory); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error calculating size for directory {directory}: {ex.Message}"); + } + return size; + } + + public async Task Start(IPAddress ipAddress = null, int port = 5555) + { + if (ipAddress == null) + { + ipAddress = IPAddress.Any; + } + + _cts = new CancellationTokenSource(); + _isRunning = true; + + if (_useUdp) + { + _udpListener = new UdpClient(port); + Console.WriteLine($"EonaCat UDP Log Server started on port {port}..."); + await ListenUdpAsync(); + } + else + { + _tcpListener = new TcpListener(ipAddress, port); + _tcpListener.Start(); + Console.WriteLine($"EonaCat TCP Log Server started on port {port}..."); + await ListenTcpAsync(); + } + } + + private async Task ListenTcpAsync() + { + try + { + while (!_cts.Token.IsCancellationRequested) + { + TcpClient client = await _tcpListener.AcceptTcpClientAsync(); + _ = Task.Run(() => HandleTcpClient(client)); + } + } + catch (OperationCanceledException) + { + Console.WriteLine("TCP Server stopping..."); + } + } + + private async Task ListenUdpAsync() + { + try + { + while (!_cts.Token.IsCancellationRequested) + { + // Increased buffer size for UDP + UdpReceiveResult result = await _udpListener.ReceiveAsync(); + string logData = Encoding.UTF8.GetString(result.Buffer); + + // If the received data is too large, process it in chunks + if (result.Buffer.Length > UdpBufferSize) + { + // Handle fragmentation and reassembly (this is a basic placeholder logic) + Console.WriteLine("Received large UDP data. Handling fragmentation."); + await ProcessLargeDataAsync(result.Buffer); + } + else + { + Console.WriteLine($"Received UDP Log: {logData}"); + await ProcessLogAsync(logData); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("UDP Server stopping..."); + } + } + + private async Task ProcessLargeDataAsync(byte[] data) + { + // You can implement your own logic here for processing large UDP data, such as fragmentation handling + string largeDataString = Encoding.UTF8.GetString(data); + await ProcessLogAsync(largeDataString); + } + + public void Stop() + { + if (_isRunning) + { + _cts.Cancel(); + + // Proper cleanup of resources + _cts.Dispose(); + if (_useUdp) + { + _udpListener?.Close(); + _udpListener?.Dispose(); + } + else + { + _tcpListener?.Stop(); + _tcpListener?.Server?.Dispose(); // Dispose of the socket (if any) + } + + _isRunning = false; + Console.WriteLine("EonaCat Log Server stopped."); + } + } + + private async Task HandleTcpClient(TcpClient client) + { + try + { + using (NetworkStream stream = client.GetStream()) + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + char[] buffer = new char[8192]; // 8KB buffer size for large data + int bytesRead; + StringBuilder logData = new StringBuilder(); + + while ((bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + logData.Append(new string(buffer, 0, bytesRead)); + } + + Console.WriteLine($"Received TCP Log: {logData.ToString()}"); + await ProcessLogAsync(logData.ToString()); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + finally + { + // Ensure client is properly disposed + client.Close(); + client.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.Status/Controllers/ApiController.cs b/EonaCat.LogStack.Status/Controllers/ApiController.cs new file mode 100644 index 0000000..c6d1d06 --- /dev/null +++ b/EonaCat.LogStack.Status/Controllers/ApiController.cs @@ -0,0 +1,554 @@ +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace EonaCat.LogStack.Status.Controllers; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +[ApiController] +[Route("api")] +public class ApiController : ControllerBase +{ + private readonly DatabaseContext _database; + private readonly MonitoringService _monitorService; + private readonly IngestionService _ingestionService; + + public ApiController(DatabaseContext database, MonitoringService monitorService, IngestionService ingestionService) + { + _database = database; + _monitorService = monitorService; + _ingestionService = ingestionService; + } + + [HttpGet("status/summary")] + public async Task GetSummary() + { + var isAdmin = IsAdmin(); + var stats = await _monitorService.GetStatsAsync(isAdmin); + return Ok(stats); + } + + [HttpGet("monitors")] + public async Task GetMonitors() + { + var isAdmin = IsAdmin(); + var query = _database.Monitors.Where(m => m.IsActive); + if (!isAdmin) + { + query = query.Where(m => m.IsPublic); + } + + return Ok(await query.ToListAsync()); + } + + [HttpGet("monitors/{id}")] + public async Task GetMonitor(int id) + { + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + if (!IsAdmin() && !monitor.IsPublic) + { + return Unauthorized(); + } + + return Ok(monitor); + } + + [HttpGet("monitors/{id}/check")] + public async Task CheckMonitor(int id) + { + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + var check = await _monitorService.CheckMonitorAsync(monitor); + return Ok(check); + } + + /// Returns the last N checks for a monitor (default 100). + [HttpGet("monitors/{id}/history")] + public async Task GetMonitorHistory(int id, [FromQuery] int limit = 100) + { + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + if (!IsAdmin() && !monitor.IsPublic) + { + return Unauthorized(); + } + + var checks = await _database.MonitorChecks + .Where(c => c.MonitorId == id) + .OrderByDescending(c => c.CheckedAt) + .Take(Math.Clamp(limit, 1, 1000)) + .ToListAsync(); + + return Ok(checks); + } + + /// Returns uptime percentages over 24h / 7d / 30d windows. + [HttpGet("monitors/{id}/uptime")] + public async Task GetMonitorUptime(int id) + { + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + if (!IsAdmin() && !monitor.IsPublic) + { + return Unauthorized(); + } + + var report = await _monitorService.GetUptimeReportAsync(id); + return Ok(report); + } + + /// Pause or resume a monitor (admin only). + [HttpPost("monitors/{id}/pause")] + public async Task PauseMonitor(int id) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + monitor.IsActive = false; + await _database.SaveChangesAsync(); + return Ok(new { id, active = false }); + } + + [HttpPost("monitors/{id}/resume")] + public async Task ResumeMonitor(int id) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + monitor.IsActive = true; + monitor.LastChecked = null; // force immediate re-check + await _database.SaveChangesAsync(); + return Ok(new { id, active = true }); + } + + [HttpGet("monitors/{id}/alerts")] + public async Task GetAlertRules(int id) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var rules = await _database.AlertRules.Where(r => r.MonitorId == id).ToListAsync(); + return Ok(rules); + } + + [HttpPost("monitors/{id}/alerts")] + public async Task CreateAlertRule(int id, [FromBody] AlertRule rule) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var monitor = await _database.Monitors.FindAsync(id); + if (monitor == null) + { + return NotFound(); + } + + rule.MonitorId = id; + _database.AlertRules.Add(rule); + await _database.SaveChangesAsync(); + return Ok(rule); + } + + [HttpDelete("alerts/{ruleId}")] + public async Task DeleteAlertRule(int ruleId) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var rule = await _database.AlertRules.FindAsync(ruleId); + if (rule == null) + { + return NotFound(); + } + + _database.AlertRules.Remove(rule); + await _database.SaveChangesAsync(); + return Ok(new { deleted = ruleId }); + } + + /// List incidents (admin sees all; public sees only IsPublic incidents). + [HttpGet("incidents")] + public async Task GetIncidents([FromQuery] bool activeOnly = false) + { + var isAdmin = IsAdmin(); + var query = _database.Incidents.Include(i => i.Updates).AsQueryable(); + if (!isAdmin) + { + query = query.Where(i => i.IsPublic); + } + + if (activeOnly) + { + query = query.Where(i => i.Status != IncidentStatus.Resolved); + } + + var list = await query.OrderByDescending(i => i.CreatedAt).ToListAsync(); + return Ok(list); + } + + [HttpGet("incidents/{id}")] + public async Task GetIncident(int id) + { + var incident = await _database.Incidents.Include(i => i.Updates).FirstOrDefaultAsync(i => i.Id == id); + if (incident == null) + { + return NotFound(); + } + + if (!IsAdmin() && !incident.IsPublic) + { + return Unauthorized(); + } + + return Ok(incident); + } + + [HttpPost("incidents")] + public async Task CreateIncident([FromBody] Incident incident) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + incident.CreatedAt = DateTime.UtcNow; + incident.UpdatedAt = DateTime.UtcNow; + _database.Incidents.Add(incident); + await _database.SaveChangesAsync(); + return Ok(incident); + } + + [HttpPatch("incidents/{id}")] + public async Task UpdateIncident(int id, [FromBody] IncidentPatchDto patch) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var incident = await _database.Incidents.FindAsync(id); + if (incident == null) + { + return NotFound(); + } + + if (patch.Status.HasValue) + { + incident.Status = patch.Status.Value; + if (patch.Status == IncidentStatus.Resolved) + { + incident.ResolvedAt = DateTime.UtcNow; + } + } + if (patch.Severity.HasValue) + { + incident.Severity = patch.Severity.Value; + } + + if (patch.Body != null) + { + incident.Body = patch.Body; + } + + incident.UpdatedAt = DateTime.UtcNow; + + if (!string.IsNullOrWhiteSpace(patch.UpdateMessage)) + { + _database.IncidentUpdates.Add(new IncidentUpdate + { + IncidentId = id, + Message = patch.UpdateMessage, + Status = patch.Status ?? incident.Status + }); + } + + await _database.SaveChangesAsync(); + return Ok(incident); + } + + [HttpDelete("incidents/{id}")] + public async Task DeleteIncident(int id) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var incident = await _database.Incidents.FindAsync(id); + if (incident == null) + { + return NotFound(); + } + + _database.Incidents.Remove(incident); + await _database.SaveChangesAsync(); + return Ok(new { deleted = id }); + } + + [HttpGet("logs")] + public async Task QueryLogs( + [FromQuery] string? level, + [FromQuery] string? source, + [FromQuery] string? search, + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 100) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var query = _database.Logs.AsQueryable(); + if (!string.IsNullOrWhiteSpace(level)) + { + query = query.Where(x => x.Level == level.ToLower()); + } + + if (!string.IsNullOrWhiteSpace(source)) + { + query = query.Where(x => x.Source == source); + } + + if (!string.IsNullOrWhiteSpace(search)) + { + query = query.Where(x => x.Message.Contains(search)); + } + + if (from.HasValue) + { + query = query.Where(x => x.Timestamp >= from.Value); + } + + if (to.HasValue) + { + query = query.Where(x => x.Timestamp <= to.Value); + } + + var total = await query.CountAsync(); + var entries = await query + .OrderByDescending(x => x.Timestamp) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return Ok(new { total, page, pageSize, entries }); + } + + /// Returns hourly log volume buckets for charting. + [HttpGet("logs/stats")] + public async Task GetLogStats([FromQuery] int hours = 24) + { + if (!IsAdmin()) + { + return Unauthorized(); + } + + var buckets = await _monitorService.GetLogStatsAsync(Math.Clamp(hours, 1, 168)); + return Ok(buckets); + } + + [HttpPost("logs/ingest")] + public async Task IngestLog([FromBody] LogEntry entry) + { + if (string.IsNullOrWhiteSpace(entry.Message)) + { + return BadRequest("message required"); + } + + entry.Level = (entry.Level ?? "info").ToLower(); + entry.Timestamp = DateTime.UtcNow; + await _ingestionService.IngestAsync(entry); + return Ok(new { success = true }); + } + + [HttpPost("logs/batch")] + public async Task IngestBatch([FromBody] List entries) + { + if (entries == null || !entries.Any()) + { + return BadRequest("entries required"); + } + + foreach (var entry in entries) + { + entry.Level = (entry.Level ?? "info").ToLower(); + if (entry.Timestamp == default) + { + entry.Timestamp = DateTime.UtcNow; + } + } + await _ingestionService.IngestBatchAsync(entries); + return Ok(new { success = true, count = entries.Count }); + } + + [HttpPost("logs/eonacat")] + public async Task IngestEonaCat([FromBody] object[] events) + { + var entries = new List(); + foreach (var evtObj in events) + { + var json = JsonSerializer.Serialize(evtObj); + var dict = JsonSerializer.Deserialize>(json)!; + var logEntry = new LogEntry + { + Source = dict.TryGetValue("properties", out var props) && + props.ValueKind == JsonValueKind.Object && + props.Deserialize>()!.TryGetValue("Application", out var appObj) + ? appObj?.ToString() ?? "EonaCat.LogStack" : "EonaCat.LogStack", + Level = dict.TryGetValue("level", out var level) ? level.GetString() ?? "Info" : "Info", + Message = dict.TryGetValue("message", out var msg) ? msg.GetString() ?? "" : "", + Exception = dict.TryGetValue("exception", out var ex) ? ex.ToString() : null, + Host = dict.TryGetValue("host", out var host) ? host.GetString() : null, + TraceId = dict.TryGetValue("traceId", out var traceId) ? traceId.GetString() : null, + Properties = dict.TryGetValue("properties", out var properties) ? properties.GetRawText() : null, + Timestamp = dict.TryGetValue("timestamp", out var ts) && DateTime.TryParse(ts.GetString(), out var dt) ? dt : DateTime.UtcNow + }; + logEntry.Level = MapEonaCatLevel(logEntry.Level); + entries.Add(logEntry); + } + if (entries.Any()) + { + await _ingestionService.IngestBatchAsync(entries); + } + + return Ok(new { success = true, count = entries.Count }); + } + + [HttpPost("logs/serilog")] + public async Task IngestSerilog([FromBody] SerilogPayload payload) + { + var entries = payload.Events?.Select(e => new LogEntry + { + Source = e.Properties?.TryGetValue("Application", out var app) == true ? app?.ToString() ?? "serilog" : "serilog", + Level = MapSerilogLevel(e.Level), + Message = e.RenderedMessage ?? e.MessageTemplate ?? "", + Exception = e.Exception, + Properties = e.Properties != null ? JsonSerializer.Serialize(e.Properties) : null, + Timestamp = e.Timestamp == default ? DateTime.UtcNow : e.Timestamp + }).ToList() ?? new List(); + + if (entries.Any()) + { + await _ingestionService.IngestBatchAsync(entries); + } + + return Ok(new { success = true, count = entries.Count }); + } + + private bool IsAdmin() => HttpContext.Session.GetString("IsAdmin") == "true"; + + private static string MapSerilogLevel(string? l) => l?.ToLower() switch + { + "verbose" or "debug" => "debug", + "information" => "info", + "warning" => "warn", + "error" => "error", + "fatal" => "critical", + _ => "info" + }; + + private static string MapEonaCatLevel(string? l) => l?.ToLower() switch + { + "trace" or "debug" => "debug", + "information" => "info", + "warning" => "warn", + "error" => "error", + "critical" => "critical", + _ => "info" + }; +} + +public class IncidentPatchDto +{ + public IncidentStatus? Status { get; set; } + public IncidentSeverity? Severity { get; set; } + public string? Body { get; set; } + /// If provided, a new IncidentUpdate is appended with this message. + public string? UpdateMessage { get; set; } +} + +public class EonaCatPayLoad +{ + public List? Events { get; set; } +} + +public class EonaCatLogEvent +{ + public string Timestamp { get; set; } = default!; + public string Level { get; set; } = default!; + public string Message { get; set; } = default!; + public string Category { get; set; } = default!; + public int ThreadId { get; set; } + public string? TraceId { get; set; } + public string? SpanId { get; set; } + public ExceptionDto? Exception { get; set; } + public Dictionary? Properties { get; set; } +} + +public class ExceptionDto +{ + public string Type { get; set; } = default!; + public string Message { get; set; } = default!; + public string? StackTrace { get; set; } +} + +public class SerilogPayload +{ + public List? Events { get; set; } +} + +public class SerilogEvent +{ + public DateTime Timestamp { get; set; } + public string? Level { get; set; } + public string? MessageTemplate { get; set; } + public string? RenderedMessage { get; set; } + public string? Exception { get; set; } + public Dictionary? Properties { get; set; } +} diff --git a/EonaCat.LogStack.Status/Data/DatabaseContext.cs b/EonaCat.LogStack.Status/Data/DatabaseContext.cs new file mode 100644 index 0000000..c4091cc --- /dev/null +++ b/EonaCat.LogStack.Status/Data/DatabaseContext.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Models; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Data; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class DatabaseContext : DbContext +{ + public DatabaseContext(DbContextOptions options) : base(options) { } + + public DbSet Monitors => Set(); + public DbSet MonitorChecks => Set(); + public DbSet Certificates => Set(); + public DbSet Logs => Set(); + public DbSet Settings => Set(); + + public DbSet Incidents => Set(); + public DbSet IncidentUpdates => Set(); + public DbSet AlertRules => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // existing indexes + modelBuilder.Entity().HasIndex(c => new { c.MonitorId, c.CheckedAt }); + modelBuilder.Entity().HasIndex(l => l.Timestamp); + modelBuilder.Entity().HasIndex(l => new { l.Level, l.Source }); + modelBuilder.Entity().HasIndex(s => s.Key).IsUnique(); + + // incident indexes + modelBuilder.Entity().HasIndex(i => i.Status); + modelBuilder.Entity().HasIndex(i => i.CreatedAt); + modelBuilder.Entity().HasIndex(u => u.IncidentId); + + // alert rule indexes + modelBuilder.Entity().HasIndex(a => a.MonitorId); + + // relationships + modelBuilder.Entity() + .HasMany(i => i.Updates) + .WithOne(u => u.Incident) + .HasForeignKey(u => u.IncidentId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasMany(m => m.AlertRules) + .WithOne(a => a.Monitor) + .HasForeignKey(a => a.MonitorId) + .OnDelete(DeleteBehavior.Cascade); + + // Seed default settings + modelBuilder.Entity().HasData( + new AppSettings { Id = 1, Key = "AdminPasswordHash", Value = BCrypt.Net.BCrypt.EnhancedHashPassword("adminEonaCat") }, + new AppSettings { Id = 2, Key = "SiteName", Value = "Status" }, + new AppSettings { Id = 3, Key = "ShowLogsPublicly", Value = "false" }, + new AppSettings { Id = 4, Key = "ShowUptimePublicly", Value = "true" }, + new AppSettings { Id = 5, Key = "MaxLogRetentionDays", Value = "30" }, + new AppSettings { Id = 6, Key = "AlertEmail", Value = "" }, + // new settings + new AppSettings { Id = 7, Key = "AlertWebhookUrl", Value = "" }, + new AppSettings { Id = 8, Key = "ShowIncidentsPublicly", Value = "true" }, + new AppSettings { Id = 9, Key = "AutoCreateIncidents", Value = "false" } + ); + } +} diff --git a/EonaCat.LogStack.Status/EonaCat.LogStack.Status.csproj b/EonaCat.LogStack.Status/EonaCat.LogStack.Status.csproj new file mode 100644 index 0000000..fc3d121 --- /dev/null +++ b/EonaCat.LogStack.Status/EonaCat.LogStack.Status.csproj @@ -0,0 +1,14 @@ + + + net8.0 + enable + enable + Status + False + + + + + + + diff --git a/EonaCat.LogStack.Status/EonaCat.LogStack.Status.db b/EonaCat.LogStack.Status/EonaCat.LogStack.Status.db new file mode 100644 index 0000000000000000000000000000000000000000..9a472209435d88229f65ab7a0ca30d67df87a462 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WY9}{#S79dK(-m98b?E5 nGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nC=3ArNJIx= literal 0 HcmV?d00001 diff --git a/EonaCat.LogStack.Status/EonaCat.LogStack.Status.db-shm b/EonaCat.LogStack.Status/EonaCat.LogStack.Status.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..25b3c2926009f10003e0dff78b7f792eea164462 GIT binary patch literal 32768 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 qSfE~bfa-3THpCZj2Sm|C7{c~)m<2yC_Number of consecutive failing checks since the monitor last went down. + public int ConsecutiveFailures { get; set; } = 0; + + /// How many consecutive failures before the monitor is marked Down (default: 1). + public int FailureThreshold { get; set; } = 1; + + /// Optional expected HTTP keyword in the response body (HTTP/HTTPS monitors). + public string? ExpectedKeyword { get; set; } + + /// Optional expected HTTP status code (HTTP/HTTPS monitors, default: any 2xx/3xx). + public int? ExpectedStatusCode { get; set; } + + public ICollection Checks { get; set; } = new List(); + public ICollection AlertRules { get; set; } = new List(); +} + +public class MonitorCheck +{ + public int Id { get; set; } + public int MonitorId { get; set; } + public Monitor? Monitor { get; set; } + public DateTime CheckedAt { get; set; } = DateTime.UtcNow; + public MonitorStatus Status { get; set; } + public double ResponseMs { get; set; } + public string? Message { get; set; } +} + +public class CertificateEntry +{ + public int Id { get; set; } + [Required] public string Name { get; set; } = ""; + [Required] public string Domain { get; set; } = ""; + public int Port { get; set; } = 443; + public DateTime? ExpiresAt { get; set; } + public DateTime? IssuedAt { get; set; } + public string? Issuer { get; set; } + public string? Subject { get; set; } + public string? Thumbprint { get; set; } + public bool IsPublic { get; set; } = true; + public bool AlertOnExpiry { get; set; } = true; + public int AlertDaysBeforeExpiry { get; set; } = 30; + public DateTime? LastChecked { get; set; } + public string? LastError { get; set; } +} + +public class LogEntry +{ + public int Id { get; set; } + public string Source { get; set; } = "system"; + public string Level { get; set; } = "info"; + public string Message { get; set; } = ""; + public string? Properties { get; set; } + public string? Exception { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public string? TraceId { get; set; } + public string? Host { get; set; } +} + +/// +/// Represents a manually-posted or auto-generated incident that is displayed +/// on the public status page to communicate outages and maintenance. +/// +public class Incident +{ + public int Id { get; set; } + [Required] public string Title { get; set; } = ""; + public string? Body { get; set; } + public IncidentSeverity Severity { get; set; } = IncidentSeverity.Minor; + public IncidentStatus Status { get; set; } = IncidentStatus.Investigating; + public int? MonitorId { get; set; } + public Monitor? Monitor { get; set; } + public bool IsPublic { get; set; } = true; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ResolvedAt { get; set; } + public ICollection Updates { get; set; } = new List(); +} + +/// +/// A timestamped update appended to an incident (visible on the public page). +/// +public class IncidentUpdate +{ + public int Id { get; set; } + public int IncidentId { get; set; } + public Incident? Incident { get; set; } + public string Message { get; set; } = ""; + public IncidentStatus Status { get; set; } + public DateTime PostedAt { get; set; } = DateTime.UtcNow; +} + +/// +/// A configurable alert rule attached to a monitor or applied globally. +/// When the condition is met, a notification is dispatched (e-mail / webhook). +/// +public class AlertRule +{ + public int Id { get; set; } + public int? MonitorId { get; set; } + public Monitor? Monitor { get; set; } + public AlertRuleCondition Condition { get; set; } + /// Threshold value used by and . + public double? ThresholdValue { get; set; } + /// Webhook URL to POST a JSON payload to when the rule fires (optional). + public string? WebhookUrl { get; set; } + public bool IsEnabled { get; set; } = true; + public DateTime? LastFiredAt { get; set; } + /// Minimum minutes between repeated firings (0 = every check). + public int CooldownMinutes { get; set; } = 10; +} + +public class AppSettings +{ + public int Id { get; set; } + public string Key { get; set; } = ""; + public string Value { get; set; } = ""; +} + +public class LogFilter +{ + public string? Level { get; set; } + public string? Source { get; set; } + public string? Search { get; set; } + public DateTime? From { get; set; } + public DateTime? To { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 100; +} + +public class DashboardStats +{ + public int TotalMonitors { get; set; } + public int UpCount { get; set; } + public int DownCount { get; set; } + public int WarnCount { get; set; } + public int UnknownCount { get; set; } + public int CertCount { get; set; } + public int CertExpiringSoon { get; set; } + public int CertExpired { get; set; } + public long TotalLogs { get; set; } + public long ErrorLogs { get; set; } + public double OverallUptime { get; set; } + public int ActiveIncidents { get; set; } + public int ResolvedIncidents { get; set; } +} + +/// Uptime percentage over a configurable window. +public class UptimeReport +{ + public int MonitorId { get; set; } + public string MonitorName { get; set; } = ""; + public double Uptime24h { get; set; } + public double Uptime7d { get; set; } + public double Uptime30d { get; set; } + public int TotalChecks { get; set; } + public int UpChecks { get; set; } + public int DownChecks { get; set; } + public double AvgResponseMs { get; set; } +} + +/// Log volume aggregated per hour / day for charting. +public class LogStatsBucket +{ + public DateTime BucketStart { get; set; } + public long Total { get; set; } + public long Errors { get; set; } + public long Warnings { get; set; } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml b/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml new file mode 100644 index 0000000..6a7a449 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml @@ -0,0 +1,118 @@ +@page +@model Status.Pages.Admin.AlertRulesModel +@{ + ViewData["Title"] = "Alert Rules"; + ViewData["Page"] = "admin-alerts"; +} + +@if (!string.IsNullOrEmpty(Model.Message)) +{ +
✓ @Model.Message
+} + +
+ Alert Rules + +
+ +
+ + + + + + + + + + + + + @foreach (var r in Model.Rules) + { + + + + + + + + + + + } + @if (!Model.Rules.Any()) + { + + } + +
MonitorConditionThresholdWebhookCooldownLast FiredEnabledActions
@(r.Monitor?.Name ?? "All Monitors")@r.Condition@(r.ThresholdValue?.ToString() ?? "-")@(string.IsNullOrEmpty(r.WebhookUrl) ? "-" : "✓ configured")@r.CooldownMinutes min@(r.LastFiredAt?.ToString("yyyy-MM-dd HH:mm") ?? "never") +
+ + +
+
+
+ + +
+
No alert rules configured.
+
+ + + + +@section Scripts { + +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml.cs new file mode 100644 index 0000000..fc31e05 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/AlertRules.cshtml.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AlertRulesModel : PageModel +{ + private readonly DatabaseContext _database; + public AlertRulesModel(DatabaseContext database) => _database = database; + + public List Rules { get; set; } = new(); + public List Monitors { get; set; } = new(); + public string? Message { get; set; } + + [BindProperty] public AlertRule NewRule { get; set; } = new(); + + public async Task OnGetAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + Rules = await _database.AlertRules + .Include(r => r.Monitor) + .OrderBy(r => r.MonitorId) + .ToListAsync(); + + Monitors = await _database.Monitors.Where(m => m.IsActive).OrderBy(m => m.Name).ToListAsync(); + return Page(); + } + + public async Task OnPostSaveAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + NewRule.IsEnabled = true; + _database.AlertRules.Add(NewRule); + await _database.SaveChangesAsync(); + Message = "Alert rule saved."; + return await OnGetAsync(); + } + + public async Task OnPostToggleAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var rule = await _database.AlertRules.FindAsync(id); + if (rule != null) + { + rule.IsEnabled = !rule.IsEnabled; + await _database.SaveChangesAsync(); + } + + Message = "Rule updated."; + return await OnGetAsync(); + } + + public async Task OnPostDeleteAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var rule = await _database.AlertRules.FindAsync(id); + if (rule != null) + { + _database.AlertRules.Remove(rule); + await _database.SaveChangesAsync(); + } + + Message = "Rule deleted."; + return await OnGetAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml new file mode 100644 index 0000000..c39a6e7 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml @@ -0,0 +1,126 @@ +@page +@model Status.Pages.Admin.CertificatesModel +@{ + ViewData["Title"] = "Manage Certificates"; + ViewData["Page"] = "admin-certs"; +} + +@if (!string.IsNullOrEmpty(Model.Message)) +{ +
✓ @Model.Message
+} + +
+ SSL Certificates + +
+ +
+ + + + + + + + + + + + + @foreach (var c in Model.Certificates) + { + var now = DateTime.UtcNow; + var days = c.ExpiresAt.HasValue ? (int)(c.ExpiresAt.Value - now).TotalDays : (int?)null; + var cls = days == null ? "" : days <= 0 ? "cert-expiry-expired" : days <= 7 ? "cert-expiry-critical" : days <= 30 ? "cert-expiry-warn" : "cert-expiry-ok"; + + + + + + + + + + + } + @if (!Model.Certificates.Any()) + { + + } + +
NameDomainIssuerIssuedExpiresDays LeftStatusActions
@c.Name@c.Domain:@c.Port@(c.Issuer?.Split(',')[0] ?? "-")@(c.IssuedAt?.ToString("yyyy-MM-dd") ?? "-")@(c.ExpiresAt?.ToString("yyyy-MM-dd") ?? "-")@(days.HasValue? days +"d" : "-") + @if (!string.IsNullOrEmpty(c.LastError)) { ERROR } + else if (days == null) { Unchecked } + else if (days <= 0) { EXPIRED } + else if (days <= 7) { CRITICAL } + else if (days <= 30) { EXPIRING } + else { VALID } + +
+
+ + +
+
+ + +
+
+
No certificates tracked yet.
+
+ + + diff --git a/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml.cs new file mode 100644 index 0000000..c5d5c00 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Certificates.cshtml.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +public class CertificatesModel : PageModel +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + private readonly DatabaseContext _db; + private readonly MonitoringService _monSvc; + public CertificatesModel(DatabaseContext db, MonitoringService monSvc) { _db = db; _monSvc = monSvc; } + + public List Certificates { get; set; } = new(); + [BindProperty] public CertificateEntry EditCert { get; set; } = new(); + public string? Message { get; set; } + + public async Task OnGetAsync(string? msg) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + Certificates = await _db.Certificates.OrderBy(c => c.ExpiresAt).ToListAsync(); + Message = msg; + return Page(); + } + + public async Task OnPostSaveAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + if (EditCert.Id == 0) + { + _db.Certificates.Add(EditCert); + } + else + { + var e = await _db.Certificates.FindAsync(EditCert.Id); + if (e != null) + { + e.Name = EditCert.Name; + e.Domain = EditCert.Domain; + e.Port = EditCert.Port; + e.IsPublic = EditCert.IsPublic; + e.AlertOnExpiry = EditCert.AlertOnExpiry; + e.AlertDaysBeforeExpiry = EditCert.AlertDaysBeforeExpiry; + } + } + await _db.SaveChangesAsync(); + return RedirectToPage(new { msg = "Certificate saved." }); + } + + public async Task OnPostDeleteAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var c = await _db.Certificates.FindAsync(id); + if (c != null) { _db.Certificates.Remove(c); await _db.SaveChangesAsync(); } + return RedirectToPage(new { msg = "Certificate deleted." }); + } + + public async Task OnPostCheckNowAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var c = await _db.Certificates.FindAsync(id); + if (c != null) + { + await _monSvc.CheckCertificateAsync(c); + } + + return RedirectToPage(new { msg = "Certificate checked." }); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml new file mode 100644 index 0000000..a34def4 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml @@ -0,0 +1,165 @@ +@page +@model Status.Pages.Admin.IncidentsModel +@{ + ViewData["Title"] = "Manage Incidents"; + ViewData["Page"] = "admin-incidents"; +} + +@if (!string.IsNullOrEmpty(Model.Message)) +{ +
✓ @Model.Message
+} + +
+ Incidents + +
+ +
+ + + + + + + + + + + + + @foreach (var i in Model.Incidents) + { + var severityBadge = i.Severity switch { + IncidentSeverity.Critical => "badge-down", + IncidentSeverity.Major => "badge-warn", + _ => "badge-info" + }; + var statusBadge = i.Status == IncidentStatus.Resolved ? "badge-up" : "badge-warn"; + + + + + + + + + + + } + @if (!Model.Incidents.Any()) + { + + } + +
TitleSeverityStatusMonitorCreatedResolvedVisibilityActions
@i.Title@i.Severity@i.Status@(i.Monitor?.Name ?? "-")@i.CreatedAt.ToString("yyyy-MM-dd HH:mm")@(i.ResolvedAt?.ToString("yyyy-MM-dd HH:mm") ?? "-")@(i.IsPublic ? "🌐 Public" : "🔒 Private") +
+ + @if (i.Status != IncidentStatus.Resolved) + { +
+ + +
+ } +
+ + +
+
+
No incidents recorded.
+
+ + + + + + + +@section Scripts { + +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml.cs new file mode 100644 index 0000000..013af7b --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Incidents.cshtml.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class IncidentsModel : PageModel +{ + private readonly DatabaseContext _database; + public IncidentsModel(DatabaseContext database) => _database = database; + + public List Incidents { get; set; } = new(); + public List Monitors { get; set; } = new(); + public string? Message { get; set; } + + [BindProperty] public Incident NewIncident { get; set; } = new(); + + public class UpdateDto + { + public int IncidentId { get; set; } + public IncidentStatus Status { get; set; } + public string Message { get; set; } = ""; + } + + [BindProperty] public UpdateDto UpdateDtoModel { get; set; } = new(); + + public async Task OnGetAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + Incidents = await _database.Incidents + .Include(i => i.Updates) + .Include(i => i.Monitor) + .OrderByDescending(i => i.CreatedAt) + .ToListAsync(); + + Monitors = await _database.Monitors.Where(m => m.IsActive).ToListAsync(); + return Page(); + } + + public async Task OnPostCreateAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + NewIncident.CreatedAt = DateTime.UtcNow; + NewIncident.UpdatedAt = DateTime.UtcNow; + _database.Incidents.Add(NewIncident); + await _database.SaveChangesAsync(); + Message = "Incident created."; + return await OnGetAsync(); + } + + public async Task OnPostPostUpdateAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var incident = await _database.Incidents.FindAsync(UpdateDtoModel.IncidentId); + if (incident != null) + { + incident.Status = UpdateDtoModel.Status; + incident.UpdatedAt = DateTime.UtcNow; + if (UpdateDtoModel.Status == IncidentStatus.Resolved) + { + incident.ResolvedAt = DateTime.UtcNow; + } + + _database.IncidentUpdates.Add(new IncidentUpdate + { + IncidentId = incident.Id, + Message = UpdateDtoModel.Message, + Status = UpdateDtoModel.Status + }); + await _database.SaveChangesAsync(); + } + + Message = "Update posted."; + return await OnGetAsync(); + } + + public async Task OnPostResolveAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var incident = await _database.Incidents.FindAsync(id); + if (incident != null) + { + incident.Status = IncidentStatus.Resolved; + incident.ResolvedAt = DateTime.UtcNow; + incident.UpdatedAt = DateTime.UtcNow; + _database.IncidentUpdates.Add(new IncidentUpdate + { + IncidentId = id, + Message = "Incident resolved.", + Status = IncidentStatus.Resolved + }); + await _database.SaveChangesAsync(); + } + + Message = "Incident resolved."; + return await OnGetAsync(); + } + + public async Task OnPostDeleteAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var incident = await _database.Incidents.FindAsync(id); + if (incident != null) + { + _database.Incidents.Remove(incident); + await _database.SaveChangesAsync(); + } + + Message = "Incident deleted."; + return await OnGetAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml new file mode 100644 index 0000000..e67056a --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml @@ -0,0 +1,133 @@ +@page +@model Status.Pages.Admin.IngestModel +@{ + ViewData["Title"] = "Log Ingestion"; + ViewData["Page"] = "admin-ingest"; + var host = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}"; +} + +
Log Ingestion API
+

+ Status accepts logs from any application via HTTP POST. Compatible with custom HTTP sinks from Serilog, NLog, log4net, and any HTTP client. +

+ +
+
+
+
Single Log Entry
+
+
POST @host/api/logs/ingest +Content-Type: application/json + +{ + "source": "my-app", + "level": "error", + "message": "Something went wrong", + "exception": "System.Exception: ...", + "properties": "{\"userId\": 42}", + "host": "prod-server-01", + "traceId": "abc123" +}
+
+
+ +
+
Batch Ingestion
+
+
POST @host/api/logs/batch +Content-Type: application/json + +[ + {"source":"app","level":"info","message":"Started"}, + {"source":"app","level":"warn","message":"Slow query","properties":"{\"ms\":2400}"} +]
+
+
+
+ +
+
+
EonaCat.LogStack HTTP Flow (.NET)
+
+
// Install: EonaCat.LogStack +var logger = new LogBuilder().WriteToHttp("@host/api/logs/eonacat").Build(); +
+
+
+ +
+
Serilog HTTP Sink (.NET)
+
+
+// Install: Serilog.Sinks.Http +Log.Logger = new LoggerConfiguration() +.WriteTo.Http( +requestUri: "@host/api/logs/serilog", +queueLimitBytes: null) +.CreateLogger(); +
+
+
+ +
+
+ Syslog (UDP) +
+
+
+# Send a syslog message using netcat (Linux/macOS) + +echo "<14>1 $(date -u +%Y-%m-%dT%H:%M:%SZ) my-host my-app - - - Hello from syslog" \ +| nc -u -w0 YOUR_HOST 514 + +# Example using logger (Linux) + +logger -n YOUR_HOST -P 514 "Hello from syslog" + + +# Example raw syslog message (RFC5424) + +<14>1 2026-03-28T12:00:00Z my-host my-app - - - Something happened + + +# Example JSON over syslog (auto-detected) + +{ +"source": "my-app", +"level": "error", +"message": "Something failed", +"host": "server-01", +"traceId": "abc123" +} +
+
+
+ +
+
Python (requests)
+
+
import requests + +requests.post("@host/api/logs/ingest", json={ + "source": "my-python-app", + "level": "info", + "message": "App started", + "host": socket.gethostname() +})
+
+
+ +
+
Log Levels
+
+ + + + + + +
DEBUGVerbose diagnostic info
INFONormal operation events
WARNINGWarning requires attention
ERRORErrors requiring attention
CRITICALSystem critical failure / crash
+
+
+
+
diff --git a/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml.cs new file mode 100644 index 0000000..77600cd --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Ingest.cshtml.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +public class IngestModel : PageModel +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public IActionResult OnGet() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + return Page(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml new file mode 100644 index 0000000..13e10a9 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml @@ -0,0 +1,52 @@ +@page +@model Status.Pages.Admin.LoginModel +@{ + Layout = null; +} + + + + + + Admin Login - Status + + + + + + + + diff --git a/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml.cs new file mode 100644 index 0000000..90576e5 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Login.cshtml.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using EonaCat.LogStack.Status.Services; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class LoginModel : PageModel +{ + private readonly AuthenticationService _auth; + public LoginModel(AuthenticationService auth) => _auth = auth; + + [BindProperty] public string Password { get; set; } = ""; + public string? Error { get; set; } + + public void OnGet() { } + + public async Task OnPostAsync() + { + if (!string.IsNullOrWhiteSpace(Password) && await _auth.ValidatePasswordAsync(Password)) + { + HttpContext.Session.SetString("IsAdmin", "true"); + return RedirectToPage("/Index"); + } + Error = "Invalid password."; + return Page(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml new file mode 100644 index 0000000..05e3709 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml @@ -0,0 +1,2 @@ +@page +@model Status.Pages.Admin.LogoutModel diff --git a/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml.cs new file mode 100644 index 0000000..4469bfe --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Logout.cshtml.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class LogoutModel : PageModel +{ + public IActionResult OnGet() + { + HttpContext.Session.Clear(); + return RedirectToPage("/Index"); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml new file mode 100644 index 0000000..e7f0b54 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml @@ -0,0 +1,229 @@ +@page +@model EonaCat.LogStack.Status.Pages.Admin.MonitorsModel +@{ + ViewData["Title"] = "Manage Monitors"; + ViewData["Page"] = "admin-monitors"; +} + +@if (!string.IsNullOrEmpty(Model.Message)) +{ +
✓ @Model.Message
+} + +
+ Monitors + +
+ +
+ + + + + + + + + + + + + @foreach (var m in Model.Monitors) + { + var badgeClass = m.LastStatus switch { + MonitorStatus.Up => "badge-up", + MonitorStatus.Down => "badge-down", + MonitorStatus.Warning or MonitorStatus.Degraded => "badge-warn", + _ => "badge-unknown" + }; + + + + + + + + + + + } + @if (!Model.Monitors.Any()) + { + + } + +
NameTypeHost / URLGroupIntervalStatusVisibilityActions
@m.Name + @if (!m.IsActive) { PAUSED } + @m.Type@(m.Url ?? (m.Host + (m.Port.HasValue ? ":" + m.Port : "")))@(m.GroupName ?? "-")@m.IntervalSeconds s + @m.LastStatus + @if (m.ConsecutiveFailures > 0 && m.LastStatus != MonitorStatus.Down) + { + (@m.ConsecutiveFailures/@m.FailureThreshold) + } + @(m.IsPublic ? "🌐 Public" : "🔒 Private") +
+
+ + +
+ +
+ + +
+
+
No monitors yet. Add one above.
+
+ + + + +@section Scripts { + +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml.cs new file mode 100644 index 0000000..f16586b --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Monitors.cshtml.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class MonitorsModel : PageModel +{ + private readonly DatabaseContext _db; + private readonly MonitoringService _monSvc; + public MonitorsModel(DatabaseContext db, MonitoringService monSvc) { _db = db; _monSvc = monSvc; } + + public List Monitors { get; set; } = new(); + + [BindProperty] public Monitor EditMonitor { get; set; } = new(); + public string? Message { get; set; } + + public async Task OnGetAsync(string? msg) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + Monitors = await _db.Monitors.OrderBy(m => m.GroupName).ThenBy(m => m.Name).ToListAsync(); + Message = msg; + return Page(); + } + + public async Task OnPostSaveAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + // If no host, extract from URL + if (string.IsNullOrWhiteSpace(EditMonitor.Host) && !string.IsNullOrEmpty(EditMonitor.Url)) + { + try + { + var uri = new Uri(EditMonitor.Url); + EditMonitor.Host = uri.Host; + if (EditMonitor.Port == null || EditMonitor.Port == 0) + { + EditMonitor.Port = uri.Port; + } + } + catch { } + } + + if (EditMonitor.Id == 0) + { + _db.Monitors.Add(EditMonitor); + } + else + { + var existing = await _db.Monitors.FindAsync(EditMonitor.Id); + if (existing == null) + { + return RedirectToPage(new { msg = "Monitor not found." }); + } + + existing.Name = EditMonitor.Name; + existing.Description = EditMonitor.Description; + existing.Type = EditMonitor.Type; + existing.Host = EditMonitor.Host; + existing.Port = EditMonitor.Port; + existing.Url = EditMonitor.Url; + existing.ProcessName = EditMonitor.ProcessName; + existing.IntervalSeconds = EditMonitor.IntervalSeconds; + existing.TimeoutMs = EditMonitor.TimeoutMs; + existing.IsActive = EditMonitor.IsActive; + existing.IsPublic = EditMonitor.IsPublic; + existing.Tags = EditMonitor.Tags; + existing.GroupName = EditMonitor.GroupName; + // new fields + existing.FailureThreshold = Math.Max(1, EditMonitor.FailureThreshold); + existing.ExpectedKeyword = EditMonitor.ExpectedKeyword; + existing.ExpectedStatusCode = EditMonitor.ExpectedStatusCode; + } + + await _db.SaveChangesAsync(); + return RedirectToPage(new { msg = "Monitor saved." }); + } + + public async Task OnPostDeleteAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var m = await _db.Monitors.FindAsync(id); + if (m != null) { _db.Monitors.Remove(m); await _db.SaveChangesAsync(); } + return RedirectToPage(new { msg = "Monitor deleted." }); + } + + public async Task OnPostCheckNowAsync(int id) + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var m = await _db.Monitors.FindAsync(id); + if (m != null) + { + await _monSvc.CheckMonitorAsync(m); + } + + return RedirectToPage(new { msg = "Check completed." }); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml b/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml new file mode 100644 index 0000000..ab3bba3 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml @@ -0,0 +1,129 @@ +@page +@model EonaCat.LogStack.Status.Pages.Admin.SettingsModel +@{ + ViewData["Title"] = "Settings"; + ViewData["Page"] = "admin-settings"; + var pwParts = (Model.PasswordMessage ?? "").Split(':', 2); + var pwType = pwParts.Length > 1 ? pwParts[0] : ""; + var pwMsg = pwParts.Length > 1 ? pwParts[1] : pwParts.ElementAtOrDefault(0) ?? ""; +} + +@if (!string.IsNullOrEmpty(Model.Message)) +{ +
✓ @Model.Message
+} + +
+ +
+
General Settings
+
+
+
+
+ + +
+
+ + +
+
+ + +
Global fallback for alert rules without a specific webhook.
+
+
+ + +
+
+ + + + +
+ +
+
+
+
+ +
+
Change Password
+
+
+ @if (!string.IsNullOrEmpty(pwMsg)) + { +
@pwMsg
+ } +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
API Endpoints
+
+
+

Use these endpoints to ingest logs or query status from external applications.

+
+
POST /api/logs/ingest
+
POST /api/logs/batch
+
POST /api/logs/serilog
+
POST /api/logs/eonacat
+
GET /api/logs ?level=&source=&search=&from=&to=&page=&pageSize=
+
GET /api/logs/stats ?hours=24
+
GET /api/status/summary
+
GET /api/monitors
+
GET /api/monitors/{id}/check
+
GET /api/monitors/{id}/history ?limit=100
+
GET /api/monitors/{id}/uptime
+
POST /api/monitors/{id}/pause
+
POST /api/monitors/{id}/resume
+
GET /api/incidents ?activeOnly=true
+
POST /api/incidents
+
PATCH /api/incidents/{id}
+
+ View Ingest Docs → +
+
+
+ +
diff --git a/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml.cs b/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml.cs new file mode 100644 index 0000000..a138153 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Admin/Settings.cshtml.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using EonaCat.LogStack.Status.Services; + +namespace EonaCat.LogStack.Status.Pages.Admin; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class SettingsModel : PageModel +{ + private readonly AuthenticationService _authenticationService; + public SettingsModel(AuthenticationService authentication) => _authenticationService = authentication; + + [BindProperty] public string SiteName { get; set; } = ""; + [BindProperty] public bool ShowLogsPublicly { get; set; } + [BindProperty] public bool ShowUptimePublicly { get; set; } + [BindProperty] public bool ShowIncidentsPublicly { get; set; } + [BindProperty] public bool AutoCreateIncidents { get; set; } + [BindProperty] public int MaxLogRetentionDays { get; set; } = 30; + [BindProperty] public string AlertEmail { get; set; } = ""; + [BindProperty] public string AlertWebhookUrl { get; set; } = ""; + [BindProperty] public string CurrentPassword { get; set; } = ""; + [BindProperty] public string NewPassword { get; set; } = ""; + [BindProperty] public string ConfirmPassword { get; set; } = ""; + public string? Message { get; set; } + public string? PasswordMessage { get; set; } + + public async Task OnGetAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + SiteName = await _authenticationService.GetSettingAsync("SiteName", "Status"); + ShowLogsPublicly = (await _authenticationService.GetSettingAsync("ShowLogsPublicly", "false")) == "true"; + ShowUptimePublicly = (await _authenticationService.GetSettingAsync("ShowUptimePublicly", "true")) == "true"; + ShowIncidentsPublicly = (await _authenticationService.GetSettingAsync("ShowIncidentsPublicly", "true")) == "true"; + AutoCreateIncidents = (await _authenticationService.GetSettingAsync("AutoCreateIncidents", "false")) == "true"; + MaxLogRetentionDays = int.TryParse(await _authenticationService.GetSettingAsync("MaxLogRetentionDays", "30"), out var d) ? d : 30; + AlertEmail = await _authenticationService.GetSettingAsync("AlertEmail", ""); + AlertWebhookUrl = await _authenticationService.GetSettingAsync("AlertWebhookUrl", ""); + return Page(); + } + + public async Task OnPostSaveSettingsAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + await _authenticationService.SetSettingAsync("SiteName", SiteName ?? "Status"); + await _authenticationService.SetSettingAsync("ShowLogsPublicly", ShowLogsPublicly ? "true" : "false"); + await _authenticationService.SetSettingAsync("ShowUptimePublicly", ShowUptimePublicly ? "true" : "false"); + await _authenticationService.SetSettingAsync("ShowIncidentsPublicly", ShowIncidentsPublicly ? "true" : "false"); + await _authenticationService.SetSettingAsync("AutoCreateIncidents", AutoCreateIncidents ? "true" : "false"); + await _authenticationService.SetSettingAsync("MaxLogRetentionDays", MaxLogRetentionDays.ToString()); + await _authenticationService.SetSettingAsync("AlertEmail", AlertEmail ?? ""); + await _authenticationService.SetSettingAsync("AlertWebhookUrl", AlertWebhookUrl ?? ""); + Message = "Settings saved."; + return await OnGetAsync(); + } + + public async Task OnPostChangePasswordAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + if (NewPassword != ConfirmPassword) { PasswordMessage = "error:Passwords do not match."; return await OnGetAsync(); } + if (NewPassword.Length < 6) { PasswordMessage = "error:Password must be at least 6 characters."; return await OnGetAsync(); } + if (await _authenticationService.ChangePasswordAsync(CurrentPassword, NewPassword)) + { + PasswordMessage = "success:Password changed successfully."; + } + else + { + PasswordMessage = "error:Current password is incorrect."; + } + + return await OnGetAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Analytics.cshtml b/EonaCat.LogStack.Status/Pages/Analytics.cshtml new file mode 100644 index 0000000..130ba1d --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Analytics.cshtml @@ -0,0 +1,80 @@ +@page +@model EonaCat.LogStack.Status.Pages.AnalyticsModel +@{ + ViewData["Title"] = "Analytics"; + ViewData["Page"] = "analytics"; +} + +
+ Uptime & Performance + @Model.Reports.Count monitors +
+ +@if (!Model.Reports.Any()) +{ +
+
+
No check history yet
+
+} +else +{ +
+ + + + + + + + + + + + @foreach (var r in Model.Reports) + { + string UptimeCls(double pct) => pct >= 99 ? "rt-good" : pct >= 95 ? "rt-ok" : "rt-slow"; + + + + + + + + + + } + +
Monitor24h Uptime7d Uptime30d UptimeAvg ResponseChecks (30d)Response Trend
@r.MonitorName@r.Uptime24h.ToString("F1")%@r.Uptime7d.ToString("F1")%@r.Uptime30d.ToString("F1")%@((int)r.AvgResponseMs)ms + @r.UpChecks ↑ + @r.DownChecks ↓ + + +
+
+ + +
+
LOG ERROR RATE - LAST 24H
+ +
+} + +@section Scripts { + + +} diff --git a/EonaCat.LogStack.Status/Pages/Analytics.cshtml.cs b/EonaCat.LogStack.Status/Pages/Analytics.cshtml.cs new file mode 100644 index 0000000..a405486 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Analytics.cshtml.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; + +namespace EonaCat.LogStack.Status.Pages; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AnalyticsModel : PageModel +{ + private readonly DatabaseContext _db; + private readonly MonitoringService _monSvc; + + public AnalyticsModel(DatabaseContext db, MonitoringService monSvc) + { + _db = db; + _monSvc = monSvc; + } + + public List Reports { get; set; } = new(); + /// Comma-separated response times (ms) for the last 30 checks - keyed by MonitorId. + public Dictionary SparklineData { get; set; } = new(); + + public async Task OnGetAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + var monitors = await _db.Monitors.Where(m => m.IsActive).OrderBy(m => m.Name).ToListAsync(); + + foreach (var m in monitors) + { + var report = await _monSvc.GetUptimeReportAsync(m.Id); + Reports.Add(report); + + // Last 30 response times for sparkline + var recent = await _db.MonitorChecks + .Where(c => c.MonitorId == m.Id) + .OrderByDescending(c => c.CheckedAt) + .Take(30) + .Select(c => c.ResponseMs) + .ToListAsync(); + + recent.Reverse(); + SparklineData[m.Id] = string.Join(",", recent.Select(v => ((int)v).ToString())); + } + + return Page(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Certificates.cshtml b/EonaCat.LogStack.Status/Pages/Certificates.cshtml new file mode 100644 index 0000000..766662f --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Certificates.cshtml @@ -0,0 +1,72 @@ +@page +@model Status.Pages.CertificatesModel +@{ + ViewData["Title"] = "Certificates"; + ViewData["Page"] = "certs"; +} + +
+ SSL/TLS Certificates + @if (Model.IsAdmin) { Manage → } +
+ +@if (!Model.Certificates.Any()) +{ +
+
+
No certificates tracked
+
+} +else +{ +
+ @{ + var now = DateTime.UtcNow; + var valid = Model.Certificates.Count(c => c.ExpiresAt.HasValue && (c.ExpiresAt.Value - now).TotalDays > 30); + var expiringSoon = Model.Certificates.Count(c => c.ExpiresAt.HasValue && (c.ExpiresAt.Value - now).TotalDays is > 0 and <= 30); + var expired = Model.Certificates.Count(c => c.ExpiresAt.HasValue && c.ExpiresAt.Value <= now); + } +
Valid
@valid
+
Expiring <30d
@expiringSoon
+
Expired
@expired
+
+ +
+ + + + + + + + + + + + + @foreach (var certificate in Model.Certificates) + { + var days = certificate.ExpiresAt.HasValue ? (int)(certificate.ExpiresAt.Value - now).TotalDays : (int?)null; + var cls = days == null ? "" : days <= 0 ? "cert-expiry-expired" : days <= 7 ? "cert-expiry-critical" : days <= 30 ? "cert-expiry-warn" : "cert-expiry-ok"; + + + + + + + + + + + } + +
NameDomainIssuerValid FromExpiresDays LeftFingerprintStatus
@certificate.Name@certificate.Domain@(certificate.Issuer?.Split(',').FirstOrDefault()?.Replace("CN=", "") ?? "-")@(certificate.IssuedAt?.ToString("yyyy-MM-dd") ?? "-")@(certificate.ExpiresAt?.ToString("yyyy-MM-dd") ?? "-")@(days.HasValue? days +"d" : "-")@(certificate.Thumbprint?[..16] ?? "-")… + @if (!string.IsNullOrEmpty(certificate.LastError)) { ERROR } + else if (days == null) { Unchecked } + else if (days <= 0) { EXPIRED } + else if (days <= 7) { CRITICAL } + else if (days <= 30) { EXPIRING } + else { VALID } +
+
+} diff --git a/EonaCat.LogStack.Status/Pages/Certificates.cshtml.cs b/EonaCat.LogStack.Status/Pages/Certificates.cshtml.cs new file mode 100644 index 0000000..491f610 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Certificates.cshtml.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; + +namespace EonaCat.LogStack.Status.Pages; + +public class CertificatesModel : PageModel +{ + private readonly DatabaseContext _db; + public CertificatesModel(DatabaseContext db) => _db = db; + public List Certificates { get; set; } = new List(); + public bool IsAdmin { get; set; } + + public async Task OnGetAsync() + { + IsAdmin = HttpContext.Session.GetString("IsAdmin") == "true"; + Certificates = await _db.Certificates.OrderBy(c => c.ExpiresAt).ToListAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Incidents.cshtml b/EonaCat.LogStack.Status/Pages/Incidents.cshtml new file mode 100644 index 0000000..3f9e737 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Incidents.cshtml @@ -0,0 +1,70 @@ +@page +@model EonaCat.LogStack.Status.Pages.IncidentsModel +@{ + ViewData["Title"] = "Incidents"; + ViewData["Page"] = "incidents"; +} + +
+ Incident History + @Model.Incidents.Count total +
+ +@if (!Model.Incidents.Any()) +{ +
+
+
No incidents recorded - all systems nominal.
+
+} + +@foreach (var inc in Model.Incidents) +{ + var headerClass = inc.Severity switch { + IncidentSeverity.Critical => "down", + IncidentSeverity.Major => "warn", + _ => "info" + }; + var statusBadge = inc.Status == IncidentStatus.Resolved ? "badge-up" : "badge-warn"; + +
+
+
+ @inc.Title + @inc.Status + @inc.Severity +
+ + @inc.CreatedAt.ToString("yyyy-MM-dd HH:mm") UTC + @if (inc.ResolvedAt.HasValue) { - resolved @inc.ResolvedAt.Value.ToString("yyyy-MM-dd HH:mm") } + +
+
+ @if (!string.IsNullOrEmpty(inc.Body)) + { +

@inc.Body

+ } + @if (inc.Monitor != null) + { +
+ Affected service: @inc.Monitor.Name +
+ } + @if (inc.Updates.Any()) + { +
+ @foreach (var u in inc.Updates.OrderByDescending(u => u.PostedAt)) + { +
+
+ @u.PostedAt.ToString("yyyy-MM-dd HH:mm") + @u.Status +
+
@u.Message
+
+ } +
+ } +
+
+} diff --git a/EonaCat.LogStack.Status/Pages/Incidents.cshtml.cs b/EonaCat.LogStack.Status/Pages/Incidents.cshtml.cs new file mode 100644 index 0000000..8caa9c0 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Incidents.cshtml.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; + +namespace EonaCat.LogStack.Status.Pages; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class IncidentsModel : PageModel +{ + private readonly DatabaseContext _db; + private readonly AuthenticationService _authSvc; + + public IncidentsModel(DatabaseContext db, AuthenticationService authSvc) + { + _db = db; + _authSvc = authSvc; + } + + public List Incidents { get; set; } = new(); + + public async Task OnGetAsync() + { + var isAdmin = HttpContext.Session.GetString("IsAdmin") == "true"; + var showIncidents = (await _authSvc.GetSettingAsync("ShowIncidentsPublicly", "true")) == "true"; + + if (!isAdmin && !showIncidents) + { + Incidents = new List(); + return; + } + + var query = _db.Incidents + .Include(i => i.Updates) + .Include(i => i.Monitor) + .AsQueryable(); + + if (!isAdmin) + { + query = query.Where(i => i.IsPublic); + } + + Incidents = await query.OrderByDescending(i => i.CreatedAt).ToListAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Index.cshtml b/EonaCat.LogStack.Status/Pages/Index.cshtml new file mode 100644 index 0000000..17d373b --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Index.cshtml @@ -0,0 +1,198 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Dashboard"; + ViewData["Page"] = "dashboard"; +} +
+ +
+
0 ? "warn" : Model.Stats.UpCount > 0 ? "up" : "neutral")"> +
Overall
+
+ @if (Model.Stats.DownCount > 0) { DEGRADED } + else if (Model.Stats.WarnCount > 0) { WARNING } + else if (Model.Stats.UpCount > 0) { OPERATIONAL } + else { UNKNOWN } +
+
@Model.Stats.TotalMonitors monitor(s) active
+
+
+
Online
+
@Model.Stats.UpCount
+
monitors up
+
+
+
Offline
+
@Model.Stats.DownCount
+
monitors down
+
+
+
Warnings
+
@Model.Stats.WarnCount
+
monitors degraded
+
+ @if (Model.ShowUptime) + { +
+
Uptime
+
@Model.Stats.OverallUptime.ToString("F1")%
+
overall availability
+
+ } +
0 ? "warn" : "neutral")"> +
Certificates
+
@Model.Stats.CertCount
+
+ @if (Model.Stats.CertExpired > 0) { @Model.Stats.CertExpired expired } + else if (Model.Stats.CertExpiringSoon > 0) { @Model.Stats.CertExpiringSoon expiring soon } + else { all valid } +
+
+ @if (Model.IsAdmin) + { +
+
Log Errors
+
@Model.Stats.ErrorLogs
+
@Model.Stats.TotalLogs total entries
+
+
+
Incidents
+
@Model.Stats.ActiveIncidents
+
@Model.Stats.ResolvedIncidents resolved
+
+ } +
+ +@* Active Incidents Banner *@ +@if (Model.ShowIncidents && Model.ActiveIncidents.Any()) +{ + foreach (var inc in Model.ActiveIncidents) + { + var incClass = inc.Severity switch { + IncidentSeverity.Critical => "alert-danger", + IncidentSeverity.Major => "alert-warning", + _ => "alert-info" + }; +
+
+ ⚠ @inc.Title + @inc.Status • @inc.Severity +
+ @if (!string.IsNullOrEmpty(inc.Body)) + { +
@inc.Body
+ } + @if (inc.Updates.Any()) + { + var latest = inc.Updates.OrderByDescending(u => u.PostedAt).First(); +
+ Latest update (@latest.PostedAt.ToString("yyyy-MM-dd HH:mm")): @latest.Message +
+ } +
+ } +} + +@if (Model.Monitors.Any()) +{ + var groups = Model.Monitors.GroupBy(m => m.GroupName ?? "General"); + foreach (var group in groups) + { +
+
+ @group.Key + @group.Count() services +
+
+ @foreach (var m in group) + { + var badgeClass = m.LastStatus switch { + MonitorStatus.Up => "badge-up", + MonitorStatus.Down => "badge-down", + MonitorStatus.Warning or MonitorStatus.Degraded => "badge-warn", + _ => "badge-unknown" + }; + var checks = Model.RecentChecks.ContainsKey(m.Id) ? Model.RecentChecks[m.Id] : new(); +
+
+
@m.Name + @if (!m.IsPublic && Model.IsAdmin) { PRIVATE } + @if (!m.IsActive) { PAUSED } +
+
@(m.Url ?? (m.Host + (m.Port.HasValue ? ":" + m.Port : "")))
+
+
@m.Type.ToString().ToUpper()
+
+ @if (Model.ShowUptime && checks.Any()) + { +
+ @foreach (var c in checks) + { + var cls = c.Status switch { MonitorStatus.Up => "up", MonitorStatus.Down => "down", MonitorStatus.Warning or MonitorStatus.Degraded => "warn", _ => "unknown" }; +
+ } +
+ } +
+
+ @if (m.LastResponseMs.HasValue) { @((int)m.LastResponseMs.Value)ms } +
+
@m.LastStatus
+
+ } +
+
+ } +} +else +{ +
+
+
No monitors have been configured
+ @if (Model.IsAdmin) { } +
+} + +@if (Model.Certificates.Any()) +{ +
+
+ SSL Certificates +
+
+ + + + + + + + + + @foreach (var c in Model.Certificates) + { + var now = DateTime.UtcNow; + var daysLeft = c.ExpiresAt.HasValue ? (int)(c.ExpiresAt.Value - now).TotalDays : (int?)null; + var expiryClass = daysLeft == null ? "" : daysLeft <= 0 ? "cert-expiry-expired" : daysLeft <= 7 ? "cert-expiry-critical" : daysLeft <= 30 ? "cert-expiry-warn" : "cert-expiry-ok"; + + + + + + + + } + +
DomainNameExpiresDays LeftStatus
@c.Domain:@c.Port@c.Name@(c.ExpiresAt?.ToString("yyyy-MM-dd") ?? "unknown")@(daysLeft.HasValue ? daysLeft + "d" : "-") + @if (daysLeft == null) { Unknown } + else if (daysLeft <= 0) { EXPIRED } + else if (daysLeft <= 7) { CRITICAL } + else if (daysLeft <= 30) { EXPIRING } + else { VALID } +
+
+
+} + +
diff --git a/EonaCat.LogStack.Status/Pages/Index.cshtml.cs b/EonaCat.LogStack.Status/Pages/Index.cshtml.cs new file mode 100644 index 0000000..3578e8e --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Index.cshtml.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Pages; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class IndexModel : PageModel +{ + private readonly DatabaseContext _db; + private readonly MonitoringService _monSvc; + private readonly AuthenticationService _authSvc; + + public IndexModel(DatabaseContext db, MonitoringService monSvc, AuthenticationService authSvc) + { + _db = db; + _monSvc = monSvc; + _authSvc = authSvc; + } + + public DashboardStats Stats { get; set; } = new(); + public List Monitors { get; set; } = new(); + public List Certificates { get; set; } = new(); + public List ActiveIncidents { get; set; } = new(); + public bool IsAdmin { get; set; } + public bool ShowUptime { get; set; } + public bool ShowIncidents { get; set; } + public string SiteName { get; set; } = "Status"; + public Dictionary> RecentChecks { get; set; } = new(); + + public async Task OnGetAsync() + { + IsAdmin = HttpContext.Session.GetString("IsAdmin") == "true"; + ShowUptime = (await _authSvc.GetSettingAsync("ShowUptimePublicly", "true")) == "true"; + ShowIncidents = (await _authSvc.GetSettingAsync("ShowIncidentsPublicly", "true")) == "true"; + SiteName = await _authSvc.GetSettingAsync("SiteName", "Status"); + Stats = await _monSvc.GetStatsAsync(IsAdmin); + + var query = _db.Monitors.Where(m => m.IsActive); + if (!IsAdmin) + { + query = query.Where(m => m.IsPublic); + } + + Monitors = await query.OrderBy(m => m.GroupName).ThenBy(m => m.Name).ToListAsync(); + + Certificates = await _db.Certificates.OrderBy(c => c.ExpiresAt).ToListAsync(); + + // Active incidents (public or admin) + var incidentQuery = _db.Incidents + .Include(i => i.Updates) + .Where(i => i.Status != IncidentStatus.Resolved); + if (!IsAdmin) + { + incidentQuery = incidentQuery.Where(i => i.IsPublic); + } + + ActiveIncidents = await incidentQuery.OrderByDescending(i => i.CreatedAt).ToListAsync(); + + // Recent checks for uptime bars (last 7 days, up to 90 per monitor) + var monitorIds = Monitors.Select(m => m.Id).ToList(); + var cutoff = DateTime.UtcNow.AddDays(-7); + var checks = await _db.MonitorChecks + .Where(c => monitorIds.Contains(c.MonitorId) && c.CheckedAt >= cutoff) + .OrderByDescending(c => c.CheckedAt) + .ToListAsync(); + + foreach (var m in Monitors) + { + RecentChecks[m.Id] = checks.Where(c => c.MonitorId == m.Id).Take(90).Reverse().ToList(); + } + } +} diff --git a/EonaCat.LogStack.Status/Pages/Logs.cshtml b/EonaCat.LogStack.Status/Pages/Logs.cshtml new file mode 100644 index 0000000..b45eedd --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Logs.cshtml @@ -0,0 +1,133 @@ +@page +@model LogsModel +@{ + ViewData["Title"] = "Log Stream"; + ViewData["Page"] = "logs"; +} + +@section Styles { + +} + +
+ + Log Stream + + + @Model.TotalCount entries +
+ + +
+
LOG VOLUME - LAST 24H
+ +
+ +
+ + + + + + + Clear + +
+ + +
+ ENTRIES + + / search + + + +
+ +
+ @if (!Model.Entries.Any()) + { +
+
+
No log entries match your filters
+
+ } + @foreach (var e in Model.Entries.AsEnumerable().Reverse()) + { +
+ @e.Timestamp.ToString("yyyy-MM-dd HH:mm:ss") + @e.Level.ToUpper() + @e.Source + + @e.Message + @if (!string.IsNullOrEmpty(e.TraceId)) + { + trace=@e.TraceId + } + @if (!string.IsNullOrEmpty(e.Exception)) + { +
+ Exception ▾ +
@e.Exception
+
+ } +
+
+ } +
+ +@if (Model.TotalPages > 1) +{ +
+ @if (Model.PageIndex > 1) + { + ← Prev + } + Page @Model.PageIndex of @Model.TotalPages + @if (Model.PageIndex < Model.TotalPages) + { + Next → + } +
+} + +@section Scripts { + + +} diff --git a/EonaCat.LogStack.Status/Pages/Logs.cshtml.cs b/EonaCat.LogStack.Status/Pages/Logs.cshtml.cs new file mode 100644 index 0000000..1c1d26c --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Logs.cshtml.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; + +namespace EonaCat.LogStack.Status.Pages; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class LogsModel : PageModel +{ + private readonly DatabaseContext _db; + public LogsModel(DatabaseContext db) => _db = db; + + public List Entries { get; set; } = new(); + public List Sources { get; set; } = new(); + public int TotalCount { get; set; } + public int TotalPages { get; set; } + + [BindProperty(SupportsGet = true)] public string? Level { get; set; } + [BindProperty(SupportsGet = true)] public string? Source { get; set; } + [BindProperty(SupportsGet = true)] public string? Search { get; set; } + [BindProperty(SupportsGet = true)] public DateTime? FromDate { get; set; } + [BindProperty(SupportsGet = true)] public DateTime? ToDate { get; set; } + [BindProperty(SupportsGet = true)] public int PageIndex { get; set; } = 1; + + private const int PageSize = 100; + + public async Task OnGetAsync() + { + if (HttpContext.Session.GetString("IsAdmin") != "true") + { + return RedirectToPage("/Admin/Login"); + } + + Sources = await _db.Logs.Select(l => l.Source).Distinct().OrderBy(s => s).ToListAsync(); + + var q = _db.Logs.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(Level)) + { + q = q.Where(l => l.Level == Level.ToLower()); + } + + if (!string.IsNullOrWhiteSpace(Source)) + { + q = q.Where(l => l.Source == Source); + } + + if (!string.IsNullOrWhiteSpace(Search)) + { + q = q.Where(l => l.Message.Contains(Search) || (l.Exception != null && l.Exception.Contains(Search))); + } + + if (FromDate.HasValue) + { + q = q.Where(l => l.Timestamp >= FromDate.Value.ToUniversalTime()); + } + + if (ToDate.HasValue) + { + q = q.Where(l => l.Timestamp <= ToDate.Value.AddDays(1).ToUniversalTime()); + } + + TotalCount = await q.CountAsync(); + TotalPages = (int)Math.Ceiling((double)TotalCount / PageSize); + + Entries = await q + .OrderByDescending(l => l.Timestamp) + .Skip((PageIndex - 1) * PageSize) + .Take(PageSize) + .ToListAsync(); + + return Page(); + } +} diff --git a/EonaCat.LogStack.Status/Pages/Monitors.cshtml b/EonaCat.LogStack.Status/Pages/Monitors.cshtml new file mode 100644 index 0000000..f3cb2f5 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Monitors.cshtml @@ -0,0 +1,83 @@ +@page +@model EonaCat.LogStack.Status.Pages.MonitorsModel +@{ + ViewData["Title"] = "Monitors"; + ViewData["Page"] = "monitors"; + var groups = Model.Monitors.GroupBy(m => m.GroupName ?? "General"); +} + +
+ All Monitors + @Model.Monitors.Count services +
+ +@foreach (var group in groups) +{ +
+
+ @group.Key + + @group.Count(m => m.LastStatus == MonitorStatus.Up) / @group.Count() up + +
+ + + + + + + + + + + + @foreach (var monitor in group) + { + var badgeClass = monitor.LastStatus switch + { + MonitorStatus.Up => "badge-up", + MonitorStatus.Down => "badge-down", + MonitorStatus.Warning or MonitorStatus.Degraded => "badge-warn", + _ => "badge-unknown" + }; + + var uptime = Model.UptimePercent.ContainsKey(monitor.Id) ? Model.UptimePercent[monitor.Id] : 0; + var uptimeColor = uptime >= 99 ? "var(--up)" : uptime >= 95 ? "var(--warn)" : "var(--down)"; + + + + + + + + + + } + +
MonitorTypeEndpointResponse30d UptimeLast CheckedStatus
+
@monitor.Name
+ @if (!string.IsNullOrEmpty(monitor.Description)) {
@monitor.Description
} +
@monitor.Type + @(monitor.Type is MonitorType.HTTP or MonitorType.HTTPS ? monitor.Url : $"{monitor.Host}{(monitor.Port.HasValue ? ":" + monitor.Port : "")}") + + @if (monitor.LastResponseMs.HasValue) { @((int)monitor.LastResponseMs.Value)ms } + else + { + + - + } + + @uptime.ToString("F1")% + + @(monitor.LastChecked.HasValue ? monitor.LastChecked.Value.ToString("HH:mm:ss") + " UTC" : "Never") + @monitor.LastStatus
+
+} + +@if (!Model.Monitors.Any()) +{ +
+
+
No monitors configured
+
+} diff --git a/EonaCat.LogStack.Status/Pages/Monitors.cshtml.cs b/EonaCat.LogStack.Status/Pages/Monitors.cshtml.cs new file mode 100644 index 0000000..b37266c --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Monitors.cshtml.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Pages; + +public class MonitorsModel : PageModel +{ + private readonly DatabaseContext _db; + public MonitorsModel(DatabaseContext db) => _db = db; + public List Monitors { get; set; } = new(); + public bool IsAdmin { get; set; } + public Dictionary UptimePercent { get; set; } = new(); + + public async Task OnGetAsync() + { + IsAdmin = HttpContext.Session.GetString("IsAdmin") == "true"; + var q = _db.Monitors.Where(m => m.IsActive); + if (!IsAdmin) + { + q = q.Where(m => m.IsPublic); + } + + Monitors = await q.OrderBy(m => m.GroupName).ThenBy(m => m.Name).ToListAsync(); + + var ids = Monitors.Select(m => m.Id).ToList(); + var cutoff = DateTime.UtcNow.AddDays(-30); + var checks = await _db.MonitorChecks.Where(c => ids.Contains(c.MonitorId) && c.CheckedAt >= cutoff).ToListAsync(); + foreach (var m in Monitors) + { + var mc = checks.Where(c => c.MonitorId == m.Id).ToList(); + UptimePercent[m.Id] = mc.Any() ? (double)mc.Count(c => c.Status == MonitorStatus.Up) / mc.Count * 100 : 0; + } + } +} diff --git a/EonaCat.LogStack.Status/Pages/Shared/_Layout.cshtml b/EonaCat.LogStack.Status/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..5400086 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/Shared/_Layout.cshtml @@ -0,0 +1,162 @@ + + + + + + + + @ViewData["Title"] - Status + + + + @RenderSection("Styles", required: false) + + +
+ + +
+
+ +
+ + + +
+ +
+ + @if (SyslogUdpService.IsRunning) + { +
+ +
+ } + + @if (Context.Session.GetString("IsAdmin") == "true") + { + ADMIN + } +
+
+ +
+ @RenderBody() +
+ + +
+
+ + +
+ + + @RenderSection("Scripts", required: false) + + diff --git a/EonaCat.LogStack.Status/Pages/_ViewImports.cshtml b/EonaCat.LogStack.Status/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..361f80f --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using EonaCat.LogStack.Status +@using EonaCat.LogStack.Status.Models +@using EonaCat.LogStack.Status.Services +@namespace EonaCat.LogStack.Status.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/EonaCat.LogStack.Status/Pages/_ViewStart.cshtml b/EonaCat.LogStack.Status/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/EonaCat.LogStack.Status/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/EonaCat.LogStack.Status/Program.cs b/EonaCat.LogStack.Status/Program.cs new file mode 100644 index 0000000..84ca0da --- /dev/null +++ b/EonaCat.LogStack.Status/Program.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Services; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +builder.Services.AddSession(options => +{ + options.IdleTimeout = TimeSpan.FromHours(8); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; +}); + +var dbPath = Path.Combine(builder.Environment.ContentRootPath, "EonaCat.LogStack.Status.db"); +builder.Services.AddDbContextFactory(options => options.UseSqlite($"Data Source={dbPath}")); + +// Register DatabaseContext directly as well (for controllers that inject it directly) +builder.Services.AddDbContext(options => options.UseSqlite($"Data Source={dbPath}")); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Ensure database is created and apply any pending migrations +using (var scope = app.Services.CreateScope()) +{ + var database = scope.ServiceProvider.GetRequiredService>().CreateDbContext(); + database.Database.EnsureCreated(); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.UseSession(); +app.MapRazorPages(); +app.MapControllers(); + +app.Run(); diff --git a/EonaCat.LogStack.Status/Properties/launchSettings.json b/EonaCat.LogStack.Status/Properties/launchSettings.json new file mode 100644 index 0000000..da9c479 --- /dev/null +++ b/EonaCat.LogStack.Status/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "EonaCat.LogStack.Status": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.Status/README.md b/EonaCat.LogStack.Status/README.md new file mode 100644 index 0000000..3ef48e1 --- /dev/null +++ b/EonaCat.LogStack.Status/README.md @@ -0,0 +1,119 @@ +# EonaCat.LogStack.Status 🟢 + +A self-hosted, application monitoring platform. + +## Features + +| Category | Details | +|----------|---------| +| **TCP Monitor** | Port connectivity checks | +| **UDP Monitor** | UDP reachability checks | +| **HTTP/HTTPS Monitor** | Full HTTP status monitoring | +| **App (Local)** | Monitor local processes by name | +| **App (Remote)** | Remote TCP-based app health | +| **Certificate Manager** | Track SSL certs, expiry, issuer | +| **Certificate Checker** | Auto-refresh cert details hourly | +| **Log Aggregator** | Ingest logs from any app via HTTP | +| **Log Viewer** | Search, filter, paginate log stream | +| **Dashboard** | Live status overview with uptime bars | +| **Admin Panel** | Full CRUD for all resources | +| **Visibility Control** | Per-monitor public/private toggle | +| **REST API** | Log ingestion + status endpoints | + +## Quick Start + +### Prerequisites +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8) + +### Run + +```bash +cd EonaCat.LogStack.Status +dotnet run +``` + +Open: http://localhost:8000 + +### Default Admin Password +`adminEonaCat` - **Change this immediately** in Settings! + +## Log Ingestion + +Send logs from any app: + +```bash +# Single log +curl -X POST http://localhost:8000/api/logs/ingest \ + -H "Content-Type: application/json" \ + -d '{"source":"my-app","level":"error","message":"Something broke"}' + +# Batch +curl -X POST http://localhost:8000/api/logs/batch \ + -H "Content-Type: application/json" \ + -d '[{"source":"app","level":"info","message":"Started"},{"source":"app","level":"warn","message":"High memory"}]' +``` + +### EonaCat.LogStack HTTP Flow +```csharp +var logger = new LogBuilder().WriteToHttp("http://localhost:8000/api/logs/eonacat").Build(); +``` + +### Serilog (.NET) +```csharp +// Install: Serilog.Sinks.Http +Log.Logger = new LoggerConfiguration() + .WriteTo.Http("http://localhost:8000/api/logs/serilog") + .CreateLogger(); +``` + +### Python +```python +import requests +requests.post("http://localhost:8000/api/logs/ingest", json={ + "source": "my-python-app", "level": "info", "message": "Hello" +}) +``` + +--- + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/status/summary` | Dashboard stats | +| `GET` | `/api/monitors` | All public monitors | +| `GET` | `/api/monitors/{id}/check` | Trigger check | +| `POST` | `/api/logs/ingest` | Ingest single log | +| `POST` | `/api/logs/batch` | Ingest log array | +| `POST` | `/api/logs/eonacat` | EonaCat.LogStack HTTP Flow | +| `POST` | `/api/logs/serilog` | Serilog HTTP sink | +| `GET` | `/api/logs` | Query logs (admin) | + +--- + +## Visibility Model + +- **Public monitors** are visible to everyone +- **Private monitors** are visible to admins only +- **Log viewer** is admin-only +- **Uptime %** can be toggled public/private in Settings +- **Certificates** are always visible (toggle per cert coming soon) + +--- + +## Production Deployment + +```bash +dotnet publish -c Release -o ./publish +# Run with: +./publish/EonaCat.LogStack.Status +``` + +Or with Docker: +```dockerfile +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY ./publish . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "EonaCat.LogStack.Status.dll"] +``` \ No newline at end of file diff --git a/EonaCat.LogStack.Status/Services/AuthenticationService.cs b/EonaCat.LogStack.Status/Services/AuthenticationService.cs new file mode 100644 index 0000000..565d3e1 --- /dev/null +++ b/EonaCat.LogStack.Status/Services/AuthenticationService.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; + +namespace EonaCat.LogStack.Status.Services; + +public class AuthenticationService +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + private readonly IDbContextFactory _dbFactory; + + public AuthenticationService(IDbContextFactory dbFactory) => _dbFactory = dbFactory; + + public async Task ValidatePasswordAsync(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + return false; + } + + await using var db = await _dbFactory.CreateDbContextAsync(); + var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == "AdminPasswordHash"); + if (setting == null) + { + return false; + } + + return BCrypt.Net.BCrypt.EnhancedVerify(password, setting.Value); + } + + public async Task ChangePasswordAsync(string currentPassword, string newPassword) + { + if (string.IsNullOrWhiteSpace(currentPassword)) + { + return false; + } + + if (string.IsNullOrWhiteSpace(newPassword)) + { + return false; + } + + if (!await ValidatePasswordAsync(currentPassword)) + { + return false; + } + + await using var db = await _dbFactory.CreateDbContextAsync(); + var setting = await db.Settings.FirstAsync(s => s.Key == "AdminPasswordHash"); + setting.Value = BCrypt.Net.BCrypt.EnhancedHashPassword(newPassword); + await db.SaveChangesAsync(); + return true; + } + + public async Task GetSettingAsync(string key, string defaultValue = "") + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var s = await db.Settings.FirstOrDefaultAsync(x => x.Key == key); + return s?.Value ?? defaultValue; + } + + public async Task SetSettingAsync(string key, string value) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var setting = await db.Settings.FirstOrDefaultAsync(x => x.Key == key); + if (setting == null) + { + db.Settings.Add(new Models.AppSettings { Key = key, Value = value }); + } + else + { + setting.Value = value; + } + await db.SaveChangesAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Services/MonitoringService.cs b/EonaCat.LogStack.Status/Services/MonitoringService.cs new file mode 100644 index 0000000..c6a2e1a --- /dev/null +++ b/EonaCat.LogStack.Status/Services/MonitoringService.cs @@ -0,0 +1,537 @@ +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using EonaCat.LogStack.Status.Data; +using EonaCat.LogStack.Status.Models; +using Monitor = EonaCat.LogStack.Status.Models.Monitor; + +namespace EonaCat.LogStack.Status.Services; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class MonitoringService +{ + private readonly IDbContextFactory _dbFactory; + private readonly ILogger _log; + + public MonitoringService(IDbContextFactory dbFactory, ILogger log) + { + _dbFactory = dbFactory; + _log = log; + } + + // check ─ + + public async Task CheckMonitorAsync(Monitor monitor) + { + var sw = Stopwatch.StartNew(); + MonitorStatus status; + string? message = null; + + try + { + (status, message) = monitor.Type switch + { + MonitorType.TCP => await CheckTcpAsync(monitor.Host, monitor.Port ?? 80, monitor.TimeoutMs), + MonitorType.UDP => await CheckUdpAsync(monitor.Host, monitor.Port ?? 53, monitor.TimeoutMs), + MonitorType.Ping => await CheckPingAsync(monitor.Host, monitor.TimeoutMs), + MonitorType.AppLocal => CheckLocalProcess(monitor.ProcessName ?? monitor.Name), + MonitorType.AppRemote => await CheckTcpAsync(monitor.Host, monitor.Port ?? 80, monitor.TimeoutMs), + MonitorType.HTTP => await CheckHttpAsync(monitor.Url ?? $"http://{monitor.Host}", monitor.TimeoutMs, monitor.ExpectedKeyword, monitor.ExpectedStatusCode), + MonitorType.HTTPS => await CheckHttpAsync(monitor.Url ?? $"https://{monitor.Host}", monitor.TimeoutMs, monitor.ExpectedKeyword, monitor.ExpectedStatusCode), + _ => (MonitorStatus.Unknown, "Unknown monitor type") + }; + } + catch (Exception ex) + { + status = MonitorStatus.Down; + message = ex.Message; + } + + sw.Stop(); + + // failure threshold ─ + if (status == MonitorStatus.Down || status == MonitorStatus.Warning) + { + monitor.ConsecutiveFailures++; + if (monitor.ConsecutiveFailures < monitor.FailureThreshold) + { + // Not enough consecutive failures yet - keep previous status + status = monitor.LastStatus == MonitorStatus.Unknown ? MonitorStatus.Unknown : monitor.LastStatus; + message = $"[Grace: {monitor.ConsecutiveFailures}/{monitor.FailureThreshold}] {message}"; + } + } + else + { + monitor.ConsecutiveFailures = 0; + } + + var check = new MonitorCheck + { + MonitorId = monitor.Id, + Status = status, + ResponseMs = sw.Elapsed.TotalMilliseconds, + Message = message, + CheckedAt = DateTime.UtcNow + }; + + await using var db = await _dbFactory.CreateDbContextAsync(); + + var prevStatus = monitor.LastStatus; + + db.MonitorChecks.Add(check); + monitor.LastChecked = DateTime.UtcNow; + monitor.LastStatus = status; + monitor.LastResponseMs = check.ResponseMs; + db.Monitors.Update(monitor); + await db.SaveChangesAsync(); + await EvaluateAlertRulesAsync(monitor, check, prevStatus, db); + + return check; + } + + private async Task<(MonitorStatus, string?)> CheckTcpAsync(string host, int port, int timeoutMs) + { + using var client = new TcpClient(); + var cts = new CancellationTokenSource(timeoutMs); + try + { + await client.ConnectAsync(host, port, cts.Token); + return (MonitorStatus.Up, $"Connected to {host}:{port}"); + } + catch (OperationCanceledException) + { + return (MonitorStatus.Down, $"Timeout connecting to {host}:{port}"); + } + catch (Exception ex) + { + return (MonitorStatus.Down, ex.Message); + } + } + + private async Task<(MonitorStatus, string?)> CheckUdpAsync(string host, int port, int timeoutMs) + { + try + { + using var udp = new UdpClient(); + udp.Connect(host, port); + var data = new byte[] { 0x00 }; + await udp.SendAsync(data, data.Length); + return (MonitorStatus.Up, $"UDP {host}:{port} reachable"); + } + catch (Exception ex) + { + return (MonitorStatus.Warning, $"UDP check: {ex.Message}"); + } + } + + /// ICMP ping check. + private async Task<(MonitorStatus, string?)> CheckPingAsync(string host, int timeoutMs) + { + try + { + using var ping = new Ping(); + var reply = await ping.SendPingAsync(host, timeoutMs); + if (reply.Status == IPStatus.Success) + { + return (MonitorStatus.Up, $"Ping {host} = {reply.RoundtripTime}ms TTL={reply.Options?.Ttl}"); + } + + return (MonitorStatus.Down, $"Ping {host}: {reply.Status}"); + } + catch (Exception ex) + { + return (MonitorStatus.Down, $"Ping error: {ex.Message}"); + } + } + + private (MonitorStatus, string?) CheckLocalProcess(string processName) + { + var procs = Process.GetProcessesByName(processName); + if (procs.Length > 0) + { + return (MonitorStatus.Up, $"Process '{processName}' running (PID: {procs[0].Id})"); + } + + return (MonitorStatus.Down, $"Process '{processName}' not found"); + } + + private async Task<(MonitorStatus, string?)> CheckHttpAsync(string url, int timeoutMs, string? expectedKeyword, int? expectedStatusCode) + { + using var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + using var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(timeoutMs) }; + try + { + var resp = await client.GetAsync(url); + var code = (int)resp.StatusCode; + string? body = null; + + // Keyword assertion + if (!string.IsNullOrEmpty(expectedKeyword)) + { + body = await resp.Content.ReadAsStringAsync(); + if (!body.Contains(expectedKeyword, StringComparison.OrdinalIgnoreCase)) + { + return (MonitorStatus.Down, $"HTTP {code} - keyword '{expectedKeyword}' not found"); + } + } + + // Status code assertion + if (expectedStatusCode.HasValue) + { + if (code == expectedStatusCode.Value) + { + return (MonitorStatus.Up, $"HTTP {code} (expected)"); + } + + return code >= 200 && code < 400 + ? (MonitorStatus.Warning, $"HTTP {code} (expected {expectedStatusCode})") + : (MonitorStatus.Down, $"HTTP {code} (expected {expectedStatusCode})"); + } + + if (code >= 200 && code < 400) + { + return (MonitorStatus.Up, $"HTTP {code}"); + } + + if (code >= 400 && code < 500) + { + return (MonitorStatus.Warning, $"HTTP {code}"); + } + + return (MonitorStatus.Down, $"HTTP {code}"); + } + catch (TaskCanceledException) + { + return (MonitorStatus.Down, "Timeout"); + } + catch (Exception ex) + { + return (MonitorStatus.Down, ex.Message); + } + } + + public async Task CheckCertificateAsync(CertificateEntry cert) + { + try + { + using var client = new TcpClient(); + await client.ConnectAsync(cert.Domain, cert.Port); + using var ssl = new SslStream(client.GetStream(), false, (_, c, _, _) => true); + await ssl.AuthenticateAsClientAsync(cert.Domain); + + var x509 = ssl.RemoteCertificate as X509Certificate2 + ?? new X509Certificate2(ssl.RemoteCertificate!); + + cert.ExpiresAt = x509.NotAfter.ToUniversalTime(); + cert.IssuedAt = x509.NotBefore.ToUniversalTime(); + cert.Issuer = x509.Issuer; + cert.Subject = x509.Subject; + cert.Thumbprint = x509.Thumbprint; + cert.LastError = null; + } + catch (Exception ex) + { + cert.LastError = ex.Message; + } + + cert.LastChecked = DateTime.UtcNow; + + await using var db = await _dbFactory.CreateDbContextAsync(); + db.Certificates.Update(cert); + await db.SaveChangesAsync(); + return cert; + } + + public async Task GetStatsAsync(bool isAdmin) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var monitors = await db.Monitors.Where(m => m.IsActive && (isAdmin || m.IsPublic)).ToListAsync(); + var certs = await db.Certificates.ToListAsync(); + var now = DateTime.UtcNow; + + return new DashboardStats + { + TotalMonitors = monitors.Count, + UpCount = monitors.Count(m => m.LastStatus == MonitorStatus.Up), + DownCount = monitors.Count(m => m.LastStatus == MonitorStatus.Down), + WarnCount = monitors.Count(m => m.LastStatus == MonitorStatus.Warning || m.LastStatus == MonitorStatus.Degraded), + UnknownCount = monitors.Count(m => m.LastStatus == MonitorStatus.Unknown), + CertCount = certs.Count, + CertExpiringSoon = certs.Count(c => c.ExpiresAt.HasValue && c.ExpiresAt.Value > now && (c.ExpiresAt.Value - now).TotalDays <= 30), + CertExpired = certs.Count(c => c.ExpiresAt.HasValue && c.ExpiresAt.Value <= now), + TotalLogs = await db.Logs.LongCountAsync(), + ErrorLogs = await db.Logs.LongCountAsync(l => l.Level == "error" || l.Level == "critical"), + OverallUptime = monitors.Count > 0 ? (double)monitors.Count(m => m.LastStatus == MonitorStatus.Up) / monitors.Count * 100 : 0, + ActiveIncidents = await db.Incidents.CountAsync(i => i.Status != IncidentStatus.Resolved), + ResolvedIncidents = await db.Incidents.CountAsync(i => i.Status == IncidentStatus.Resolved) + }; + } + + /// + /// Returns uptime percentages and response time stats for a single monitor. + /// + public async Task GetUptimeReportAsync(int monitorId) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var monitor = await db.Monitors.FindAsync(monitorId); + if (monitor == null) + { + throw new KeyNotFoundException($"Monitor {monitorId} not found."); + } + + var now = DateTime.UtcNow; + + var checks24h = await db.MonitorChecks + .Where(c => c.MonitorId == monitorId && c.CheckedAt >= now.AddHours(-24)) + .ToListAsync(); + + var checks7d = await db.MonitorChecks + .Where(c => c.MonitorId == monitorId && c.CheckedAt >= now.AddDays(-7)) + .ToListAsync(); + + var checks30d = await db.MonitorChecks + .Where(c => c.MonitorId == monitorId && c.CheckedAt >= now.AddDays(-30)) + .ToListAsync(); + + static double CalcUptime(List list) => + list.Count == 0 ? 100.0 : (double)list.Count(c => c.Status == MonitorStatus.Up) / list.Count * 100.0; + + return new UptimeReport + { + MonitorId = monitorId, + MonitorName = monitor.Name, + Uptime24h = CalcUptime(checks24h), + Uptime7d = CalcUptime(checks7d), + Uptime30d = CalcUptime(checks30d), + TotalChecks = checks30d.Count, + UpChecks = checks30d.Count(c => c.Status == MonitorStatus.Up), + DownChecks = checks30d.Count(c => c.Status == MonitorStatus.Down), + AvgResponseMs = checks30d.Count > 0 ? checks30d.Average(c => c.ResponseMs) : 0 + }; + } + + /// + /// Returns log volume bucketed by hour for the last hours. + /// + public async Task> GetLogStatsAsync(int hours = 24) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var from = DateTime.UtcNow.AddHours(-hours); + var logs = await db.Logs.Where(l => l.Timestamp >= from).ToListAsync(); + + return logs + .GroupBy(l => new DateTime(l.Timestamp.Year, l.Timestamp.Month, l.Timestamp.Day, l.Timestamp.Hour, 0, 0, DateTimeKind.Utc)) + .OrderBy(g => g.Key) + .Select(g => new LogStatsBucket + { + BucketStart = g.Key, + Total = g.LongCount(), + Errors = g.LongCount(l => l.Level == "error" || l.Level == "critical"), + Warnings = g.LongCount(l => l.Level == "warn" || l.Level == "warning") + }) + .ToList(); + } + + private async Task EvaluateAlertRulesAsync(Monitor monitor, MonitorCheck check, MonitorStatus prevStatus, DatabaseContext db) + { + var rules = await db.AlertRules + .Where(r => r.IsEnabled && (r.MonitorId == monitor.Id || r.MonitorId == null)) + .ToListAsync(); + + var globalWebhook = await db.Settings.FirstOrDefaultAsync(s => s.Key == "AlertWebhookUrl"); + var webhookUrl = globalWebhook?.Value; + + foreach (var rule in rules) + { + bool fired = rule.Condition switch + { + AlertRuleCondition.IsDown => check.Status == MonitorStatus.Down && prevStatus != MonitorStatus.Down, + AlertRuleCondition.IsUp => check.Status == MonitorStatus.Up && prevStatus == MonitorStatus.Down, + AlertRuleCondition.ResponseAboveMs => check.ResponseMs > (rule.ThresholdValue ?? double.MaxValue), + AlertRuleCondition.CertExpiresWithinDays => false, // evaluated by cert loop separately + _ => false + }; + + if (!fired) + { + continue; + } + + // Cooldown check + if (rule.LastFiredAt.HasValue && + (DateTime.UtcNow - rule.LastFiredAt.Value).TotalMinutes < rule.CooldownMinutes) + { + continue; + } + + rule.LastFiredAt = DateTime.UtcNow; + db.AlertRules.Update(rule); + + // Auto-create incident when a monitor goes down + var autoIncidents = await db.Settings.FirstOrDefaultAsync(s => s.Key == "AutoCreateIncidents"); + if (autoIncidents?.Value == "true" && rule.Condition == AlertRuleCondition.IsDown) + { + var incident = new Incident + { + Title = $"{monitor.Name} is down", + Body = check.Message, + Severity = IncidentSeverity.Major, + Status = IncidentStatus.Investigating, + MonitorId = monitor.Id, + IsPublic = monitor.IsPublic + }; + db.Incidents.Add(incident); + } + + // Fire webhook + var target = rule.WebhookUrl ?? webhookUrl; + if (!string.IsNullOrEmpty(target)) + { + _ = Task.Run(() => FireWebhookAsync(target, monitor, check, rule.Condition)); + } + + await db.SaveChangesAsync(); + } + } + + private async Task FireWebhookAsync(string url, Monitor monitor, MonitorCheck check, AlertRuleCondition condition) + { + try + { + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + var payload = JsonSerializer.Serialize(new + { + monitorId = monitor.Id, + monitorName = monitor.Name, + condition = condition.ToString(), + status = check.Status.ToString(), + responseMs = check.ResponseMs, + message = check.Message, + checkedAt = check.CheckedAt.ToString("o") + }); + await client.PostAsync(url, new StringContent(payload, Encoding.UTF8, "application/json")); + } + catch (Exception ex) + { + _log.LogWarning("Webhook delivery to {Url} failed: {Msg}", url, ex.Message); + } + } +} + +public class MonitoringBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _log; + + public MonitoringBackgroundService(IServiceScopeFactory scopeFactory, ILogger log) + { + _scopeFactory = scopeFactory; + _log = log; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var dbFactory = scope.ServiceProvider.GetRequiredService>(); + + await using var db = await dbFactory.CreateDbContextAsync(stoppingToken); + var monitors = await db.Monitors.Where(m => m.IsActive).ToListAsync(stoppingToken); + var now = DateTime.UtcNow; + + foreach (var m in monitors) + { + if (m.LastChecked == null || (now - m.LastChecked.Value).TotalSeconds >= m.IntervalSeconds) + { + var captured = m; + _ = Task.Run(async () => + { + using var checkScope = _scopeFactory.CreateScope(); + var svc = checkScope.ServiceProvider.GetRequiredService(); + await svc.CheckMonitorAsync(captured); + }, stoppingToken); + } + } + + // Check certs every hour + var certs = await db.Certificates.ToListAsync(stoppingToken); + foreach (var c in certs) + { + if (c.LastChecked == null || (now - c.LastChecked.Value).TotalHours >= 1) + { + var captured = c; + _ = Task.Run(async () => + { + using var certScope = _scopeFactory.CreateScope(); + var svc = certScope.ServiceProvider.GetRequiredService(); + await svc.CheckCertificateAsync(captured); + }, stoppingToken); + } + } + + // Log retention purge - run once per hour + if (now.Minute == 0) + { + using var purgeScope = _scopeFactory.CreateScope(); + var ingest = purgeScope.ServiceProvider.GetRequiredService(); + var auth = purgeScope.ServiceProvider.GetRequiredService(); + var days = int.TryParse(await auth.GetSettingAsync("MaxLogRetentionDays", "30"), out var d) ? d : 30; + await ingest.PurgeOldLogsAsync(days); + } + } + catch (Exception ex) + { + _log.LogError(ex, "Error in monitor loop"); + } + + await Task.Delay(10_000, stoppingToken); + } + } +} + +public class IngestionService +{ + private readonly IDbContextFactory _dbFactory; + + public IngestionService(IDbContextFactory dbFactory) + { + _dbFactory = dbFactory; + } + + public async Task IngestAsync(LogEntry entry) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + db.Logs.Add(entry); + await db.SaveChangesAsync(); + } + + public async Task IngestBatchAsync(IEnumerable entries) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + db.Logs.AddRange(entries); + await db.SaveChangesAsync(); + } + + public async Task PurgeOldLogsAsync(int retentionDays) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var cutoff = DateTime.UtcNow.AddDays(-retentionDays); + // Use ExecuteDeleteAsync for efficiency with large tables + await db.Logs.Where(l => l.Timestamp < cutoff).ExecuteDeleteAsync(); + } +} diff --git a/EonaCat.LogStack.Status/Services/SyslogService.cs b/EonaCat.LogStack.Status/Services/SyslogService.cs new file mode 100644 index 0000000..f4ce85a --- /dev/null +++ b/EonaCat.LogStack.Status/Services/SyslogService.cs @@ -0,0 +1,241 @@ +using EonaCat.LogStack.Status.Models; +using EonaCat.LogStack.Status.Services; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading.Channels; + +public class SyslogUdpService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IngestionService _ingestionService; + private readonly int _port; + + private readonly Channel _channel; + + private readonly IServiceScopeFactory _scopeFactory; + + public static bool IsRunning { get; private set; } + + public SyslogUdpService(ILogger logger, IServiceScopeFactory scopeFactory, IConfiguration config) + { + _logger = logger; + _scopeFactory = scopeFactory; + _port = config.GetValue("Syslog:Port", 514); + + _channel = Channel.CreateBounded(new BoundedChannelOptions(10_000) + { + FullMode = BoundedChannelFullMode.DropOldest + }); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var receiverTask = ReceiveLoop(stoppingToken); + var processorTask = ProcessLoop(stoppingToken); + + await Task.WhenAll(receiverTask, processorTask); + } + + private async Task ReceiveLoop(CancellationToken token) + { + using var udpClient = new UdpClient(_port); + udpClient.Client.ReceiveBufferSize = 4 * 1024 * 1024; + + _logger.LogInformation("Syslog UDP server listening on port {Port}", _port); + IsRunning = true; + + while (!token.IsCancellationRequested) + { + try + { + var result = await udpClient.ReceiveAsync(token); + + var message = Encoding.UTF8.GetString(result.Buffer); + var remoteIp = result.RemoteEndPoint.Address.ToString(); + + var entry = ParseMessage(message, remoteIp); + + if (entry != null) + { + await _channel.Writer.WriteAsync(entry, token); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Receive loop error"); + } + } + } + + private async Task ProcessLoop(CancellationToken token) + { + var batch = new List(100); + + while (await _channel.Reader.WaitToReadAsync(token)) + { + while (_channel.Reader.TryRead(out var entry)) + { + batch.Add(entry); + + if (batch.Count >= 100) + { + await Flush(batch); + } + } + + if (batch.Count > 0) + { + await Flush(batch); + } + } + } + + private async Task Flush(List batch) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var ingestionService = scope.ServiceProvider.GetRequiredService(); + + await ingestionService.IngestBatchAsync(batch.ToArray()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Batch ingestion failed"); + } + finally + { + batch.Clear(); + } + } + + private LogEntry ParseMessage(string rawMessage, string remoteIp) + { + try + { + if (IsJson(rawMessage)) + return ParseJson(rawMessage, remoteIp); + + return ParseSyslogAdvanced(rawMessage, remoteIp); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Parsing failed"); + + return new LogEntry + { + Source = "Syslog.Unknown", + Level = "Info", + Message = rawMessage, + Host = remoteIp, + Timestamp = DateTime.UtcNow + }; + } + } + + private bool IsJson(string input) + { + input = input.TrimStart(); + return input.StartsWith("{") || input.StartsWith("["); + } + + private LogEntry ParseJson(string json, string remoteIp) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + string Get(string name) => + root.TryGetProperty(name, out var val) ? val.ToString() : null; + + return new LogEntry + { + Source = Get("source") ?? "Custom.Json", + Level = MapLevel(Get("level")), + Message = Get("message"), + Exception = Get("exception"), + Host = Get("host") ?? remoteIp, + TraceId = Get("traceId"), + Properties = json, + Timestamp = DateTime.TryParse(Get("timestamp"), out var dt) + ? dt : DateTime.UtcNow + }; + } + + private LogEntry ParseSyslogAdvanced(string message, string remoteIp) + { + var entry = new LogEntry + { + Source = "Syslog", + Host = remoteIp, + Message = message, + Timestamp = DateTime.UtcNow, + Level = "Info" + }; + + try + { + // PRI parsing + if (message.StartsWith("<")) + { + var end = message.IndexOf('>'); + if (end > 0) + { + var pri = int.Parse(message[1..end]); + var severity = pri % 8; + entry.Level = MapSyslogSeverity(severity); + + message = message[(end + 1)..].Trim(); + } + } + + // Try RFC5424 parsing + var parts = message.Split(' ', 7, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length >= 7 && int.TryParse(parts[0], out _)) + { + entry.Timestamp = DateTime.TryParse(parts[1], out var ts) + ? ts : entry.Timestamp; + + entry.Host = parts[2]; + entry.Source = parts[3]; + entry.Message = parts[6]; + } + else + { + entry.Message = message; + } + } + catch + { + // fallback + } + + return entry; + } + + private string MapSyslogSeverity(int severity) => severity switch + { + 0 or 1 => "Fatal", + 2 or 3 => "Error", + 4 => "Warning", + 5 or 6 => "Info", + 7 => "Debug", + _ => "Info" + }; + + private string MapLevel(string level) => level?.ToLower() switch + { + "trace" => "Debug", + "debug" => "Debug", + "info" => "Info", + "warn" or "warning" => "Warning", + "error" => "Error", + "fatal" => "Fatal", + _ => "Info" + }; +} \ No newline at end of file diff --git a/EonaCat.LogStack.Status/appsettings.json b/EonaCat.LogStack.Status/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/EonaCat.LogStack.Status/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/EonaCat.LogStack.Status/wwwroot/css/site.css b/EonaCat.LogStack.Status/wwwroot/css/site.css new file mode 100644 index 0000000..9facd90 --- /dev/null +++ b/EonaCat.LogStack.Status/wwwroot/css/site.css @@ -0,0 +1,803 @@ +/* This file is part of the EonaCat project(s) which is released under the Apache License. */ +/* See the LICENSE file or go to https://EonaCat.com/License for full license details. */ + +:root { + --bg-base: #0a0b0e; + --bg-surface: #0f1117; + --bg-elevated: #161922; + --bg-card: #1a1d28; + --bg-hover: #1f2335; + --border: #252836; + --border-light: #2e3347; + --text-primary: #e8eaf0; + --text-secondary: #8b8fa8; + --text-muted: #4e5268; + --accent: #00d4aa; + --accent-dim: rgba(0,212,170,0.12); + --accent-glow: rgba(0,212,170,0.3); + --up: #00d4aa; + --down: #ff4b6e; + --warn: #ffb547; + --unknown: #5c6080; + --info: #5b9cf6; + --font-mono: 'Space Mono', monospace; + --font-body: 'DM Sans', sans-serif; + --radius: 6px; + --shadow: 0 1px 3px rgba(0,0,0,0.4); + --shadow-lg: 0 8px 32px rgba(0,0,0,0.5); +} + +.page-footer { + text-align: center; + font-size: 0.5rem; + color: var(--text-muted); + margin-top: 2rem; + margin-bottom: 1px; +} + + .page-footer a { + color: inherit; + text-decoration: none; + } + + .page-footer a:hover, + .page-footer a:active, + .page-footer a:visited { + color: inherit; + text-decoration: none; + } + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: var(--bg-base); + color: var(--text-primary); + font-family: var(--font-body); + font-size: 14px; + line-height: 1.6; + min-height: 100vh; +} + +/* Wrapper */ +.app-shell { display: flex; min-height: 100vh; } + +/* Sidebar */ +.sidebar { + width: 220px; + background: var(--bg-surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: fixed; + top: 0; left: 0; bottom: 0; + z-index: 100; + transition: width 0.25s ease; + overflow: hidden; +} +.sidebar.collapsed { width: 56px; } + +/* hide text/labels when collapsed */ +.sidebar.collapsed .logo-text, +.sidebar.collapsed .nav-label, +.sidebar.collapsed .nav-text, +.sidebar.collapsed .sidebar-footer, +.sidebar.collapsed .status-dot { display: none; } + +/* centre icons when collapsed */ +.sidebar.collapsed .nav-item { + justify-content: center; + padding: 10px 0; + gap: 0; +} +.sidebar.collapsed .sidebar-header { + justify-content: center; + padding: 18px 0 14px; +} +.sidebar.collapsed .sidebar-toggle { margin: 0; } + +/* tooltip on collapsed nav items */ +.sidebar.collapsed .nav-item { position: relative; } +.sidebar.collapsed .nav-item::after { + content: attr(data-label); + position: absolute; + left: calc(100% + 10px); + top: 50%; + transform: translateY(-50%); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 4px 10px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-primary); + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s; + z-index: 200; +} +.sidebar.collapsed .nav-item:hover::after { opacity: 1; } + +.sidebar-header { + padding: 18px 16px 14px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); +} + +.logo { display: flex; align-items: center; gap: 10px; } +.logo-icon { color: var(--accent); font-size: 18px; } +.logo-text { font-family: var(--font-mono); font-size: 13px; letter-spacing: -0.5px; color: var(--text-primary); } +.logo-text strong { color: var(--accent); } + +.sidebar-toggle { + background: none; border: none; color: var(--text-muted); + cursor: pointer; font-size: 14px; padding: 2px 6px; + border-radius: 3px; transition: color 0.2s; +} +.sidebar-toggle:hover { color: var(--text-primary); } + +.nav-section { + padding: 12px 0 4px; + border-bottom: 1px solid var(--border); +} +.nav-section:last-of-type { border-bottom: none; margin-top: auto; } + +.nav-label { + display: block; + padding: 4px 16px 6px; + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 1.5px; + color: var(--text-muted); +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px; + color: var(--text-secondary); + text-decoration: none; + font-size: 13px; + font-weight: 500; + transition: all 0.15s; + position: relative; + border-left: 2px solid transparent; +} +.nav-item:hover { background: var(--bg-elevated); color: var(--text-primary); border-left-color: var(--border-light); } +.nav-item.active { background: var(--accent-dim); color: var(--accent); border-left-color: var(--accent); } +.nav-item--danger:hover { color: var(--down); border-left-color: var(--down); } +.nav-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; } + +.status-dot { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--unknown); + margin-left: auto; +} + +.sidebar-footer { + padding: 12px 16px; + border-top: 1px solid var(--border); + display: flex; + align-items: center; + gap: 8px; +} +.clock { font-family: var(--font-mono); font-size: 11px; color: var(--accent); } + +/* Main */ +.main-content { + margin-left: 220px; + flex: 1; + display: flex; + flex-direction: column; + transition: margin-left 0.25s ease; + min-height: 100vh; +} +.sidebar.collapsed ~ .main-content, +body.sidebar-collapsed .main-content { margin-left: 56px; } + +.topbar { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + padding: 0 24px; + height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 50; +} + +.breadcrumb { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} + +.topbar-right { display: flex; align-items: center; gap: 16px; } + +.live-indicator { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--accent); + letter-spacing: 1.5px; +} + +.pulse-dot { + width: 7px; height: 7px; + background: var(--accent); + border-radius: 50%; + animation: pulse 2s infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.8); } +} + +.admin-badge { + background: rgba(91,156,246,0.15); + color: var(--info); + border: 1px solid rgba(91,156,246,0.3); + padding: 2px 8px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 1.5px; +} + +.page-content { padding: 24px; flex: 1; } + +/* Cards */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +.card-header { + padding: 14px 18px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} +.card-title { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 1px; + color: var(--text-secondary); + text-transform: uppercase; +} +.card-body { padding: 18px; } + +/* Stats */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} +.stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + position: relative; + overflow: hidden; + transition: border-color 0.2s; +} +.stat-card:hover { border-color: var(--border-light); } +.stat-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; +} +.stat-card.up::before { background: var(--up); } +.stat-card.down::before { background: var(--down); } +.stat-card.warn::before { background: var(--warn); } +.stat-card.info::before { background: var(--info); } +.stat-card.neutral::before { background: var(--border-light); } + +.stat-label { + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 1.5px; + color: var(--text-muted); + text-transform: uppercase; + margin-bottom: 8px; +} +.stat-value { + font-family: var(--font-mono); + font-size: 28px; + font-weight: 700; + line-height: 1; + color: var(--text-primary); +} +.stat-card.up .stat-value { color: var(--up); } +.stat-card.down .stat-value { color: var(--down); } +.stat-card.warn .stat-value { color: var(--warn); } +.stat-sub { + font-size: 11px; + color: var(--text-muted); + margin-top: 4px; +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; +} +.badge-up { background: rgba(0,212,170,0.12); color: var(--up); border: 1px solid rgba(0,212,170,0.25); } +.badge-down { background: rgba(255,75,110,0.12); color: var(--down); border: 1px solid rgba(255,75,110,0.25); } +.badge-warn { background: rgba(255,181,71,0.12); color: var(--warn); border: 1px solid rgba(255,181,71,0.25); } +.badge-unknown { background: rgba(92,96,128,0.12); color: var(--unknown); border: 1px solid rgba(92,96,128,0.25); } +.badge-info { background: rgba(91,156,246,0.12); color: var(--info); border: 1px solid rgba(91,156,246,0.25); } + +.badge::before { content: '●'; font-size: 6px; } + +/* Monitoring table */ +.monitor-grid { + display: grid; + gap: 8px; +} +.monitor-row { + display: grid; + grid-template-columns: 1fr auto auto auto auto; + gap: 12px; + align-items: center; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px 16px; + transition: border-color 0.15s; +} +.monitor-row:hover { border-color: var(--border-light); } +.monitor-name { font-weight: 500; color: var(--text-primary); font-size: 13px; } +.monitor-host { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); } +.monitor-type { font-family: var(--font-mono); font-size: 10px; color: var(--text-secondary); } +.monitor-latency { font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); width: 70px; text-align: right; } + +/* Table */ +.data-table { width: 100%; border-collapse: collapse; } +.data-table th { + text-align: left; + padding: 8px 12px; + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 1.5px; + color: var(--text-muted); + border-bottom: 1px solid var(--border); + text-transform: uppercase; +} +.data-table td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 13px; +} +.data-table tr:last-child td { border-bottom: none; } +.data-table tr:hover td { background: var(--bg-hover); } + +/* Form crap */ +.form-group { margin-bottom: 16px; } +.form-label { + display: block; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 1px; + color: var(--text-muted); + text-transform: uppercase; + margin-bottom: 6px; +} +.form-control { + width: 100%; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 8px 12px; + color: var(--text-primary); + font-family: var(--font-body); + font-size: 13px; + outline: none; + transition: border-color 0.2s; +} +.form-control:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); } +.form-control::placeholder { color: var(--text-muted); } +select.form-control { cursor: pointer; } + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.5px; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.15s; + text-decoration: none; + white-space: nowrap; +} +.btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 700; } +.btn-primary:hover { background: #00f0c0; } +.btn-outline { background: transparent; color: var(--text-secondary); border-color: var(--border); } +.btn-outline:hover { border-color: var(--border-light); color: var(--text-primary); background: var(--bg-elevated); } +.btn-danger { background: rgba(255,75,110,0.1); color: var(--down); border-color: rgba(255,75,110,0.3); } +.btn-danger:hover { background: rgba(255,75,110,0.2); } +.btn-sm { padding: 4px 10px; font-size: 10px; } + +/* Log viewer */ +.log-stream { + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 12px; + overflow-y: auto; + max-height: 600px; +} +.log-entry { + display: grid; + grid-template-columns: 160px 60px 130px 1fr; + gap: 8px; + padding: 5px 12px; + border-bottom: 1px solid rgba(255,255,255,0.03); + align-items: start; + transition: background 0.1s; +} +.log-entry:hover { background: var(--bg-elevated); } +.log-ts { color: var(--text-muted); font-size: 11px; } +.log-level { font-weight: 700; font-size: 10px; letter-spacing: 0.5px; } +.log-level.error, .log-level.critical { color: var(--down); } +.log-level.warn { color: var(--warn); } +.log-level.info { color: var(--info); } +.log-level.debug { color: var(--text-muted); } +.log-source { color: var(--text-secondary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.log-message { color: var(--text-primary); word-break: break-word; } + +/* certificate table */ +.cert-expiry-ok { color: var(--up); } +.cert-expiry-warn { color: var(--warn); } +.cert-expiry-critical { color: var(--down); } +.cert-expiry-expired { color: var(--down); font-weight: 700; } + +/* section header */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.section-title { + font-family: var(--font-mono); + font-size: 12px; + letter-spacing: 1px; + color: var(--text-primary); + text-transform: uppercase; + display: flex; + align-items: center; + gap: 8px; +} +.section-title::before { + content: ''; + display: block; + width: 3px; + height: 14px; + background: var(--accent); + border-radius: 2px; +} + +/* Uptime */ +.uptime-bar { + display: flex; + gap: 2px; + height: 24px; + align-items: center; +} +.uptime-block { + flex: 1; + height: 18px; + border-radius: 2px; + min-width: 4px; +} +.uptime-block.up { background: var(--up); opacity: 0.7; } +.uptime-block.down { background: var(--down); } +.uptime-block.warn { background: var(--warn); opacity: 0.7; } +.uptime-block.unknown { background: var(--bg-elevated); } + +/* Layouts */ +.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; } + +/* alerts */ +.alert { + padding: 10px 14px; + border-radius: var(--radius); + margin-bottom: 16px; + font-size: 13px; + display: flex; + align-items: center; + gap: 8px; +} +.alert-success { background: rgba(0,212,170,0.1); border: 1px solid rgba(0,212,170,0.25); color: var(--up); } +.alert-danger { background: rgba(255,75,110,0.1); border: 1px solid rgba(255,75,110,0.25); color: var(--down); } +.alert-warn, .alert-warning { background: rgba(255,181,71,0.1); border: 1px solid rgba(255,181,71,0.25); color: var(--warn); } +.alert-info { background: rgba(91,156,246,0.1); border: 1px solid rgba(91,156,246,0.25); color: var(--info); } + +/* Filtering */ +.filter-bar { + display: flex; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; + align-items: center; +} +.filter-bar .form-control { max-width: 200px; } + +/* Login */ +.login-wrap { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-base); +} +.login-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 40px; + width: 380px; + box-shadow: var(--shadow-lg); +} +.login-title { + font-family: var(--font-mono); + font-size: 18px; + color: var(--accent); + margin-bottom: 8px; +} +.login-sub { color: var(--text-muted); font-size: 13px; margin-bottom: 28px; } + +/* Toggle crap */ +.toggle { position: relative; display: inline-block; width: 40px; height: 22px; } +.toggle input { display: none; } +.toggle-slider { + position: absolute; + inset: 0; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 22px; + cursor: pointer; + transition: background 0.2s; +} +.toggle-slider::before { + content: ''; + position: absolute; + width: 16px; height: 16px; + left: 2px; top: 2px; + background: var(--text-muted); + border-radius: 50%; + transition: all 0.2s; +} +.toggle input:checked + .toggle-slider { background: var(--accent-dim); border-color: var(--accent); } +.toggle input:checked + .toggle-slider::before { transform: translateX(18px); background: var(--accent); } + +/* Empty state */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--text-muted); +} +.empty-state-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.3; } +.empty-state-text { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.5px; } + +/* Mobile crap */ +@media (max-width: 768px) { + .sidebar { width: 56px; } + .sidebar .logo-text, .sidebar .nav-label, + .sidebar .nav-text, + .sidebar .sidebar-footer { display: none; } + .sidebar .nav-item { justify-content: center; padding: 10px 0; gap: 0; } + .sidebar .sidebar-header { justify-content: center; padding: 18px 0 14px; } + .main-content { margin-left: 56px; } + .stats-grid { grid-template-columns: repeat(2, 1fr); } + .two-col, .three-col { grid-template-columns: 1fr; } + .monitor-row { grid-template-columns: 1fr auto; } + .monitor-latency, .monitor-type { display: none; } + .log-entry { grid-template-columns: 100px 50px 1fr; } + .log-source { display: none; } +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--bg-base); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--border-light); } + +/* Other crap */ +.mono { font-family: var(--font-mono); } +.text-muted { color: var(--text-muted); } +.text-up { color: var(--up); } +.text-down { color: var(--down); } +.text-warn { color: var(--warn); } +.mt-1 { margin-top: 8px; } .mt-2 { margin-top: 16px; } .mt-3 { margin-top: 24px; } +.mb-1 { margin-bottom: 8px; } .mb-2 { margin-bottom: 16px; } .mb-3 { margin-bottom: 24px; } +.flex { display: flex; } .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } +.align-center { align-items: center; } +.justify-between { justify-content: space-between; } + +/* MODAL */ +.modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.7); + z-index: 200; + align-items: center; + justify-content: center; +} +.modal-overlay.open { display: flex; } +.modal { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + width: 500px; + max-width: 95vw; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-lg); +} +.modal-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} +.modal-title { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1px; color: var(--text-primary); text-transform: uppercase; } +.modal-close { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 18px; } +.modal-close:hover { color: var(--text-primary); } +.modal-body { padding: 20px; } +.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; } + +/* Latency sparkline canvas inside monitor rows */ +.sparkline-wrap { display: flex; align-items: center; gap: 6px; } +.sparkline-canvas { display: block; } + +/* Response-time colour helpers */ +.rt-good { color: var(--up); } +.rt-ok { color: var(--warn); } +.rt-slow { color: var(--down); } + +/* Log stream toolbar */ +.log-toolbar { + display: flex; align-items: center; gap: 8px; + padding: 6px 12px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-bottom: none; + border-radius: var(--radius) var(--radius) 0 0; +} +.log-stream { border-radius: 0 0 var(--radius) var(--radius); } + +/* Highlight search hits */ +mark { + background: rgba(255,181,71,0.25); + color: var(--warn); + border-radius: 2px; + padding: 0 2px; +} + +/* Toast improvements */ +.toast-container { + position: fixed; bottom: 20px; right: 20px; + display: flex; flex-direction: column; gap: 8px; + z-index: 9999; +} +.toast { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 10px 14px; + font-size: 13px; + min-width: 240px; + animation: slideIn 0.25s ease; + display: flex; align-items: center; gap: 8px; +} +.toast.toast-success { border-color: rgba(0,212,170,0.4); color: var(--up); } +.toast.toast-error { border-color: rgba(255,75,110,0.4); color: var(--down); } +.toast.toast-warn { border-color: rgba(255,181,71,0.4); color: var(--warn); } +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* Chart wrapper */ +.chart-wrap { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + margin-top: 16px; +} +.chart-wrap canvas { max-height: 220px; } + +/* Keyboard shortcut hints */ +.kbd { + display: inline-block; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 3px; + padding: 1px 5px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary); +} + +/* Refresh countdown ring */ +.refresh-ring { + width: 18px; height: 18px; + position: relative; display: inline-flex; + align-items: center; justify-content: center; +} +.refresh-ring svg { position: absolute; top: 0; left: 0; transform: rotate(-90deg); } +.refresh-ring-track { fill: none; stroke: var(--border); stroke-width: 2; } +.refresh-ring-fill { fill: none; stroke: var(--accent); stroke-width: 2; + stroke-dasharray: 50.27; stroke-linecap: round; + transition: stroke-dashoffset 1s linear; } +.refresh-ring-label { font-family: var(--font-mono); font-size: 9px; color: var(--text-muted); } + +/* Incident timeline */ +.incident-timeline { border-left: 2px solid var(--border); padding-left: 16px; } +.incident-timeline-dot { + width: 8px; height: 8px; + border-radius: 50%; + background: var(--accent); + margin-left: -20px; + margin-right: 8px; + flex-shrink: 0; +} + +/* Mini tag chips */ +.tag-chip { + display: inline-block; + padding: 1px 6px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 10px; + font-size: 10px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +/* Live log badge */ +.live-count { + font-family: var(--font-mono); + font-size: 10px; + background: rgba(0,212,170,0.1); + border: 1px solid rgba(0,212,170,0.2); + color: var(--accent); + padding: 1px 6px; + border-radius: 10px; +} diff --git a/EonaCat.LogStack.Status/wwwroot/js/site.js b/EonaCat.LogStack.Status/wwwroot/js/site.js new file mode 100644 index 0000000..30ee4ae --- /dev/null +++ b/EonaCat.LogStack.Status/wwwroot/js/site.js @@ -0,0 +1,317 @@ +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +'use strict'; + +function updateClock() { + const now = new Date(); + const pad = n => String(n).padStart(2, '0'); + const el = document.getElementById('clock'); + if (el) el.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; +} +updateClock(); +setInterval(updateClock, 1000); + +function toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + const main = document.getElementById('main-content'); + const btn = sidebar && sidebar.querySelector('.sidebar-toggle'); + const collapsed = sidebar.classList.toggle('collapsed'); + + // Sync main content margin + if (main) main.style.marginLeft = collapsed ? '56px' : '220px'; + + // Flip the arrow glyph + if (btn) btn.textContent = collapsed ? '⟩' : '⟨'; + + // Persist preference + try { localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0'); } catch {} +} + +// Restore sidebar state on load +(function () { + try { + const pref = localStorage.getItem('sidebarCollapsed'); + if (pref === '1') { + const sidebar = document.getElementById('sidebar'); + const main = document.getElementById('main-content'); + const btn = sidebar && sidebar.querySelector('.sidebar-toggle'); + if (sidebar) { sidebar.classList.add('collapsed'); } + if (main) main.style.marginLeft = '56px'; + if (btn) btn.textContent = '⟩'; + } + } catch {} +})(); + +function openModal(id) { + const el = document.getElementById(id); + if (el) el.classList.add('open'); +} +function closeModal(id) { + const el = document.getElementById(id); + if (el) el.classList.remove('open'); +} +// Close modal on overlay click +document.addEventListener('click', function (e) { + if (e.target.classList.contains('modal-overlay')) { + e.target.classList.remove('open'); + } +}); +// Close modal on Escape +document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open')); + } +}); + +function showToast(msg, type = 'info', durationMs = 4000) { + const container = document.getElementById('toast-container'); + if (!container) return; + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + const icons = { success: '✓', error: '✕', warn: '⚠', info: 'ℹ' }; + toast.innerHTML = `${icons[type] ?? 'ℹ'}${msg}`; + container.appendChild(toast); + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s'; + setTimeout(() => toast.remove(), 300); + }, durationMs); +} + +const AUTO_REFRESH_SECS = 30; + +(function () { + const pageEl = document.querySelector('[data-autorefresh]'); + if (!pageEl) return; + + const ringEl = document.getElementById('refresh-ring'); + const fillEl = document.getElementById('refresh-ring-fill'); + const labelEl = document.getElementById('refresh-ring-label'); + const circumference = 50.27; // 2π × 8 + + if (ringEl) ringEl.style.display = 'inline-flex'; + + let remaining = AUTO_REFRESH_SECS; + + function tick() { + remaining--; + if (labelEl) labelEl.textContent = remaining; + if (fillEl) { + const offset = circumference * (1 - remaining / AUTO_REFRESH_SECS); + fillEl.style.strokeDashoffset = offset; + } + if (remaining <= 0) location.reload(); + } + + if (labelEl) labelEl.textContent = remaining; + if (fillEl) fillEl.style.strokeDashoffset = 0; + + setInterval(tick, 1000); +})(); + +fetch('/api/status/summary') + .then(r => r.json()) + .then(d => { + const dot = document.getElementById('overall-dot'); + if (!dot) return; + if (d.downCount > 0) dot.style.background = 'var(--down)'; + else if (d.warnCount > 0) dot.style.background = 'var(--warn)'; + else if (d.upCount > 0) dot.style.background = 'var(--up)'; + else dot.style.background = 'var(--unknown)'; + }) + .catch(() => {}); + +(function () { + const input = document.getElementById('log-search'); + if (!input) return; + let timer; + input.addEventListener('input', function () { + clearTimeout(timer); + timer = setTimeout(() => { + const form = input.closest('form'); + if (form) form.submit(); + }, 400); + }); +})(); + +function scrollLogsToBottom() { + const el = document.getElementById('log-stream'); + if (el) el.scrollTop = el.scrollHeight; +} +document.addEventListener('DOMContentLoaded', scrollLogsToBottom); + +(function () { + const params = new URLSearchParams(window.location.search); + const term = params.get('Search') || params.get('search'); + if (!term || term.length < 2) return; + + const stream = document.getElementById('log-stream'); + if (!stream) return; + + const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escaped})`, 'gi'); + + stream.querySelectorAll('.log-message').forEach(el => { + el.innerHTML = el.textContent.replace(regex, '$1'); + }); +})(); + +(function () { + const stream = document.getElementById('log-stream'); + const liveCountEl = document.getElementById('live-count'); + if (!stream || !liveCountEl) return; + + let lastId = parseInt(stream.dataset.lastId || '0', 10); + + async function pollLogs() { + try { + const res = await fetch(`/api/logs?page=1&pageSize=20`); + if (!res.ok) return; + const data = await res.json(); + const newEntries = (data.entries || []).filter(e => e.id > lastId); + if (newEntries.length === 0) return; + + newEntries.reverse().forEach(e => { + const div = document.createElement('div'); + div.className = 'log-entry'; + div.innerHTML = ` + ${e.timestamp.replace('T', ' ').substring(0, 19)} + ${e.level.toUpperCase()} + ${e.source || ''} + ${escHtml(e.message)}`; + stream.prepend(div); + lastId = Math.max(lastId, e.id); + }); + + if (liveCountEl) liveCountEl.textContent = `+${newEntries.length} new`; + setTimeout(() => { if (liveCountEl) liveCountEl.textContent = ''; }, 2500); + } catch {} + } + + setInterval(pollLogs, 5000); +})(); + +function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>'); +} + +document.addEventListener('keydown', function (e) { + // Don't fire inside inputs / textareas + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return; + + switch (e.key) { + case 'b': toggleSidebar(); break; // B = toggle sidebar + case 'g': + if (e.shiftKey) { window.location.href = '/'; } // G = dashboard + break; + case 'l': + if (e.shiftKey) { window.location.href = '/logs'; } // L = logs + break; + case 'm': + if (e.shiftKey) { window.location.href = '/monitors';} // M = monitors + break; + case 'r': location.reload(); break; // R = refresh + case '/': { // / = focus search + const s = document.getElementById('log-search'); + if (s) { e.preventDefault(); s.focus(); } + break; + } + } +}); + +function copyText(text, label) { + navigator.clipboard.writeText(text).then(() => { + showToast(`${label || 'Copied'} to clipboard`, 'success', 2000); + }).catch(() => showToast('Copy failed', 'error')); +} + +function confirmDelete(name) { + return confirm(`Delete "${name}"? This cannot be undone.`); +} + +function drawSparkline(canvas, values, color) { + if (!canvas || !values || values.length === 0) return; + const ctx = canvas.getContext('2d'); + const w = canvas.width, h = canvas.height; + const max = Math.max(...values, 1); + const min = 0; + const step = w / (values.length - 1 || 1); + + ctx.clearRect(0, 0, w, h); + ctx.strokeStyle = color || 'var(--accent)'; + ctx.lineWidth = 1.5; + ctx.lineJoin = 'round'; + ctx.beginPath(); + + values.forEach((v, i) => { + const x = i * step; + const y = h - ((v - min) / (max - min)) * h * 0.9 - h * 0.05; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + ctx.stroke(); +} + +function formatMs(ms) { + if (ms < 1000) return `${Math.round(ms)}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +function buildUptimeRing(pct, size = 36) { + const r = (size / 2) - 3; + const circ = 2 * Math.PI * r; + const fill = circ * (pct / 100); + const color = pct >= 99 ? 'var(--up)' : pct >= 95 ? 'var(--warn)' : 'var(--down)'; + return ` + + + + ${Math.round(pct)}%`; +} + +async function loadLogStatsChart(canvasId) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + try { + const res = await fetch('/api/logs/stats?hours=24'); + if (!res.ok) return; + const buckets = await res.json(); + if (!buckets.length) return; + + const labels = buckets.map(b => { + const d = new Date(b.bucketStart); + return `${String(d.getHours()).padStart(2,'0')}:00`; + }); + const totals = buckets.map(b => b.total); + const errors = buckets.map(b => b.errors); + const warnings = buckets.map(b => b.warnings); + + // Use Chart.js if available, otherwise draw with canvas API + if (window.Chart) { + new window.Chart(canvas, { + type: 'bar', + data: { + labels, + datasets: [ + { label: 'Total', data: totals, backgroundColor: 'rgba(91,156,246,0.25)', borderColor: 'rgba(91,156,246,0.7)', borderWidth: 1 }, + { label: 'Errors', data: errors, backgroundColor: 'rgba(255,75,110,0.25)', borderColor: 'rgba(255,75,110,0.7)', borderWidth: 1 }, + { label: 'Warnings', data: warnings, backgroundColor: 'rgba(255,181,71,0.2)', borderColor: 'rgba(255,181,71,0.6)', borderWidth: 1 }, + ] + }, + options: { + responsive: true, maintainAspectRatio: false, + plugins: { legend: { labels: { color: '#8b8fa8', font: { family: 'Space Mono', size: 10 } } } }, + scales: { + x: { ticks: { color: '#4e5268', font: { family: 'Space Mono', size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } }, + y: { ticks: { color: '#4e5268', font: { family: 'Space Mono', size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } } + } + } + }); + } else { + // Fallback: simple bar sparkline + drawSparkline(canvas, totals, 'rgba(91,156,246,0.7)'); + } + } catch {} +} diff --git a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj new file mode 100644 index 0000000..eeb6a69 --- /dev/null +++ b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj @@ -0,0 +1,46 @@ + + + + netstandard2.1 + enable + True + EonaCat.LogStack.Flows.WindowsEventLog + EonaCat (Jeroen Saey) + https://git.saey.me/EonaCat/EonaCat.LogStack + icon.png + README.md + https://git.saey.me/EonaCat/EonaCat.LogStack + EonaCat;Windows;EventLog;Flow + LICENSE + 0.0.1 + EonaCat (Jeroen Saey) + EonaCat.LogStack.Flows.WindowsEventLog + EonaCat (Jeroen Saey) + EonaCat Windows EventLog Flow for LogStack + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + + diff --git a/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs b/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs new file mode 100644 index 0000000..f7f8181 --- /dev/null +++ b/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs @@ -0,0 +1,32 @@ +using EonaCat.LogStack.Configuration; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EonaCat.LogStack.Flows.WindowsEventLog +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public static class EonaCatLogStackExtensions + { + /// + /// Write to Windows Event log + /// + public static LogBuilder WriteToWindowsEventLog(this LogBuilder logBuilder, + string sourceName = "EonaCatLogStack", + string logName = "Application", + int maxMessageLength = 30000, + Core.LogLevel minimumLevel = Core.LogLevel.Warning) + { + logBuilder.AddFlow(new WindowsEventLogFlow( + sourceName, + logName, + maxMessageLength, + minimumLevel)); + WindowsEventLogFlow.EnsureSourceExists(); + return logBuilder; + } + } +} diff --git a/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs b/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs new file mode 100644 index 0000000..06ff8b9 --- /dev/null +++ b/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs @@ -0,0 +1,256 @@ +using EonaCat.LogStack.Core; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Writes log events to the Windows Event Log. + /// + /// Requires the source to be registered before first use. + /// Call once during application setup + /// (requires elevated privileges the first time). + /// + /// .NET 4.8.1 compatible. Silently no-ops on non-Windows platforms. + /// + public sealed class WindowsEventLogFlow : FlowBase + { + private readonly string _sourceName; + private readonly string _logName; + private readonly int _maxMessageLength; + private System.Diagnostics.EventLog _eventLog; + private readonly object _initLock = new object(); + private volatile bool _initialized; + + public WindowsEventLogFlow( + string sourceName = "EonaCatLogStack", + string logName = "Application", + int maxMessageLength = 30000, + LogLevel minimumLevel = LogLevel.Warning) + : base("WindowsEventLog:" + sourceName, minimumLevel) + { + if (sourceName == null) + { + throw new ArgumentNullException("sourceName"); + } + + if (logName == null) + { + throw new ArgumentNullException("logName"); + } + + _sourceName = sourceName; + _logName = logName; + _maxMessageLength = maxMessageLength; + } + + /// + /// Registers the event source with the OS. Must be called with admin rights + /// the first time on each machine. Safe to call repeatedly. + /// + public static void EnsureSourceExists(string sourceName = "EonaCatLogStack", + string logName = "Application") + { + if (!IsWindows()) + { + return; + } + + try + { + if (!System.Diagnostics.EventLog.SourceExists(sourceName)) + { + System.Diagnostics.EventLog.CreateEventSource(sourceName, logName); + } + } + catch (Exception ex) + { + Console.Error.WriteLine("[WindowsEventLogFlow] Cannot create source: " + ex.Message); + } + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (!IsWindows()) + { + return Task.FromResult(WriteResult.Success); + } + + EnsureInitialized(); + if (_eventLog == null) + { + return Task.FromResult(WriteResult.Dropped); + } + + try + { + string msg = BuildMessage(logEvent); + if (msg.Length > _maxMessageLength) + { + msg = msg.Substring(0, _maxMessageLength) + "... [truncated]"; + } + + _eventLog.WriteEntry(msg, ToEventType(logEvent.Level), ToEventId(logEvent.Level)); + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + catch (Exception ex) + { + Console.Error.WriteLine("[WindowsEventLogFlow] Write error: " + ex.Message); + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (LogEvent e in logEvents.ToArray()) + { + if (IsLogLevelEnabled(e)) + { + BlastAsync(e, cancellationToken); + } + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(0); + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + if (_eventLog != null) { try { _eventLog.Dispose(); } catch { } } + await base.DisposeAsync().ConfigureAwait(false); + } + + private void EnsureInitialized() + { + if (_initialized) + { + return; + } + + lock (_initLock) + { + if (_initialized) + { + return; + } + + try + { + if (System.Diagnostics.EventLog.SourceExists(_sourceName)) + { + _eventLog = new System.Diagnostics.EventLog(_logName) { Source = _sourceName }; + } + else + { + Console.Error.WriteLine( + "[WindowsEventLogFlow] Source '" + _sourceName + + "' not registered. Call EnsureSourceExists() with admin rights."); + } + } + catch (Exception ex) + { + Console.Error.WriteLine("[WindowsEventLogFlow] Init error: " + ex.Message); + } + _initialized = true; + } + } + + private static string BuildMessage(LogEvent log) + { + var sb = new System.Text.StringBuilder(512); + sb.Append("Level: ").AppendLine(LevelString(log.Level)); + sb.Append("Category: ").AppendLine(log.Category ?? string.Empty); + sb.Append("Time: ").AppendLine(LogEvent.GetDateTime(log.Timestamp).ToString("O")); + sb.Append("Message: ").AppendLine(log.Message.Length > 0 ? log.Message.ToString() : string.Empty); + + if (log.Exception != null) + { + sb.Append("Exception: ").AppendLine(log.Exception.ToString()); + } + + if (log.Properties.Count > 0) + { + sb.AppendLine("Properties:"); + foreach (var kv in log.Properties.ToArray()) + { + sb.Append(" ").Append(kv.Key).Append(" = ") + .AppendLine(kv.Value != null ? kv.Value.ToString() : "null"); + } + } + return sb.ToString(); + } + + private static System.Diagnostics.EventLogEntryType ToEventType(LogLevel level) + { + switch (level) + { + case LogLevel.Warning: return System.Diagnostics.EventLogEntryType.Warning; + case LogLevel.Error: + case LogLevel.Critical: return System.Diagnostics.EventLogEntryType.Error; + default: return System.Diagnostics.EventLogEntryType.Information; + } + } + + private static int ToEventId(LogLevel level) + { + // Stable event IDs per level for easy filtering in Event Viewer + switch (level) + { + case LogLevel.Trace: return 1000; + case LogLevel.Debug: return 1001; + case LogLevel.Information: return 1002; + case LogLevel.Warning: return 1003; + case LogLevel.Error: return 1004; + case LogLevel.Critical: return 1005; + default: return 1999; + } + } + + private static string LevelString(LogLevel level) + { + switch (level) + { + case LogLevel.Trace: return "TRACE"; + case LogLevel.Debug: return "DEBUG"; + case LogLevel.Information: return "INFO"; + case LogLevel.Warning: return "WARN"; + case LogLevel.Error: return "ERROR"; + case LogLevel.Critical: return "CRITICAL"; + default: return level.ToString().ToUpperInvariant(); + } + } + + private static bool IsWindows() + { +#if NET48 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET481 + return true; +#else + return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); +#endif + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack.sln b/EonaCat.LogStack.sln new file mode 100644 index 0000000..8603db3 --- /dev/null +++ b/EonaCat.LogStack.sln @@ -0,0 +1,140 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.LogStack", "EonaCat.LogStack\EonaCat.LogStack.csproj", "{DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.LogClient", "EonaCat.LogStack.LogClient\EonaCat.LogStack.LogClient.csproj", "{D1025803-9588-46EB-8771-88E25209B780}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{C9F66B51-6661-467A-9E22-E0E578EB76A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Flows.WindowsEventLog", "EonaCat.LogStack.WindowsEventLogFlow\EonaCat.LogStack.Flows.WindowsEventLog.csproj", "{F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.OpenTelemetryFlow", "EonaCat.LogStack.OpenTelemetryFlow\EonaCat.LogStack.OpenTelemetryFlow.csproj", "{CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.SerilogTest", "EonaCat.LogStack.SerilogTest\EonaCat.LogStack.SerilogTest.csproj", "{F360998D-46E0-5A88-BA3E-47A4162C8EB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Test.Web", "Testers\EonaCat.LogStack.Test.Web\EonaCat.LogStack.Test.Web.csproj", "{9240A706-1852-C232-FB58-E54A5A528135}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{85A2505C-8976-4046-963B-D7B63EF81E47}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Status", "EonaCat.LogStack.Status\EonaCat.LogStack.Status.csproj", "{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x64.Build.0 = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x86.Build.0 = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.Build.0 = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x64.ActiveCfg = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x64.Build.0 = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x86.ActiveCfg = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x86.Build.0 = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|x64.Build.0 = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Debug|x86.Build.0 = Debug|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|Any CPU.Build.0 = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|x64.ActiveCfg = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|x64.Build.0 = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|x86.ActiveCfg = Release|Any CPU + {D1025803-9588-46EB-8771-88E25209B780}.Release|x86.Build.0 = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x64.Build.0 = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x86.Build.0 = Debug|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|Any CPU.Build.0 = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x64.ActiveCfg = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x64.Build.0 = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x86.ActiveCfg = Release|Any CPU + {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x86.Build.0 = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x64.Build.0 = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x86.Build.0 = Debug|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|Any CPU.Build.0 = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x64.ActiveCfg = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x64.Build.0 = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x86.ActiveCfg = Release|Any CPU + {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x86.Build.0 = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x64.Build.0 = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x86.Build.0 = Debug|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|Any CPU.Build.0 = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x64.ActiveCfg = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x64.Build.0 = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x86.ActiveCfg = Release|Any CPU + {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x86.Build.0 = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x64.ActiveCfg = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x64.Build.0 = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x86.Build.0 = Debug|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|Any CPU.Build.0 = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x64.ActiveCfg = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x64.Build.0 = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x86.ActiveCfg = Release|Any CPU + {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x86.Build.0 = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x64.ActiveCfg = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x64.Build.0 = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x86.ActiveCfg = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x86.Build.0 = Debug|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|Any CPU.Build.0 = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|x64.ActiveCfg = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|x64.Build.0 = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|x86.ActiveCfg = Release|Any CPU + {9240A706-1852-C232-FB58-E54A5A528135}.Release|x86.Build.0 = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|x64.ActiveCfg = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|x64.Build.0 = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|x86.ActiveCfg = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Debug|x86.Build.0 = Debug|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|Any CPU.Build.0 = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x64.ActiveCfg = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x64.Build.0 = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.ActiveCfg = Release|Any CPU + {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F} + EndGlobalSection +EndGlobal diff --git a/EonaCat.LogStack/Constants.cs b/EonaCat.LogStack/Constants.cs new file mode 100644 index 0000000..e98bb36 --- /dev/null +++ b/EonaCat.LogStack/Constants.cs @@ -0,0 +1,11 @@ +namespace EonaCat.LogStack; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class Constants +{ + public static class DateTimeFormats + { + public static string LOGGING { get; set; } = "yyyy-MM-dd HH:mm:ss.fff"; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/DllInfo.cs b/EonaCat.LogStack/DllInfo.cs new file mode 100644 index 0000000..35e42f2 --- /dev/null +++ b/EonaCat.LogStack/DllInfo.cs @@ -0,0 +1,27 @@ +using EonaCat.Versioning.Helpers; +using System.Reflection; +namespace EonaCat.LogStack; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class DllInfo +{ + public const string NAME = "EonaCatLogStack"; + public const string VERSION = "0.0.1"; + + static DllInfo() + { + var isDebug = false; +#if DEBUG + isDebug = true; +#endif + VersionName = isDebug ? "DEBUG" : "RELEASE"; + } + + internal static string VersionName { get; } + + public static string ApplicationName { get; set; } = "EonaCatLogStack"; + + public static string EonaCatVersion => VersionHelper.GetEonaCatVersion(Assembly.GetExecutingAssembly()); +} \ No newline at end of file diff --git a/EonaCat.LogStack/Enums.cs b/EonaCat.LogStack/Enums.cs new file mode 100644 index 0000000..7e4b835 --- /dev/null +++ b/EonaCat.LogStack/Enums.cs @@ -0,0 +1,199 @@ +using Microsoft.Extensions.Logging; + +namespace EonaCat.LogStack; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class LogTypeConverter +{ + public static Core.LogLevel FromLogLevel(this LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.None: + return Core.LogLevel.None; + case LogLevel.Error: + return Core.LogLevel.Error; + case LogLevel.Debug: + return Core.LogLevel.Debug; + case LogLevel.Critical: + return Core.LogLevel.Critical; + case LogLevel.Warning: + return Core.LogLevel.Warning; + case LogLevel.Trace: + return Core.LogLevel.Trace; + case LogLevel.Information: + return Core.LogLevel.Information; + default: + return Core.LogLevel.Trace; + } + } + + public static LogLevel ToLogLevel(this Core.LogLevel logLevel) + { + switch (logLevel) + { + case Core.LogLevel.None: + return LogLevel.None; + case Core.LogLevel.Error: + return LogLevel.Error; + case Core.LogLevel.Debug: + return LogLevel.Debug; + case Core.LogLevel.Critical: + return LogLevel.Critical; + case Core.LogLevel.Warning: + return LogLevel.Warning; + case Core.LogLevel.Trace: + return LogLevel.Trace; + case Core.LogLevel.Information: + return LogLevel.Information; + default: + return LogLevel.Information; + } + } + + public static string ToString(this Core.LogLevel logLevel) + { + switch (logLevel) + { + case Core.LogLevel.None: + return "NONE"; + case Core.LogLevel.Error: + return "ERROR"; + case Core.LogLevel.Debug: + return "DEBUG"; + case Core.LogLevel.Critical: + return "CRITICAL"; + case Core.LogLevel.Warning: + return "WARNING"; + case Core.LogLevel.Trace: + return "TRACE"; + case Core.LogLevel.Information: + return "INFO"; + default: + return "INFO"; + } + } + + public static Core.LogLevel FromSeverity(this ESeverity logLevel) + { + switch (logLevel) + { + case ESeverity.Debug: + return Core.LogLevel.Debug; + case ESeverity.Warn: + return Core.LogLevel.Warning; + case ESeverity.Emergency: + return Core.LogLevel.Trace; + case ESeverity.Critical: + return Core.LogLevel.Critical; + case ESeverity.Error: + return Core.LogLevel.Error; + default: + return Core.LogLevel.Information; + } + } + + public static int ToGrayLogLevel(this Core.LogLevel logLevel) + { + // Loglevel to GELF format + switch (logLevel.ToString()) + { + case "TRACE": return 7; + case "DEBUG": return 7; + case "INFO": return 6; + case "WARNING": return 4; + case "ERROR": return 3; + case "CRITICAL": return 2; + default: return 6; // Default to INFO + } + } + + public static ESeverity ToSeverity(this Core.LogLevel logLevel) + { + switch (logLevel) + { + case Core.LogLevel.Debug: + return ESeverity.Debug; + case Core.LogLevel.Warning: + return ESeverity.Warn; + case Core.LogLevel.Critical: + return ESeverity.Critical; + case Core.LogLevel.Trace: + return ESeverity.Emergency; + case Core.LogLevel.Error: + return ESeverity.Error; + default: + return ESeverity.Info; + } + } +} + +public enum SyslogFacility +{ + Kernel = 0, // 0 - Kernel messages + UserLevel = 1, // 1 - User-level messages + MailSystem = 2, // 2 - Mail system + Daemon = 3, // 3 - Daemon messages + Auth = 4, // 4 - Security/authorization messages + Syslog = 5, // 5 - Messages generated by syslogd + Lpr = 6, // 6 - Line printer subsystem + News = 7, // 7 - Network news subsystem + UUCP = 8, // 8 - UUCP subsystem + Clock = 9, // 9 - Clock daemon + AuthPriv = 10, // 10 - Security/authorization messages (privileged) + Ftp = 11, // 11 - FTP daemon + Ntp = 12, // 12 - NTP subsystem + Audit = 13, // 13 - Audit messages + Alert = 14, // 14 - Log alert messages + Cron = 15, // 15 - Cron daemon + Local0 = 16, // 16 - Local use 0 (custom usage) + Local1 = 17, // 17 - Local use 1 (custom usage) + Local2 = 18, // 18 - Local use 2 (custom usage) + Local3 = 19, // 19 - Local use 3 (custom usage) + Local4 = 20, // 20 - Local use 4 (custom usage) + Local5 = 21, // 21 - Local use 5 (custom usage) + Local6 = 22, // 22 - Local use 6 (custom usage) + Local7 = 23 // 23 - Local use 7 (custom usage) +} + +/// +/// Message severity. +/// +public enum ESeverity +{ + /// + /// Debug messages. + /// + Debug = 0, + + /// + /// Informational messages. + /// + Info = 1, + + /// + /// Warning messages. + /// + Warn = 2, + + /// + /// Error messages. + /// + Error = 3, + + /// + /// Alert messages. + /// + Alert = 4, + + /// + /// Critical messages. + /// + Critical = 5, + + /// + /// Emergency messages. + /// + Emergency = 6 +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj new file mode 100644 index 0000000..6482035 --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -0,0 +1,92 @@ + + + .netstandard2.1; net8.0; net4.8; + icon.ico + latest + EonaCat (Jeroen Saey) + true + EonaCat (Jeroen Saey) + icon.png + https://www.nuget.org/packages/EonaCat.LogStack/ + flow-based logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency. +It features a rich fluent API for routing log events to dozens of destinations from console and file to Slack, Discord, Redis, Elasticsearch, and beyond. + Public release version + EonaCat (Jeroen Saey) + EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey + + 0.0.3 + README.md + True + LICENSE + + True + EonaCat.LogStack + git + + + + 0.0.3+{chash:10}.{c:ymd} + true + true + v[0-9]* + true + git + true + true + + + + 0.0.3 + EonaCat.LogStack + EonaCat.LogStack + https://git.saey.me/EonaCat/EonaCat.LogStack + + + + + + $(GeneratedVersion) + + + + + + + True + \ + + + True + \ + + + True + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + True + \ + + + True + \ + + + \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs new file mode 100644 index 0000000..f6f2065 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLogger.cs @@ -0,0 +1,291 @@ +using EonaCat.LogStack.Boosters; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack +{ + /// + /// EonaCat logger with flow-based architecture, booster, and pre-build modifier hook. + /// Designed for zero-allocation logging paths and superior memory efficiency. + /// + public sealed class EonaCatLogStack : IAsyncDisposable + { + private readonly string _category; + private readonly List _flows = new List(); + private readonly List _boosters = new List(); + private readonly ConcurrentBag _concurrentFlows = new ConcurrentBag(); + private readonly LogLevel _minimumLevel; + private readonly TimestampMode _timestampMode; + + private volatile bool _isDisposed; + private long _totalLoggedCount; + private long _totalDroppedCount; + + private readonly List> _modifiers = new List>(); + public delegate void ActionRef(ref T item); + + private readonly object _modifiersLock = new object(); + + public event EventHandler OnLog; + + /// + /// Creates a new logger instance + /// + public EonaCatLogStack(string category = "Application", + LogLevel minimumLevel = LogLevel.Trace, + TimestampMode timestampMode = TimestampMode.Utc) + { + _category = category ?? throw new ArgumentNullException(nameof(category)); + _minimumLevel = minimumLevel; + _timestampMode = timestampMode; + } + + /// + /// Adds a flow (output destination) to this logger + /// + public EonaCatLogStack AddFlow(IFlow flow) + { + if (flow == null) + { + throw new ArgumentNullException(nameof(flow)); + } + + lock (_flows) { _flows.Add(flow); } + _concurrentFlows.Add(flow); + return this; + } + + /// + /// Adds a booster to this logger + /// + public EonaCatLogStack AddBooster(IBooster booster) + { + if (booster == null) + { + throw new ArgumentNullException(nameof(booster)); + } + + lock (_boosters) { _boosters.Add(booster); } + return this; + } + + /// + /// Removes a flow by name + /// + public EonaCatLogStack RemoveFlow(string name) + { + lock (_flows) { _flows.RemoveAll(f => f.Name == name); } + return this; + } + + /// + /// Removes a booster by name + /// + public EonaCatLogStack RemoveBooster(string name) + { + lock (_boosters) { _boosters.RemoveAll(b => b.Name == name); } + return this; + } + + /// + /// Adds a modifier to run before building the LogEvent. + /// Return false to cancel logging. + /// + public EonaCatLogStack AddModifier(ActionRef modifier) + { + if (modifier == null) + { + throw new ArgumentNullException(nameof(modifier)); + } + + lock (_modifiersLock) { _modifiers.Add(modifier); } + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(string message, LogLevel level = LogLevel.Information) + { + if (_isDisposed || level < _minimumLevel) + { + return; + } + + var builder = new LogEventBuilder() + .WithLevel(level) + .WithCategory(_category) + .WithMessage(message) + .WithTimestamp(GetTimestamp()); + + ProcessLogEvent(ref builder); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(LogLevel level, Exception exception, string message) + { + if (_isDisposed || level < _minimumLevel) + { + return; + } + + var builder = new LogEventBuilder() + .WithLevel(level) + .WithCategory(_category) + .WithMessage(message) + .WithException(exception) + .WithTimestamp(GetTimestamp()); + + ProcessLogEvent(ref builder); + + OnLog?.Invoke(this, new LogMessage + { + Level = level, + Exception = exception, + Message = message, + Category = _category, + Origin = null + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(LogLevel level, string message, params (string Key, object Value)[] properties) + { + if (_isDisposed || level < _minimumLevel) + { + return; + } + + var builder = new LogEventBuilder() + .WithLevel(level) + .WithCategory(_category) + .WithMessage(message) + .WithTimestamp(GetTimestamp()); + + foreach (var (key, value) in properties) + { + builder.WithProperty(key, value); + } + + ProcessLogEvent(ref builder); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Trace(string message) => Log(LogLevel.Trace, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Debug(string message) => Log(LogLevel.Debug, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Information(string message) => Log(LogLevel.Information, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Warning(string message) => Log(LogLevel.Warning, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Warning(Exception ex, string message) => Log(LogLevel.Warning, ex, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Error(string message) => Log(LogLevel.Error, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Error(Exception ex, string message) => Log(LogLevel.Error, ex, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Critical(string message) => Log(LogLevel.Critical, message); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Critical(Exception ex, string message) => Log(LogLevel.Critical, ex, message); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessLogEvent(ref LogEventBuilder builder) + { + // Apply boosters + lock (_boosters) + { + foreach (var booster in _boosters) + { + try + { + if (!booster.Boost(ref builder)) + { + return; // filtered out + } + } + catch { } + } + } + + // Apply modifiers + foreach (var mod in _modifiers) + { + try + { + mod(ref builder); + } + catch { } + } + + var logEvent = builder.Build(); + Interlocked.Increment(ref _totalLoggedCount); + + // Blast to flows + foreach (var flow in _concurrentFlows) + { + try + { + var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult(); + if (result == WriteResult.Dropped) + { + Interlocked.Increment(ref _totalDroppedCount); + } + } + catch { } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private long GetTimestamp() + { + switch (_timestampMode) + { + case TimestampMode.Local: return DateTime.Now.Ticks; + case TimestampMode.HighPrecision: return System.Diagnostics.Stopwatch.GetTimestamp(); + default: return DateTime.UtcNow.Ticks; + } + } + + public async Task FlushAsync(CancellationToken cancellationToken = default) + { + var tasks = _concurrentFlows.Select(f => f.FlushAsync(cancellationToken)); + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + public LoggerDiagnostics GetDiagnostics() + { + var flowDiagnostics = _concurrentFlows + .Select(f => f is FlowBase fb ? fb.GetDiagnostics() : null) + .Where(d => d != null) + .ToList(); + + return new LoggerDiagnostics + { + Category = _category, + MinimumLevel = _minimumLevel, + TotalLogged = Interlocked.Read(ref _totalLoggedCount), + TotalDropped = Interlocked.Read(ref _totalDroppedCount), + FlowCount = _flows.Count, + BoosterCount = _boosters.Count, + Flows = flowDiagnostics + }; + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + await FlushAsync().ConfigureAwait(false); + + var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask()); + await Task.WhenAll(disposeTasks).ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs b/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs new file mode 100644 index 0000000..0d4b569 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs @@ -0,0 +1,22 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Base class for boosters that need configuration +/// +public abstract class BoosterBase : IBooster +{ + protected BoosterBase(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public string Name { get; } + + public abstract bool Boost(ref LogEventBuilder builder); +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs new file mode 100644 index 0000000..19c978c --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs @@ -0,0 +1,25 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.Boosters +{ + public sealed class AppBooster : BoosterBase + { + private static readonly string AppName = AppDomain.CurrentDomain.FriendlyName; + private static readonly string AppBase = AppDomain.CurrentDomain.BaseDirectory; + + public AppBooster() : base("App") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("App", AppName); + builder.WithProperty("AppBase", AppBase); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs new file mode 100644 index 0000000..078a2dc --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs @@ -0,0 +1,34 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds application name and version to log events +/// +public sealed class ApplicationBooster : BoosterBase +{ + private readonly string _applicationName; + private readonly string? _version; + + public ApplicationBooster(string applicationName, string? version = null) : base("Application") + { + _applicationName = applicationName ?? throw new ArgumentNullException(nameof(applicationName)); + _version = version; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Application", _applicationName); + if (_version != null) + { + builder.WithProperty("Version", _version); + } + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs new file mode 100644 index 0000000..75248d8 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs @@ -0,0 +1,41 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds custom properties from a callback +/// +public sealed class CallbackBooster : BoosterBase +{ + private readonly Func> _propertiesCallback; + + public CallbackBooster(string name, Func> propertiesCallback) : base(name) + { + _propertiesCallback = propertiesCallback ?? throw new ArgumentNullException(nameof(propertiesCallback)); + } + + public override bool Boost(ref LogEventBuilder builder) + { + try + { + var properties = _propertiesCallback(); + if (properties != null) + { + foreach (var kvp in properties) + { + builder.WithProperty(kvp.Key, kvp.Value); + } + } + } + catch + { + // Swallow exceptions in boosters to prevent logging failures + } + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs new file mode 100644 index 0000000..ac4d1b2 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs @@ -0,0 +1,27 @@ +using EonaCat.LogStack.Core; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds correlation ID from Activity or custom source +/// +public sealed class CorrelationIdBooster : BoosterBase +{ + public CorrelationIdBooster() : base("CorrelationId") { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + var activity = Activity.Current; + if (activity != null) + { + builder.WithProperty("CorrelationId", activity.Id ?? activity.TraceId.ToString()); + } + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs new file mode 100644 index 0000000..a0d22e0 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs @@ -0,0 +1,37 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Adds a custom text property to log events + /// + public sealed class CustomTextBooster : BoosterBase + { + private readonly string _propertyName; + private readonly string _text; + + /// + /// Creates a new booster that adds a custom text property to logs + /// + /// The name of the property to add + /// The text value to set + public CustomTextBooster(string propertyName, string text) + : base("CustomText") + { + _propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); + _text = text ?? throw new ArgumentNullException(nameof(text)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty(_propertyName, _text); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs new file mode 100644 index 0000000..b9654b7 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class DateBooster : BoosterBase + { + public DateBooster() : base("Date") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Date", DateTime.UtcNow.ToString("yyyy-MM-dd")); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs new file mode 100644 index 0000000..f2d28ec --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs @@ -0,0 +1,27 @@ +using EonaCat.LogStack.Core; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds environment name to log events +/// +public sealed class EnvironmentBooster : BoosterBase +{ + private readonly string _environmentName; + + public EnvironmentBooster(string environmentName) : base("Environment") + { + _environmentName = environmentName ?? "Production"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Environment", _environmentName); + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs new file mode 100644 index 0000000..0c21759 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs @@ -0,0 +1,23 @@ +using EonaCat.LogStack.Core; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class FrameworkBooster : BoosterBase + { + private static readonly string FrameworkDesc = RuntimeInformation.FrameworkDescription; + + public FrameworkBooster() : base("Framework") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Framework", FrameworkDesc); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs new file mode 100644 index 0000000..b41b111 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs @@ -0,0 +1,28 @@ +using EonaCat.LogStack.Core; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Filters log events based on level +/// +public sealed class LevelFilterBooster : BoosterBase +{ + private readonly LogLevel _minimumLevel; + + public LevelFilterBooster(LogLevel minimumLevel) : base("LevelFilter") + { + _minimumLevel = minimumLevel; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + // Filter will be handled by the pipeline, this is a no-op booster + // Actual filtering happens in the logger pipeline based on configuration + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs new file mode 100644 index 0000000..da255ab --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs @@ -0,0 +1,25 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds machine name to log events +/// +public sealed class MachineNameBooster : BoosterBase +{ + private static readonly string MachineName = Environment.MachineName; + + public MachineNameBooster() : base("MachineName") { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("MachineName", MachineName); + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs new file mode 100644 index 0000000..39657bb --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs @@ -0,0 +1,22 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class MemoryBooster : BoosterBase + { + public MemoryBooster() : base("Memory") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + var memoryMB = GC.GetTotalMemory(false) / 1024 / 1024; + builder.WithProperty("Memory", memoryMB); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs new file mode 100644 index 0000000..1f01969 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs @@ -0,0 +1,23 @@ +using EonaCat.LogStack.Core; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class OSBooster : BoosterBase + { + private static readonly string OSDesc = RuntimeInformation.OSDescription; + + public OSBooster() : base("OS") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("OS", OSDesc); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs new file mode 100644 index 0000000..311131a --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs @@ -0,0 +1,24 @@ +using EonaCat.LogStack.Core; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class ProcStartBooster : BoosterBase + { + private static readonly DateTime ProcessStart = Process.GetCurrentProcess().StartTime; + + public ProcStartBooster() : base("ProcStart") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("ProcStart", ProcessStart); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs new file mode 100644 index 0000000..55b1c22 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs @@ -0,0 +1,25 @@ +using EonaCat.LogStack.Core; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds process ID to log events +/// +public sealed class ProcessIdBooster : BoosterBase +{ + private static readonly int ProcessId = Process.GetCurrentProcess().Id; + + public ProcessIdBooster() : base("ProcessId") { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("ProcessId", ProcessId); + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs new file mode 100644 index 0000000..78b7930 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class ThreadIdBooster : BoosterBase + { + public ThreadIdBooster() : base("ThreadId") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("ThreadId", Environment.CurrentManagedThreadId); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs new file mode 100644 index 0000000..b53009f --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class ThreadNameBooster : BoosterBase + { + public ThreadNameBooster() : base("ThreadName") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("ThreadName", Thread.CurrentThread.Name ?? "n/a"); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs new file mode 100644 index 0000000..007a07d --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class TicksBooster : BoosterBase + { + public TicksBooster() : base("Ticks") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Ticks", DateTime.UtcNow.Ticks); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs new file mode 100644 index 0000000..ab65971 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class TimeBooster : BoosterBase + { + public TimeBooster() : base("Time") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("Time", DateTime.UtcNow.ToString("HH:mm:ss.fff")); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs new file mode 100644 index 0000000..ab39b39 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs @@ -0,0 +1,36 @@ +using EonaCat.LogStack.Core; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Adds timestamp in multiple formats +/// +public sealed class TimestampBooster : BoosterBase +{ + private readonly TimestampMode _mode; + + public TimestampBooster(TimestampMode mode = TimestampMode.Utc) : base("Timestamp") + { + _mode = mode; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + var timestamp = _mode switch + { + TimestampMode.Local => DateTime.Now.Ticks, + TimestampMode.HighPrecision => Stopwatch.GetTimestamp(), + _ => DateTime.UtcNow.Ticks + }; + + builder.WithTimestamp(timestamp); + return true; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs new file mode 100644 index 0000000..77f78dc --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs @@ -0,0 +1,25 @@ +using EonaCat.LogStack.Core; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class UptimeBooster : BoosterBase + { + private static readonly DateTime ProcessStart = Process.GetCurrentProcess().StartTime; + + public UptimeBooster() : base("Uptime") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + var uptime = (DateTime.Now - ProcessStart).TotalSeconds; + builder.WithProperty("Uptime", uptime); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs new file mode 100644 index 0000000..9a492cc --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs @@ -0,0 +1,23 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Boosters +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class UserBooster : BoosterBase + { + private static readonly string UserName = Environment.UserName; + + public UserBooster() : base("User") { } + + [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("User", UserName); + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs b/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs new file mode 100644 index 0000000..ebbeffc --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs @@ -0,0 +1,74 @@ +using System; + +namespace EonaCat.LogStack; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Colors to use when writing to the console. +/// +public class ColorSchema +{ + /// + /// The color to use for critical messages. + /// + public ColorScheme Critical = new(ConsoleColor.DarkRed, ConsoleColor.Black); + + /// + /// The color to use for debug messages. + /// + public ColorScheme Debug = new(ConsoleColor.Green, ConsoleColor.Black); + + /// + /// The color to use for error messages. + /// + public ColorScheme Error = new(ConsoleColor.Red, ConsoleColor.Black); + + /// + /// The color to use for informational messages. + /// + public ColorScheme Info = new(ConsoleColor.Blue, ConsoleColor.Black); + + /// + /// The color to use for emergency messages. + /// + public ColorScheme Trace = new(ConsoleColor.Cyan, ConsoleColor.Black); + + /// + /// The color to use for alert messages. + /// + public ColorScheme Traffic = new(ConsoleColor.DarkMagenta, ConsoleColor.Black); + + /// + /// The color to use for warning messages. + /// + public ColorScheme Warning = new(ConsoleColor.DarkYellow, ConsoleColor.Black); +} + +/// +/// Color scheme for logging messages. +/// +public class ColorScheme +{ + /// + /// Background color. + /// + public ConsoleColor Background = Console.BackgroundColor; + + /// + /// Foreground color. + /// + public ConsoleColor Foreground = Console.ForegroundColor; + + /// + /// Instantiates a new color scheme. + /// + /// Foreground color. + /// Background color. + public ColorScheme(ConsoleColor foreground, ConsoleColor background) + { + Foreground = foreground; + Background = background; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs b/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs new file mode 100644 index 0000000..5989004 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs @@ -0,0 +1,11 @@ +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.EonaCatLogStackCore +{ + public enum CompressionFormat + { + None, + GZip, + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs b/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs new file mode 100644 index 0000000..ce21e4a --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs @@ -0,0 +1,59 @@ +namespace EonaCat.LogStack.Core; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Defines the severity level of log entries +/// +public enum LogLevel : byte +{ + None = 0, + Trace = 1, + Debug = 2, + Information = 3, + Warning = 4, + Error = 5, + Critical = 6, +} + +/// +/// Result of a log write operation +/// +public enum WriteResult : byte +{ + Success = 0, + Dropped = 1, + Failed = 2, + FlowDisabled = 3, + LevelFiltered = 4, + NoBlastZone = 5 +} + +/// +/// Strategy for handling backpressure in flows +/// +public enum BackpressureStrategy : byte +{ + /// Wait for capacity to become available + Wait = 0, + + /// Drop the newest incoming message + DropNewest = 1, + + /// Drop the oldest message in the queue + DropOldest = 2, + + /// Block until space is available (may impact performance) + Block = 3 +} + +/// +/// Options for timestamp generation +/// +public enum TimestampMode : byte +{ + Utc = 0, + Local = 1, + HighPrecision = 2 +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs b/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs new file mode 100644 index 0000000..e66a7e5 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs @@ -0,0 +1,14 @@ +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.EonaCatLogStackCore +{ + public enum FileOutputFormat + { + Text, + Json, + Xml, + Csv, // RFC-4180 CSV + StructuredJson, // Machine-readable JSON with correlation IDs + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs new file mode 100644 index 0000000..0b16a6d --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs @@ -0,0 +1,400 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Audit log severity filter only these levels are written to the audit trail. + /// + public enum AuditLevel + { + All, + WarningAndAbove, + ErrorAndAbove, + CriticalOnly, + } + + /// + /// A tamper-evident, append-only audit flow. + /// + /// Each entry is written as: + /// SEQ|ISO-TIMESTAMP|LEVEL|CATEGORY|MESSAGE|PROPS|HASH + /// + /// Where HASH = SHA-256( previousHash + currentLineWithoutHash ). + /// This creates a hash-chain so any deletion or modification of a past + /// entry invalidates all subsequent hashes, making tampering detectable. + /// + /// The file is opened with FileShare.Read only (no concurrent writers). + /// The flow is synchronous-by-design: audit entries must land on disk + /// before the method returns, so blocks until + /// the entry is flushed. + /// + public sealed class AuditFlow : FlowBase + { + private const string Delimiter = "|"; + private const int HashLength = 64; // hex SHA-256 + + private readonly string _filePath; + private readonly AuditLevel _auditLevel; + private readonly bool _includeProperties; + + private readonly object _writeLock = new object(); + private readonly FileStream _stream; + private readonly StreamWriter _writer; + + private long _sequence; + private string _previousHash; + + private long _totalEntries; + + public AuditFlow( + string directory, + string filePrefix = "audit", + AuditLevel auditLevel = AuditLevel.All, + LogLevel minimumLevel = LogLevel.Trace, + bool includeProperties = true) + : base("Audit:" + directory, minimumLevel) + { + if (directory == null) + { + throw new ArgumentNullException(nameof(directory)); + } + + if (filePrefix == null) + { + throw new ArgumentNullException(nameof(filePrefix)); + } + + _auditLevel = auditLevel; + _includeProperties = includeProperties; + + // Resolve relative path + if (directory.StartsWith("./", StringComparison.Ordinal)) + { + directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, directory.Substring(2)); + } + + Directory.CreateDirectory(directory); + + // One file per day, named with date stamp + string date = DateTime.UtcNow.ToString("yyyyMMdd"); + _filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit"); + + // Exclusive write access + _stream = new FileStream( + _filePath, + FileMode.Append, + FileAccess.Write, + FileShare.Read, // allow external readers, but no other writers + bufferSize: 4096, + FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately + + _writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true }; + + // Derive starting hash from the last line already in the file (for continuity) + _previousHash = ReadLastHash(directory, filePrefix, date); + _sequence = CountExistingLines(_filePath); + } + + /// Path to the current audit file. + public string FilePath => _filePath; + + /// Total entries written in this session. + public long TotalEntries => Interlocked.Read(ref _totalEntries); + + /// + /// Verify the integrity of the audit file by replaying the hash chain. + /// Returns (true, null) if intact, (false, reason) if tampered. + /// + public static (bool ok, string reason) Verify(string filePath) + { + if (!File.Exists(filePath)) + { + return (false, "File not found."); + } + + string previousHash = new string('0', HashLength); + long expectedSeq = 1; + + foreach (string raw in File.ReadLines(filePath, Encoding.UTF8)) + { + if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#")) + { + continue; + } + + int lastPipe = raw.LastIndexOf(Delimiter, StringComparison.Ordinal); + if (lastPipe < 0) + { + return (false, $"Malformed line (no delimiter): {Truncate(raw, 120)}"); + } + + string body = raw.Substring(0, lastPipe); + string storedHash = raw.Substring(lastPipe + 1).Trim(); + + if (storedHash.Length != HashLength) + { + return (false, $"Bad hash length on line {expectedSeq}: '{storedHash}'"); + } + + string computedHash = ComputeHash(previousHash, body); + + if (!string.Equals(storedHash, computedHash, StringComparison.OrdinalIgnoreCase)) + { + return (false, $"Hash mismatch on sequence {expectedSeq}. " + + $"Expected {computedHash}, found {storedHash}. " + + $"Entry may have been tampered with."); + } + + // Verify sequence number (first field) + int firstPipe = body.IndexOf(Delimiter, StringComparison.Ordinal); + if (firstPipe > 0) + { + string seqStr = body.Substring(0, firstPipe); + if (long.TryParse(seqStr, out long seq) && seq != expectedSeq) + { + return (false, $"Sequence gap: expected {expectedSeq}, found {seq}."); + } + } + + previousHash = computedHash; + expectedSeq++; + } + + return (true, null); + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (!PassesAuditLevel(logEvent.Level)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + WriteEntry(logEvent); + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (var e in logEvents.ToArray()) + { + if (IsLogLevelEnabled(e) && PassesAuditLevel(e.Level)) + { + WriteEntry(e); + } + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + lock (_writeLock) + { + _writer.Flush(); + _stream.Flush(flushToDisk: true); + } + return Task.CompletedTask; + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + lock (_writeLock) + { + try { _writer.Flush(); } catch { } + try { _stream.Flush(true); } catch { } + try { _writer.Dispose(); } catch { } + try { _stream.Dispose(); } catch { } + } + await base.DisposeAsync().ConfigureAwait(false); + } + + private void WriteEntry(LogEvent log) + { + lock (_writeLock) + { + long seq = Interlocked.Increment(ref _sequence); + + var sb = new StringBuilder(256); + sb.Append(seq); + sb.Append(Delimiter); + sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O")); + sb.Append(Delimiter); + sb.Append(LevelString(log.Level)); + sb.Append(Delimiter); + sb.Append(Escape(log.Category)); + sb.Append(Delimiter); + sb.Append(Escape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty)); + + if (log.Exception != null) + { + sb.Append(Delimiter); + sb.Append("EX="); + sb.Append(Escape(log.Exception.GetType().Name + ": " + log.Exception.Message)); + } + + if (_includeProperties && log.Properties.Count > 0) + { + sb.Append(Delimiter); + bool first = true; + foreach (var kv in log.Properties.ToArray()) + { + if (!first) + { + sb.Append(';'); + } + + first = false; + sb.Append(Escape(kv.Key)).Append('=').Append(Escape(kv.Value?.ToString() ?? "null")); + } + } + + string body = sb.ToString(); + string hash = ComputeHash(_previousHash, body); + string line = body + Delimiter + hash; + + _writer.WriteLine(line); + // AutoFlush=true + WriteThrough stream = immediate disk write + + _previousHash = hash; + Interlocked.Increment(ref _totalEntries); + Interlocked.Increment(ref BlastedCount); + } + } + + private bool PassesAuditLevel(LogLevel level) => _auditLevel switch + { + AuditLevel.All => true, + AuditLevel.WarningAndAbove => level >= LogLevel.Warning, + AuditLevel.ErrorAndAbove => level >= LogLevel.Error, + AuditLevel.CriticalOnly => level >= LogLevel.Critical, + _ => true + }; + + private static string LevelString(LogLevel level) => level switch + { + LogLevel.Trace => "TRACE", + LogLevel.Debug => "DEBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Critical => "CRITICAL", + _ => level.ToString().ToUpperInvariant() + }; + + /// Replace pipe characters inside field values so the delimiter stays unique. + private static string Escape(string value) + => string.IsNullOrEmpty(value) ? string.Empty : value.Replace("|", "\\|").Replace("\r", "\\r").Replace("\n", "\\n"); + + public static string ComputeHash(string previousHash, string body) + { + if (string.IsNullOrEmpty(previousHash) || string.IsNullOrEmpty(body)) + { + throw new ArgumentException("Input values cannot be null or empty."); + } + + string inputString = previousHash + "|" + body; + byte[] input = Encoding.UTF8.GetBytes(inputString); + + using (SHA256 sha = SHA256.Create()) + { + byte[] digest = sha.ComputeHash(input); + return BitConverter.ToString(digest).Replace("-", "").ToLowerInvariant(); + } + } + + private static string ReadLastHash(string directory, string prefix, string date) + { + string path = Path.Combine(directory, $"{prefix}_{Environment.MachineName}_{date}.audit"); + + if (!File.Exists(path)) + { + return new string('0', HashLength); + } + + string lastLine = null; + + // Open file with FileShare.ReadWrite to allow reading while it's being written to + using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + // Read lines from the file + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) + { + lastLine = line; + } + } + } + + if (lastLine == null) + { + return new string('0', HashLength); + } + + int lastPipe = lastLine.LastIndexOf(Delimiter, StringComparison.Ordinal); + return lastPipe >= 0 ? lastLine.Substring(lastPipe + 1).Trim() : new string('0', HashLength); + } + + + private static long CountExistingLines(string path) + { + if (!File.Exists(path)) + { + return 0; + } + + long count = 0; + + // Open the file with FileShare.ReadWrite to allow concurrent read/write access + using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) + { + count++; + } + } + } + + return count; + } + + + private static string Truncate(string s, int max) + => s.Length <= max ? s : s.Substring(0, max) + "..."; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs new file mode 100644 index 0000000..28d66d5 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs @@ -0,0 +1,285 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// console flow with color support and minimal allocations + /// Uses a ColorSchema for configurable colors + /// + public sealed class ConsoleFlow : FlowBase + { + private readonly bool _useColors; + private readonly TimestampMode _timestampMode; + private readonly StringBuilder _buffer = new(1024); + private readonly object _consoleLock = new(); + private readonly ColorSchema _colors; + + private readonly string _template; + private List> _compiledTemplate; + + public ConsoleFlow( + LogLevel minimumLevel = LogLevel.Trace, + bool useColors = true, + TimestampMode timestampMode = TimestampMode.Local, + ColorSchema? colorSchema = null, + string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") + : base("Console", minimumLevel) + { + _useColors = useColors; + _timestampMode = timestampMode; + _colors = colorSchema ?? new ColorSchema(); + _template = template ?? throw new ArgumentNullException(nameof(template)); + + CompileTemplate(_template); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + WriteToConsole(logEvent); + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (var logEvent in logEvents.Span) + { + if (logEvent.Level >= MinimumLevel) + { + WriteToConsole(logEvent); + Interlocked.Increment(ref BlastedCount); + } + } + + return Task.FromResult(WriteResult.Success); + } + + private void WriteToConsole(LogEvent logEvent) + { + lock (_consoleLock) + { + _buffer.Clear(); + + foreach (var action in _compiledTemplate) + { + action(logEvent, _buffer); + } + + if (_useColors && TryGetColor(logEvent.Level, out var color)) + { + Console.ForegroundColor = color.Foreground; + } + + Console.WriteLine(_buffer.ToString()); + + if (logEvent.Exception != null) + { + if (_useColors) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + } + + Console.WriteLine(logEvent.Exception.ToString()); + + if (_useColors) + { + Console.ResetColor(); + } + } + + if (_useColors) + { + Console.ResetColor(); + } + } + } + + private void CompileTemplate(string template) + { + _compiledTemplate = new List>(); + int pos = 0; + + while (pos < template.Length) + { + int open = template.IndexOf('{', pos); + if (open < 0) + { + string lit = template.Substring(pos); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + break; + } + + if (open > pos) + { + string lit = template.Substring(pos, open - pos); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + } + + int close = template.IndexOf('}', open); + if (close < 0) + { + string lit = template.Substring(open); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + break; + } + + string token = template.Substring(open + 1, close - open - 1); + _compiledTemplate.Add(ResolveToken(token)); + pos = close + 1; + } + } + + private Action ResolveToken(string token) + { + switch (token.ToLowerInvariant()) + { + case "ts": + return (log, sb) => + sb.Append(LogEvent.GetDateTime(log.Timestamp) + .ToString("yyyy-MM-dd HH:mm:ss.fff")); + + case "tz": + return (_, sb) => + sb.Append(_timestampMode == TimestampMode.Local + ? TimeZoneInfo.Local.StandardName + : "UTC"); + + case "host": + return (_, sb) => sb.Append(Environment.MachineName); + + case "category": + return (log, sb) => + { + if (!string.IsNullOrEmpty(log.Category)) + { + sb.Append(log.Category); + } + }; + + case "thread": + return (_, sb) => sb.Append(Thread.CurrentThread.ManagedThreadId); + + case "pid": + return (_, sb) => sb.Append(Process.GetCurrentProcess().Id); + + case "message": + return (log, sb) => sb.Append(log.Message); + + case "props": + return AppendProperties; + + case "newline": + return (_, sb) => sb.AppendLine(); + + case "logtype": + return (log, sb) => + { + var levelText = GetLevelText(log.Level); + + if (_useColors && TryGetColor(log.Level, out var color)) + { + Console.ForegroundColor = color.Foreground; + Console.BackgroundColor = color.Background; + + Console.Write(sb.ToString()); + Console.Write(levelText); + + Console.ResetColor(); + sb.Clear(); + } + else + { + sb.Append(levelText); + } + }; + + default: + return (_, sb) => sb.Append('{').Append(token).Append('}'); + } + } + + private void AppendProperties(LogEvent log, StringBuilder sb) + { + if (log.Properties.Count == 0) + { + return; + } + + sb.Append(" {"); + + bool first = true; + foreach (var prop in log.Properties) + { + if (!first) + { + sb.Append(", "); + } + + sb.Append(prop.Key); + sb.Append('='); + sb.Append(prop.Value?.ToString() ?? "null"); + + first = false; + } + + sb.Append('}'); + } + + private bool TryGetColor(LogLevel level, out ColorScheme color) + { + color = level switch + { + LogLevel.Trace => _colors.Trace, + LogLevel.Debug => _colors.Debug, + LogLevel.Information => _colors.Info, + LogLevel.Warning => _colors.Warning, + LogLevel.Error => _colors.Error, + LogLevel.Critical => _colors.Critical, + _ => _colors.Info + }; + return color != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetLevelText(LogLevel level) + { + return level switch + { + LogLevel.Trace => "TRACE", + LogLevel.Debug => "DEBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Critical => "CRITICAL", + _ => "???" + }; + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + // Console auto-flushes + return Task.CompletedTask; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs new file mode 100644 index 0000000..66086a6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs @@ -0,0 +1,233 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// database flow with batched inserts for any ADO.NET database + /// + public sealed class DatabaseFlow : FlowBase + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _writerTask; + private readonly CancellationTokenSource _cts; + + private readonly Func _connectionFactory; + private readonly string _tableName; + + public DatabaseFlow( + Func connectionFactory, + string tableName = "Logs", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + : base($"Database:{tableName}", minimumLevel) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _tableName = tableName; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _writerTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _writerTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false); + batch.Clear(); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"DatabaseFlow error: {ex.Message}"); + } + } + + private async Task WriteBatchAsync(List batch, CancellationToken cancellationToken) + { + using var connection = _connectionFactory(); + await connection.OpenAsync(cancellationToken); + + using var transaction = connection.BeginTransaction(); + + // Build a single SQL command with multiple inserts + var sb = new StringBuilder(); + var parameters = new List(); + int paramIndex = 0; + + foreach (var logEvent in batch) + { + sb.Append($"INSERT INTO {_tableName} (Timestamp, Level, Category, Message, ThreadId, Exception, Properties) VALUES ("); + + // Timestamp + var timestampParam = CreateParameter(connection, $"@p{paramIndex++}", LogEvent.GetDateTime(logEvent.Timestamp).ToString("O")); + parameters.Add(timestampParam); + sb.Append(timestampParam.ParameterName).Append(", "); + + // Level + var levelParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Level.ToString()); + parameters.Add(levelParam); + sb.Append(levelParam.ParameterName).Append(", "); + + // Category + var categoryParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Category ?? string.Empty); + parameters.Add(categoryParam); + sb.Append(categoryParam.ParameterName).Append(", "); + + // Message + var messageParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Message.ToString()); + parameters.Add(messageParam); + sb.Append(messageParam.ParameterName).Append(", "); + + // ThreadId + var threadParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.ThreadId); + parameters.Add(threadParam); + sb.Append(threadParam.ParameterName).Append(", "); + + // Exception + object exValue = logEvent.Exception != null + ? JsonHelper.ToJson(new + { + type = logEvent.Exception.GetType().FullName, + message = logEvent.Exception.Message, + stackTrace = logEvent.Exception.StackTrace + }) + : DBNull.Value; + + var exParam = CreateParameter(connection, $"@p{paramIndex++}", exValue); + parameters.Add(exParam); + sb.Append(exParam.ParameterName).Append(", "); + + // Properties + object propsValue = logEvent.Properties.Count > 0 + ? JsonHelper.ToJson(logEvent.Properties) + : DBNull.Value; + + var propsParam = CreateParameter(connection, $"@p{paramIndex++}", propsValue); + parameters.Add(propsParam); + sb.Append(propsParam.ParameterName).Append(");"); + } + + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = sb.ToString(); + + foreach (var p in parameters) + { + command.Parameters.Add(p); + } + + await command.ExecuteNonQueryAsync(cancellationToken); + transaction.Commit(); + } + + private static Dictionary ToDictionary(ReadOnlyMemory> properties) + { + var dict = new Dictionary(); + foreach (var prop in properties.Span) + { + dict[prop.Key] = prop.Value; + } + + return dict; + } + + private static DbParameter CreateParameter(DbConnection connection, string name, object value) + { + var p = connection.CreateCommand().CreateParameter(); + p.ParameterName = name; + p.Value = value ?? DBNull.Value; + return p; + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _writerTask.ConfigureAwait(false); + } + catch { } + + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs new file mode 100644 index 0000000..6f36364 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs @@ -0,0 +1,300 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.Extensions; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Diagnostic counters snapshot emitted on a regular interval. + /// + public sealed class DiagnosticsSnapshot + { + public DateTime CapturedAt { get; internal set; } + public double CpuPercent { get; internal set; } + public long WorkingSetBytes { get; internal set; } + public long GcGen0 { get; internal set; } + public long GcGen1 { get; internal set; } + public long GcGen2 { get; internal set; } + public long ThreadCount { get; internal set; } + public long HandleCount { get; internal set; } + public double UptimeSeconds { get; internal set; } + public Dictionary Custom { get; internal set; } + } + + /// + /// A flow that periodically captures process diagnostics (CPU, memory, GC, threads) + /// and writes them as structured log events. Also acts as a pass-through: every + /// normal log event optionally gets runtime metrics injected as properties. + /// + /// Additionally exposes an in-process registry so application + /// code can record business metrics (request count, error rate, etc.) that are + /// flushed alongside diagnostic snapshots. + /// + public sealed class DiagnosticsFlow : FlowBase + { + /// Counter for business metrics. + public sealed class Counter + { + private long _value; + public string Name { get; } + public Counter(string name) { Name = name; } + public void Increment() { Interlocked.Increment(ref _value); } + public void IncrementBy(long delta) { Interlocked.Add(ref _value, delta); } + public void Reset() { Interlocked.Exchange(ref _value, 0); } + public long Value { get { return Interlocked.Read(ref _value); } } + } + + private readonly ConcurrentDictionary _counters + = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly TimeSpan _snapshotInterval; + private readonly bool _injectIntoEvents; + private readonly bool _writeSnapshotEvents; + private readonly string _snapshotCategory; + private readonly IFlow _forwardTo; + private readonly Func> _customMetricsFactory; + + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _samplerThread; + private readonly Stopwatch _uptime = Stopwatch.StartNew(); + + private volatile DiagnosticsSnapshot _latest; + + private TimeSpan _lastCpuTime; + private DateTime _lastCpuSample; + private readonly Process _proc; + + public DiagnosticsSnapshot LatestSnapshot { get { return _latest; } } + + public DiagnosticsFlow( + TimeSpan snapshotInterval = default(TimeSpan), + bool injectIntoEvents = false, + bool writeSnapshotEvents = true, + string snapshotCategory = "Diagnostics", + IFlow forwardTo = null, + LogLevel minimumLevel = LogLevel.Trace, + Func> customMetrics = null) + : base("Diagnostics", minimumLevel) + { + _snapshotInterval = snapshotInterval == default(TimeSpan) + ? TimeSpan.FromSeconds(60) + : snapshotInterval; + _injectIntoEvents = injectIntoEvents; + _writeSnapshotEvents = writeSnapshotEvents; + _snapshotCategory = snapshotCategory ?? "Diagnostics"; + _forwardTo = forwardTo; + _customMetricsFactory = customMetrics; + + _proc = Process.GetCurrentProcess(); + _lastCpuTime = _proc.TotalProcessorTime; + _lastCpuSample = DateTime.UtcNow; + + _samplerThread = new Thread(SamplerLoop) + { + IsBackground = true, + Name = "DiagnosticsFlow.Sampler", + Priority = ThreadPriority.BelowNormal + }; + _samplerThread.Start(); + } + + /// Gets or creates a named counter. + public Counter GetCounter(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + return _counters.GetOrAdd(name, n => new Counter(n)); + } + + /// Current value of a named counter (0 if not yet created). + public long ReadCounter(string name) + { + Counter c; + return _counters.TryGetValue(name, out c) ? c.Value : 0; + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_injectIntoEvents) + { + DiagnosticsSnapshot snap = _latest; + if (snap != null) + { + logEvent.Properties.TryAdd("diag.mem_mb",(snap.WorkingSetBytes / 1024 / 1024).ToString()); + logEvent.Properties.TryAdd("diag.cpu",snap.CpuPercent.ToString("F1")); + logEvent.Properties.TryAdd("diag.threads",snap.ThreadCount.ToString()); + } + } + + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (LogEvent e in logEvents.ToArray()) + { + if (IsLogLevelEnabled(e)) + { + BlastAsync(e, cancellationToken); + } + } + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(0); + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _cts.Cancel(); + _samplerThread.Join(TimeSpan.FromSeconds(3)); + _cts.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + + + private void SamplerLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + try + { + Thread.Sleep(_snapshotInterval); + DiagnosticsSnapshot snap = Capture(); + _latest = snap; + + if (_writeSnapshotEvents && _forwardTo != null) + { + LogEvent ev = BuildSnapshotEvent(snap); + _forwardTo.BlastAsync(ev).GetAwaiter().GetResult(); + } + } + catch (ThreadInterruptedException) { break; } + catch (Exception ex) + { + Console.Error.WriteLine("[DiagnosticsFlow] Sampler error: " + ex.Message); + } + } + } + + private DiagnosticsSnapshot Capture() + { + _proc.Refresh(); + + DateTime now = DateTime.UtcNow; + TimeSpan cpuNow = _proc.TotalProcessorTime; + double elapsed = (now - _lastCpuSample).TotalSeconds; + double cpu = elapsed > 0 + ? (cpuNow - _lastCpuTime).TotalSeconds / elapsed / Environment.ProcessorCount * 100.0 + : 0; + + _lastCpuTime = cpuNow; + _lastCpuSample = now; + + Dictionary custom = null; + if (_customMetricsFactory != null) + { + try { custom = _customMetricsFactory(); } catch { } + } + + // Append counters to custom dict + if (_counters.Count > 0) + { + if (custom == null) + { + custom = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + foreach (KeyValuePair kv in _counters) + { + custom["counter." + kv.Key] = kv.Value.Value; + } + } + + return new DiagnosticsSnapshot + { + CapturedAt = now, + CpuPercent = Math.Round(cpu, 2), + WorkingSetBytes = _proc.WorkingSet64, + GcGen0 = GC.CollectionCount(0), + GcGen1 = GC.CollectionCount(1), + GcGen2 = GC.CollectionCount(2), + ThreadCount = _proc.Threads.Count, + HandleCount = _proc.HandleCount, + UptimeSeconds = _uptime.Elapsed.TotalSeconds, + Custom = custom + }; + } + + private LogEvent BuildSnapshotEvent(DiagnosticsSnapshot snap) + { + var sb = new StringBuilder(256); + sb.AppendFormat( + "Diagnostics | CPU={0:F1}% Mem={1}MB GC=[{2},{3},{4}] Threads={5} Handles={6} Uptime={7:F0}s", + snap.CpuPercent, + snap.WorkingSetBytes / 1024 / 1024, + snap.GcGen0, snap.GcGen1, snap.GcGen2, + snap.ThreadCount, + snap.HandleCount, + snap.UptimeSeconds); + + var ev = new LogEvent + { + Level = LogLevel.Information, + Category = _snapshotCategory, + Message = new StringSegment(sb.ToString()), + Timestamp = snap.CapturedAt.Ticks + }; + + ev.Properties.TryAdd("cpu_pct", snap.CpuPercent.ToString("F2")); + ev.Properties.TryAdd("mem_bytes", snap.WorkingSetBytes.ToString()); + ev.Properties.TryAdd("gc_gen0", snap.GcGen0.ToString()); + ev.Properties.TryAdd("gc_gen1", snap.GcGen1.ToString()); + ev.Properties.TryAdd("gc_gen2", snap.GcGen2.ToString()); + ev.Properties.TryAdd("threads", snap.ThreadCount.ToString()); + ev.Properties.TryAdd("handles", snap.HandleCount.ToString()); + ev.Properties.TryAdd("uptime_s", snap.UptimeSeconds.ToString("F0")); + + if (snap.Custom != null) + { + foreach (KeyValuePair kv in snap.Custom) + { + ev.Properties.TryAdd(kv.Key, kv.Value != null ? kv.Value.ToString() : "null"); + } + } + + return ev; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs new file mode 100644 index 0000000..ab051ab --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs @@ -0,0 +1,199 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// logging flow that sends messages to a Discord channel via webhook. + /// + public sealed class DiscordFlow : FlowBase, IAsyncDisposable + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _workerTask; + private readonly CancellationTokenSource _cts; + private readonly HttpClient _httpClient; + private readonly string _webhookUrl; + + public DiscordFlow( + string webhookUrl, + string botName, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Information) + : base("Discord", minimumLevel) + { + _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl)); + _httpClient = new HttpClient(); + _batchSize = batchSize <= 0 ? 1 : batchSize; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _workerTask = Task.Run(() => ProcessQueueAsync(botName, _cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + private async Task ProcessQueueAsync(string botName, CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await SendBatchAsync(botName, batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(botName, batch, cancellationToken); + batch.Clear(); + } + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"DiscordFlow error: {ex.Message}"); + } + } + + private async Task SendBatchAsync(string botName, List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var content = new + { + username = botName, + embeds = new[] + { + new + { + title = logEvent.Level.ToString(), + description = logEvent.Message, + color = GetDiscordColor(logEvent.Level), + timestamp = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"), + fields = logEvent.Properties.Count > 0 + ? GetFields(logEvent) + : Array.Empty() + } + } + }; + + var json = JsonHelper.ToJson(content); + using var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(_webhookUrl, stringContent, cancellationToken); + + if (logEvent.Exception != null) + { + var exContent = new + { + username = botName, + content = $"**Exception:** {logEvent.Exception.GetType().FullName}\n```{logEvent.Exception.Message}\n{logEvent.Exception.StackTrace}```" + }; + var exJson = JsonHelper.ToJson(exContent); + using var exStringContent = new StringContent(exJson, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(_webhookUrl, exStringContent, cancellationToken); + } + } + } + + private static int GetDiscordColor(LogLevel level) + { + return level switch + { + LogLevel.Trace => 0x00FFFF, // Cyan + LogLevel.Debug => 0x00FF00, // Green + LogLevel.Information => 0xFFFFFF, // White + LogLevel.Warning => 0xFFFF00, // Yellow + LogLevel.Error => 0xFF0000, // Red + LogLevel.Critical => 0x800000, // Dark Red + _ => 0x808080, // Gray + }; + } + + private static object[] GetFields(LogEvent logEvent) + { + var fields = new List(); + foreach (var prop in logEvent.Properties) + { + fields.Add(new + { + name = prop.Key, + value = prop.Value?.ToString() ?? "null", + inline = true + }); + } + return fields.ToArray(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/ElasticSearchFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ElasticSearchFlow.cs new file mode 100644 index 0000000..969446b --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ElasticSearchFlow.cs @@ -0,0 +1,190 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Elasticsearch logging flow using HTTP bulk API (without NEST) + /// + public sealed class ElasticSearchFlow : FlowBase, IAsyncDisposable + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _workerTask; + private readonly CancellationTokenSource _cts; + private readonly HttpClient _httpClient; + private readonly string _elasticsearchUrl; + private readonly string _indexName; + + public ElasticSearchFlow( + string elasticsearchUrl, + string indexName = "logs", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + : base($"Elasticsearch:{indexName}", minimumLevel) + { + _elasticsearchUrl = elasticsearchUrl?.TrimEnd('/') ?? throw new ArgumentNullException(nameof(elasticsearchUrl)); + _indexName = indexName; + _httpClient = new HttpClient(); + _batchSize = batchSize <= 0 ? 1 : batchSize; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _workerTask = Task.Run(() => ProcessQueueAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + private async Task ProcessQueueAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await SendBulkAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBulkAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBulkAsync(batch, cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"ElasticSearchFlow error: {ex.Message}"); + } + } + + private async Task SendBulkAsync(List batch, CancellationToken cancellationToken) + { + var sb = new StringBuilder(); + + foreach (var logEvent in batch) + { + // Action metadata + sb.AppendLine(JsonHelper.ToJson(new { index = new { _index = _indexName } })); + + // Document + var doc = new Dictionary + { + ["timestamp"] = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"), + ["level"] = logEvent.Level.ToString(), + ["category"] = logEvent.Category ?? string.Empty, + ["message"] = logEvent.Message.ToString(), + ["threadId"] = logEvent.ThreadId + }; + + if (logEvent.Exception != null) + { + doc["exception"] = new + { + type = logEvent.Exception.GetType().FullName, + message = logEvent.Exception.Message, + stackTrace = logEvent.Exception.StackTrace + }; + } + + if (logEvent.Properties.Count > 0) + { + var props = new Dictionary(); + foreach (var prop in logEvent.Properties) + { + props[prop.Key] = prop.Value; + } + + doc["properties"] = props; + } + + sb.AppendLine(JsonHelper.ToJson(doc)); + } + + var content = new StringContent(sb.ToString(), Encoding.UTF8, "application/x-ndjson"); + using var response = await _httpClient.PostAsync($"{_elasticsearchUrl}/_bulk", content, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + var respText = await response.Content.ReadAsStringAsync(); + Console.Error.WriteLine($"ElasticSearchFlow bulk insert failed: {response.StatusCode} {respText}"); + } + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/EmailFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EmailFlow.cs new file mode 100644 index 0000000..b3ae043 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EmailFlow.cs @@ -0,0 +1,331 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Mail; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Sends log events as email via SMTP. + /// + /// Includes built-in digest batching: instead of one email per event, events are + /// accumulated for up to and sent as a single digest. + /// A "flush-on-critical" option bypasses batching for Critical events. + /// + public sealed class EmailFlow : FlowBase + { + private readonly string _headerName = "

EonaCat Logger – Log Digest

"; + private readonly string _smtpHost; + private readonly int _smtpPort; + private readonly bool _useSsl; + private readonly string _username; + private readonly string _password; + private readonly string _from; + private readonly string[] _to; + private readonly string _subjectPrefix; + private readonly TimeSpan _digestInterval; + private readonly bool _flushOnCritical; + private readonly int _maxEventsPerDigest; + + private readonly List _pending = new List(); + private readonly object _lock = new object(); + private DateTime _lastSent = DateTime.UtcNow; + + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _digestThread; + + private long _totalEmails; + + public EmailFlow( + string smtpHost, + int smtpPort = 587, + bool useSsl = true, + string username = null, + string password = null, + string from = null, + string to = null, + string subjectPrefix = "[EonaCatLogStack]", + int digestMinutes = 5, + bool flushOnCritical = true, + int maxEventsPerDigest = 100, + string headerName = null, + LogLevel minimumLevel = LogLevel.Error) + : base("Email:" + smtpHost, minimumLevel) + { + if (smtpHost == null) + { + throw new ArgumentNullException("smtpHost"); + } + + if (to == null) + { + throw new ArgumentNullException("to"); + } + + if (string.IsNullOrWhiteSpace(headerName)) + { + _headerName = headerName; + } + + _smtpHost = smtpHost; + _smtpPort = smtpPort; + _useSsl = useSsl; + _username = username; + _password = password; + _from = from ?? ("eonacat-logger@" + smtpHost); + _to = to.Split(new char[] { ',', ';' }, + StringSplitOptions.RemoveEmptyEntries); + _subjectPrefix = subjectPrefix ?? "[EonaCatLogStack]"; + _digestInterval = TimeSpan.FromMinutes(digestMinutes < 1 ? 1 : digestMinutes); + _flushOnCritical = flushOnCritical; + _maxEventsPerDigest = maxEventsPerDigest < 1 ? 1 : maxEventsPerDigest; + + _digestThread = new Thread(DigestLoop) + { + IsBackground = true, + Name = "EmailFlow.Digest", + Priority = ThreadPriority.BelowNormal + }; + _digestThread.Start(); + } + + public long TotalEmailsSent { get { return Interlocked.Read(ref _totalEmails); } } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + bool sendNow = false; + lock (_lock) + { + _pending.Add(logEvent); + if (_flushOnCritical && logEvent.Level >= LogLevel.Critical) + { + sendNow = true; + } + + if (_pending.Count >= _maxEventsPerDigest) + { + sendNow = true; + } + } + + if (sendNow) + { + SendDigestAsync(); + } + + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (LogEvent e in logEvents.ToArray()) + { + if (IsLogLevelEnabled(e)) + { + BlastAsync(e, cancellationToken); + } + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + SendDigestAsync(); + return Task.FromResult(0); + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _cts.Cancel(); + _digestThread.Join(TimeSpan.FromSeconds(5)); + SendDigestAsync(); + _cts.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + + private void DigestLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + try { Thread.Sleep(TimeSpan.FromSeconds(30)); } + catch (ThreadInterruptedException) { break; } + + lock (_lock) + { + if (_pending.Count > 0 && DateTime.UtcNow - _lastSent >= _digestInterval) + { + SendDigestAsync(); + } + } + } + } + + private void SendDigestAsync() + { + List batch; + lock (_lock) + { + if (_pending.Count == 0) + { + return; + } + + batch = new List(_pending); + _pending.Clear(); + _lastSent = DateTime.UtcNow; + } + + // Fire-and-forget on a thread pool thread + ThreadPool.QueueUserWorkItem(_ => SendDigest(batch)); + } + + private void SendDigest(List events) + { + try + { + string subject = BuildSubject(events); + string body = BuildBody(events); + + using (SmtpClient smtp = new SmtpClient(_smtpHost, _smtpPort)) + { + smtp.EnableSsl = _useSsl; + if (!string.IsNullOrEmpty(_username)) + { + smtp.Credentials = new NetworkCredential(_username, _password); + } + + using (MailMessage msg = new MailMessage()) + { + msg.From = new MailAddress(_from); + msg.Subject = subject; + msg.Body = body; + msg.IsBodyHtml = true; + + foreach (string addr in _to) + { + msg.To.Add(addr.Trim()); + } + + smtp.Send(msg); + } + } + + Interlocked.Increment(ref _totalEmails); + } + catch (Exception ex) + { + Console.Error.WriteLine("[EmailFlow] Send error: " + ex.Message); + Interlocked.Increment(ref DroppedCount); + } + } + + private string BuildSubject(List events) + { + LogLevel maxLevel = LogLevel.Trace; + foreach (LogEvent e in events) + { + if (e.Level > maxLevel) + { + maxLevel = e.Level; + } + } + + return _subjectPrefix + " " + LevelString(maxLevel) + + " – " + events.Count + " event(s) @ " + + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC"; + } + + private string BuildBody(List events) + { + var sb = new StringBuilder(events.Count * 300); + sb.AppendLine(""); + sb.AppendLine(_headerName); + sb.AppendLine(""); + sb.AppendLine("" + + "" + + ""); + + foreach (LogEvent e in events) + { + string color = LevelColor(e.Level); + string ts = LogEvent.GetDateTime(e.Timestamp).ToString("HH:mm:ss.fff"); + string msg = HtmlEncode(e.Message.Length > 0 ? e.Message.ToString() : string.Empty); + string exc = e.Exception != null + ? HtmlEncode(e.Exception.GetType().Name + ": " + e.Exception.Message) + : string.Empty; + + sb.AppendFormat( + "", + color, ts, LevelString(e.Level), + HtmlEncode(e.Category ?? string.Empty), msg, exc); + sb.AppendLine(); + } + + sb.AppendLine("
TimeLevelCategoryMessageException
{1}{2}{3}{4}{5}
"); + return sb.ToString(); + } + + private static string LevelColor(LogLevel level) + { + switch (level) + { + case LogLevel.Warning: return "#FFF3CD"; + case LogLevel.Error: return "#F8D7DA"; + case LogLevel.Critical: return "#F1AEB5"; + default: return "#FFFFFF"; + } + } + + private static string LevelString(LogLevel level) + { + switch (level) + { + case LogLevel.Trace: return "TRACE"; + case LogLevel.Debug: return "DEBUG"; + case LogLevel.Information: return "INFO"; + case LogLevel.Warning: return "WARN"; + case LogLevel.Error: return "ERROR"; + case LogLevel.Critical: return "CRITICAL"; + default: return level.ToString().ToUpperInvariant(); + } + } + + private static string HtmlEncode(string s) + { + if (string.IsNullOrEmpty(s)) + { + return string.Empty; + } + + return s.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """); + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs new file mode 100644 index 0000000..1e589b3 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs @@ -0,0 +1,499 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Writes log events to AES-256-CBC encrypted, append-only binary files. + /// + /// File layout: + /// [4 bytes magic "EONA"] [32 bytes salt] [16 bytes IV] + /// repeated: [4 bytes LE block-length] [N bytes ciphertext] + /// + /// Key derivation: PBKDF2-HMACSHA1 with 100 000 iterations. + /// Each individual line is encrypted independently (ECB-safe CBC block) so the + /// file can be read entry-by-entry via . + /// + /// + public sealed class EncryptedFileFlow : FlowBase + { + private static readonly byte[] Magic = new byte[] { 0x45, 0x4F, 0x4E, 0x41 }; // "EONA" + private const int SaltSize = 32; + private const int IvSize = 16; + private const int KeySize = 32; // AES-256 + private const int Pbkdf2Iter = 100000; + + private readonly BlockingCollection _queue; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _writerThread; + private readonly Thread _flushThread; + + private readonly string _directory; + private readonly string _filePrefix; + private readonly string _password; + private readonly long _maxFileSize; + private readonly int _flushIntervalMs; + private readonly TimestampMode _timestampMode; + + private readonly object _lock = new object(); + private FileStream _currentStream; + private ICryptoTransform _encryptor; + private string _currentPath; + private long _currentSize; + private DateTime _currentDate; + + private long _totalWritten; + private long _totalRotations; + + public EncryptedFileFlow( + string directory, + string password, + string filePrefix = "encrypted_log", + long maxFileSize = 50L * 1024 * 1024, + int flushIntervalMs = 3000, + LogLevel minimumLevel = LogLevel.Trace, + TimestampMode tsMode = TimestampMode.Utc) + : base("EncryptedFile:" + directory, minimumLevel) + { + if (directory == null) + { + throw new ArgumentNullException("directory"); + } + + if (password == null) + { + throw new ArgumentNullException("password"); + } + + if (filePrefix == null) + { + throw new ArgumentNullException("filePrefix"); + } + + _directory = directory; + _password = password; + _filePrefix = filePrefix; + _maxFileSize = maxFileSize; + _flushIntervalMs = flushIntervalMs; + _timestampMode = tsMode; + + // Resolve relative path + if (_directory.StartsWith("./", StringComparison.Ordinal)) + { + _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2)); + } + + Directory.CreateDirectory(_directory); + + _queue = new BlockingCollection(new ConcurrentQueue(), 8192); + + _writerThread = new Thread(WriterLoop) + { + IsBackground = true, + Name = "EncryptedFileFlow.Writer", + Priority = ThreadPriority.AboveNormal + }; + _writerThread.Start(); + + _flushThread = new Thread(FlushLoop) + { + IsBackground = true, + Name = "EncryptedFileFlow.Flush", + Priority = ThreadPriority.BelowNormal + }; + _flushThread.Start(); + } + + /// + /// Decrypts an .eona file produced by this flow to a plain-text file. + /// + public static bool DecryptToFile(string encryptedPath, string outputPath, string password) + { + if (encryptedPath == null) + { + throw new ArgumentNullException("encryptedPath"); + } + + if (outputPath == null) + { + throw new ArgumentNullException("outputPath"); + } + + if (password == null) + { + throw new ArgumentNullException("password"); + } + + try + { + using (FileStream source = File.OpenRead(encryptedPath)) + { + byte[] magic = new byte[4]; + ReadExact(source, magic, 4); + for (int i = 0; i < 4; i++) + { + if (magic[i] != Magic[i]) + { + throw new InvalidDataException("Not a valid EONA encrypted log file."); + } + } + + byte[] salt = new byte[SaltSize]; + byte[] iv = new byte[IvSize]; + ReadExact(source, salt, SaltSize); + ReadExact(source, iv, IvSize); + + byte[] key = DeriveKey(password, salt); + + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = key; + aes.IV = iv; + + using (ICryptoTransform dec = aes.CreateDecryptor()) + using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8)) + { + byte[] buffer = new byte[4]; + while (source.Position < source.Length) + { + int read = source.Read(buffer, 0, 4); + if (read < 4) + { + break; + } + + int blockLength = BitConverter.ToInt32(buffer, 0); + if (blockLength <= 0 || blockLength > 16 * 1024 * 1024) + { + throw new InvalidDataException("Corrupt block at offset " + (source.Position - 4)); + } + + byte[] cipher = new byte[blockLength]; + ReadExact(source, cipher, blockLength); + byte[] plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); + out_.WriteLine(Encoding.UTF8.GetString(plain)); + return true; + } + } + } + } + } + catch (Exception e) + { + Console.WriteLine($"Exception during decryption => {e.Message}"); + } + return false; + } + + public LogStats GetStats() + { + return new LogStats( + Interlocked.Read(ref _totalWritten), + Interlocked.Read(ref DroppedCount), + Interlocked.Read(ref _totalRotations), 0, 0); + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + return Task.FromResult(TryEnqueue(Format(logEvent))); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + WriteResult result = WriteResult.Success; + foreach (LogEvent e in logEvents.ToArray()) + { + if (e.Level < MinimumLevel) + { + continue; + } + + if (TryEnqueue(Format(e)) == WriteResult.Dropped) + { + result = WriteResult.Dropped; + } + } + return Task.FromResult(result); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + lock (_lock) + { + if (_currentStream != null) + { + _currentStream.Flush(true); + } + } + return Task.FromResult(0); + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _queue.CompleteAdding(); + _cts.Cancel(); + _writerThread.Join(TimeSpan.FromSeconds(5)); + _flushThread.Join(TimeSpan.FromSeconds(2)); + lock (_lock) { CloseCurrentFile(); } + _cts.Dispose(); + _queue.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + + private void WriterLoop() + { + try + { + while (!_queue.IsCompleted) + { + string line; + try { line = _queue.Take(_cts.Token); } + catch (OperationCanceledException) { break; } + catch (InvalidOperationException) { break; } + + WriteEncrypted(line); + + string extra; + int batch = 0; + while (batch < 256 && _queue.TryTake(out extra)) + { + WriteEncrypted(extra); + batch++; + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine("[EncryptedFileFlow] Writer error: " + ex.Message); + } + finally + { + string remaining; + while (_queue.TryTake(out remaining)) + { + WriteEncrypted(remaining); + } + + lock (_lock) { CloseCurrentFile(); } + } + } + + private void FlushLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + try { Thread.Sleep(_flushIntervalMs); } + catch (ThreadInterruptedException) { break; } + + lock (_lock) + { + if (_currentStream != null) + { + try { _currentStream.Flush(true); } catch { } + } + } + } + } + + private void WriteEncrypted(string line) + { + lock (_lock) + { + DateTime today = _timestampMode == TimestampMode.Local + ? DateTime.Now.Date + : DateTime.UtcNow.Date; + + if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize) + { + if (_currentStream != null) + { + Interlocked.Increment(ref _totalRotations); + } + + CloseCurrentFile(); + OpenNewFile(today); + } + + byte[] plain = Encoding.UTF8.GetBytes(line); + byte[] cipher = _encryptor.TransformFinalBlock(plain, 0, plain.Length); + byte[] lenBuf = BitConverter.GetBytes(cipher.Length); + + _currentStream.Write(lenBuf, 0, 4); + _currentStream.Write(cipher, 0, cipher.Length); + _currentSize += 4 + cipher.Length; + + Interlocked.Increment(ref _totalWritten); + Interlocked.Increment(ref BlastedCount); + } + } + + private void OpenNewFile(DateTime date) + { + _currentDate = date; + _currentPath = Path.Combine( + _directory, + _filePrefix + "_" + Environment.MachineName + "_" + date.ToString("yyyyMMdd") + ".eona"); + + bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0; + + _currentStream = new FileStream( + _currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536); + + byte[] salt = new byte[SaltSize]; + byte[] iv = new byte[IvSize]; + + if (isNew) + { + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(salt); + rng.GetBytes(iv); + } + _currentStream.Write(Magic, 0, 4); + _currentStream.Write(salt, 0, SaltSize); + _currentStream.Write(iv, 0, IvSize); + _currentSize = 4 + SaltSize + IvSize; + } + else + { + // Re-read header so we continue the same key/IV session + using (FileStream hdr = File.OpenRead(_currentPath)) + { + hdr.Seek(4, SeekOrigin.Begin); + ReadExact(hdr, salt, SaltSize); + ReadExact(hdr, iv, IvSize); + } + _currentSize = new FileInfo(_currentPath).Length; + } + + byte[] key = DeriveKey(_password, salt); + + Aes aes = Aes.Create(); + aes.KeySize = 256; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = key; + aes.IV = iv; + _encryptor = aes.CreateEncryptor(); + } + + private void CloseCurrentFile() + { + if (_encryptor != null) + { + try { _encryptor.Dispose(); } catch { } + _encryptor = null; + } + if (_currentStream != null) + { + try { _currentStream.Flush(true); _currentStream.Dispose(); } catch { } + _currentStream = null; + } + } + + private WriteResult TryEnqueue(string line) + { + if (_queue.TryAdd(line)) + { + return WriteResult.Success; + } + + Interlocked.Increment(ref DroppedCount); + return WriteResult.Dropped; + } + + private string Format(LogEvent log) + { + DateTime ts = LogEvent.GetDateTime(log.Timestamp); + var sb = new StringBuilder(256); + sb.Append(ts.ToString("yyyy-MM-dd HH:mm:ss.fff")); + sb.Append(" [").Append(LevelString(log.Level)).Append("] "); + sb.Append(log.Category ?? string.Empty); + sb.Append(": "); + sb.Append(log.Message.Length > 0 ? log.Message.ToString() : string.Empty); + + if (log.Exception != null) + { + sb.Append(" | EX: ").Append(log.Exception.GetType().Name) + .Append(": ").Append(log.Exception.Message); + } + + if (log.Properties.Count > 0) + { + sb.Append(" |"); + foreach (var kv in log.Properties.ToArray()) + { + sb.Append(' ').Append(kv.Key).Append('=') + .Append(kv.Value != null ? kv.Value.ToString() : "null"); + } + } + return sb.ToString(); + } + + private static string LevelString(LogLevel level) + { + switch (level) + { + case LogLevel.Trace: return "TRACE"; + case LogLevel.Debug: return "DEBUG"; + case LogLevel.Information: return "INFO"; + case LogLevel.Warning: return "WARN"; + case LogLevel.Error: return "ERROR"; + case LogLevel.Critical: return "CRITICAL"; + default: return level.ToString().ToUpperInvariant(); + } + } + + private static byte[] DeriveKey(string password, byte[] salt) + { + using (Rfc2898DeriveBytes kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iter)) + { + return kdf.GetBytes(KeySize); + } + } + + private static void ReadExact(Stream stream, byte[] buf, int count) + { + int offset = 0; + while (offset < count) + { + int r = stream.Read(buf, offset, count - offset); + if (r == 0) + { + throw new EndOfStreamException("Unexpected end of encrypted log stream."); + } + + offset += r; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/EventLogFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EventLogFlow.cs new file mode 100644 index 0000000..a0c4ce6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EventLogFlow.cs @@ -0,0 +1,214 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.IO; +using System.Net.Security; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.Flows +{ + public sealed class EventLogFlow : FlowBase + { + private readonly string _destination; + private readonly int _port; + private TcpClient? _tcpClient; + private NetworkStream? _stream; + private SslStream? _sslStream; + private readonly bool _useTls; + private readonly RemoteCertificateValidationCallback? _certificateValidationCallback; + private readonly X509CertificateCollection? _clientCertificates; + + private readonly List _logBuffer; + private readonly int _bufferSize; + private readonly TimeSpan _flushInterval; + private readonly CancellationTokenSource _cts; + + public EventLogFlow( + string destination, + int port = 514, + LogLevel minimumLevel = LogLevel.Trace, + int bufferSize = 100, + TimeSpan? flushInterval = null, + bool useTls = false, + RemoteCertificateValidationCallback? certificateValidationCallback = null, + X509CertificateCollection? clientCertificates = null + ) : base($"EventLogFlow:{destination}:{port}", minimumLevel) + { + _destination = destination ?? throw new ArgumentNullException(nameof(destination)); + _port = port; + _useTls = useTls; + _certificateValidationCallback = certificateValidationCallback; + _clientCertificates = clientCertificates; + _bufferSize = bufferSize; + _flushInterval = flushInterval ?? TimeSpan.FromSeconds(5); + _logBuffer = new List(bufferSize); + _cts = new CancellationTokenSource(); + + _tcpClient = new TcpClient(); + _ = StartFlushingLogsAsync(_cts.Token); + } + + public void Log(string message, string category = "CustomEvent", LogLevel level = LogLevel.Information, object customData = null) + { + var logEvent = new LogEvent + { + Timestamp = DateTime.UtcNow.Ticks, + Level = level, + Message = message.ToCharArray(), + Category = category, + CustomData = customData != null ? JsonHelper.ToJson(customData) : string.Empty + }; + + // Add to buffer and trigger flush if needed + _logBuffer.Add(logEvent); + if (_logBuffer.Count >= _bufferSize) + { + _ = FlushLogsAsync(); + } + } + + private async Task StartFlushingLogsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(_flushInterval, cancellationToken); + + if (_logBuffer.Count > 0) + { + await FlushLogsAsync(); + } + } + } + + private async Task FlushLogsAsync() + { + if (_logBuffer.Count == 0) + { + return; + } + + var logsToSend = new List(_logBuffer); + _logBuffer.Clear(); + + await SendLogsAsync(logsToSend); + } + + private async Task SendLogsAsync(IEnumerable logEvents) + { + try + { + await EnsureConnectedAsync(); + + var logMessages = new StringBuilder(); + foreach (var logEvent in logEvents) + { + logMessages.AppendLine(FormatLogMessage(logEvent)); + } + + var data = Encoding.UTF8.GetBytes(logMessages.ToString()); + if (_useTls && _sslStream != null) + { + await _sslStream.WriteAsync(data, 0, data.Length); + await _sslStream.FlushAsync(); + } + else if (_stream != null) + { + await _stream.WriteAsync(data, 0, data.Length); + await _stream.FlushAsync(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error sending logs: {ex.Message}"); + } + } + + private async Task EnsureConnectedAsync() + { + if (_tcpClient?.Connected ?? false) + { + return; + } + + await _tcpClient?.ConnectAsync(_destination, _port); + + if (_useTls) + { + _stream = _tcpClient?.GetStream(); + _sslStream = new SslStream(_stream, false, _certificateValidationCallback); + await _sslStream.AuthenticateAsClientAsync(_destination, _clientCertificates, System.Security.Authentication.SslProtocols.Tls12, false); + } + else + { + _stream = _tcpClient?.GetStream(); + } + } + + private string FormatLogMessage(LogEvent logEvent) + { + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + var sb = new StringBuilder(); + + sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss.fff")); + sb.Append(" ["); + sb.Append(logEvent.Level.ToString().ToUpperInvariant()); + sb.Append("] "); + sb.Append(logEvent.Category); + sb.Append(": "); + sb.Append(logEvent.Message); + + if (!string.IsNullOrEmpty(logEvent.CustomData)) + { + sb.Append(" | CustomData: "); + sb.Append(logEvent.CustomData); + } + + return sb.ToString(); + } + + public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return WriteResult.LevelFiltered; + } + + await SendLogsAsync(new List { logEvent }); + return WriteResult.Success; + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.LevelFiltered; + } + + await SendLogsAsync(logEvents.Span.ToArray()); + return WriteResult.Success; + } + + public override async ValueTask DisposeAsync() + { + _cts.Cancel(); + _sslStream?.Dispose(); + _stream?.Dispose(); + _tcpClient?.Dispose(); + await base.DisposeAsync(); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs new file mode 100644 index 0000000..92837c0 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs @@ -0,0 +1,50 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class FailoverFlow : FlowBase + { + private readonly IFlow _primary; + private readonly IFlow _secondary; + + public FailoverFlow(IFlow primary, IFlow secondary) + : base($"Failover({primary.Name})", primary.MinimumLevel) + { + _primary = primary ?? throw new ArgumentNullException(nameof(primary)); + _secondary = secondary ?? throw new ArgumentNullException(nameof(secondary)); + } + + public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + var result = await _primary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + + if (result == WriteResult.Success) + { + return result; + } + + return await _secondary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + await _primary.FlushAsync(cancellationToken); + await _secondary.FlushAsync(cancellationToken); + } + + public override async ValueTask DisposeAsync() + { + await _primary.DisposeAsync(); + await _secondary.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs new file mode 100644 index 0000000..06b9142 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs @@ -0,0 +1,1638 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.EonaCatLogStackCore.Policies; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// High-performance file sink with batching, rotation, compression, sampling, + /// enrichment, retention policies, and pluggable secondary writers. + /// + public sealed class FileFlow : FlowBase + { + private const int FileBufferSize = 131072; // 128 KB + private const int WriterBufferSize = 131072; // 128 KB + private readonly int _batchSize; + private const int QueueCapacity = 8192; + public bool IgnoreConsoleErrors { get; set; } + + private static readonly Dictionary LevelStrings = + new Dictionary + { + { LogLevel.Trace, "TRACE" }, + { LogLevel.Debug, "DEBUG" }, + { LogLevel.Information, "INFO" }, + { LogLevel.Warning, "WARN" }, + { LogLevel.Error, "ERROR" }, + { LogLevel.Critical, "CRITICAL" }, + }; + + private const string CsvHeader = "timestamp,level,category,message,exception,properties\r\n"; + + private readonly BlockingCollection _queue; + private readonly ConcurrentQueue _compressionQueue = new ConcurrentQueue(); + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _writerThread; + private readonly Thread _compressionThread; + private readonly Task _flushTask; + private readonly Task _retentionTask; + private readonly Stopwatch _uptime = Stopwatch.StartNew(); + private readonly SemaphoreSlim _compressionSignal = new SemaphoreSlim(0, int.MaxValue); + + private readonly string _directory; + private readonly string _filePrefix; + private readonly long _maxFileSize; + private readonly FileRetentionPolicy _retention; + private readonly TimestampMode _timestampMode; + private readonly TimeSpan _flushInterval; + private readonly FileOutputFormat _outputFormat; + private readonly CompressionFormat _compressionFormat; + private readonly string _template; + private readonly bool _useCategoryRouting; + private readonly HashSet _logLevelsForSeparateFiles; + private readonly long _maxMemoryBytes; + + private volatile SamplingPolicy _samplingPolicy; + private volatile Action _onDrop; + private volatile Action _onRotate; + private List> _secondaryWriters; + private readonly object _secondaryWritersLock = new object(); + private int _correlationSeed; + + private readonly List>> _enrichers + = new List>>(); + + private long _currentMemoryBytes; + + private long _totalBytesWritten; + private long _totalRotations; + + private readonly Dictionary _openFiles + = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly object _fileLock = new object(); + + private string _fileExtension = ".log"; + + private List> _compiledTemplate; + private readonly Dictionary> _customTokens + = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + private sealed class OpenFile : IDisposable + { + public readonly FileStream Stream; + public readonly StreamWriter Writer; + public readonly DateTime Date; + public long Size; + public bool HasCsvHeader; + public bool HasXmlHeader; + + public OpenFile(FileStream fs, StreamWriter sw, DateTime date) + { + Stream = fs; + Writer = sw; + Date = date; + Size = fs.Length; + } + + public void Dispose() + { + try { Writer.Flush(); } catch { } + try { Writer.Dispose(); } catch { } + try { Stream.Dispose(); } catch { } + } + } + + public FileFlow( + string directory, + string filePrefix = "log", + long maxFileSize = 200 * 1024 * 1024, + FileRetentionPolicy retention = null, + int flushIntervalMs = 2000, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + bool useCategoryRouting = false, + LogLevel[] logLevelsForSeparateFiles = null, + TimestampMode timestampMode = TimestampMode.Utc, + BackpressureStrategy backpressure = BackpressureStrategy.DropOldest, + FileOutputFormat outputFormat = FileOutputFormat.Text, + CompressionFormat compression = CompressionFormat.GZip, + string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}", + long maxMemoryBytes = 20 * 1024 * 1024) + : base("File:" + Path.Combine(directory, filePrefix), minimumLevel) + { + if (directory == null) + { + throw new ArgumentNullException("directory"); + } + + if (filePrefix == null) + { + throw new ArgumentNullException("filePrefix"); + } + + if (template == null) + { + throw new ArgumentNullException("template"); + } + + CheckForProcessTermination(); + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _directory = directory; + _filePrefix = filePrefix; + _template = template; + _maxFileSize = maxFileSize; + _retention = retention ?? new FileRetentionPolicy(); + _timestampMode = timestampMode; + _useCategoryRouting = useCategoryRouting; + _maxMemoryBytes = maxMemoryBytes; + _flushInterval = TimeSpan.FromMilliseconds(flushIntervalMs); + _outputFormat = outputFormat; + _compressionFormat = compression; + + _logLevelsForSeparateFiles = logLevelsForSeparateFiles != null + ? new HashSet(logLevelsForSeparateFiles) + : new HashSet(); + + SetFileExtension(outputFormat); + CompileTemplate(template); + + // Resolve relative path + if (_directory.StartsWith("./", StringComparison.Ordinal)) + { + _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2)); + } + + Directory.CreateDirectory(_directory); + + // BlockingCollection with bounded capacity + _queue = new BlockingCollection(new ConcurrentQueue(), QueueCapacity); + + // Dedicated writer thread + _writerThread = new Thread(WriterThreadBody) + { + IsBackground = true, + Name = "FileFlow.Writer[" + filePrefix + "]", + Priority = ThreadPriority.AboveNormal, + }; + _writerThread.Start(); + + // Dedicated compression thread + _compressionThread = new Thread(CompressionThreadBody) + { + IsBackground = true, + Name = "FileFlow.Compress[" + filePrefix + "]", + Priority = ThreadPriority.BelowNormal, + }; + _compressionThread.Start(); + + _flushTask = flushIntervalMs > 0 + ? Task.Factory.StartNew(PeriodicFlushLoop, TaskCreationOptions.LongRunning) + : Task.FromResult(0); + + _retentionTask = Task.Factory.StartNew(RetentionLoop, TaskCreationOptions.LongRunning); + } + + private void CheckForProcessTermination() + { + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch + { + // Do nothing + } + }; + + Console.CancelKeyPress += (_, e) => + { + try + { + DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch + { + // Do nothing + } + }; + } + + /// Add an ambient property enricher applied to every event. + public FileFlow EnrichWith(string key, Func valueFactory) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + if (valueFactory == null) + { + throw new ArgumentNullException("valueFactory"); + } + + _enrichers.Add(new KeyValuePair>(key, valueFactory)); + return this; + } + + /// Add a static ambient property. + public FileFlow EnrichWith(string key, object value) + { + return EnrichWith(key, _ => value); + } + + /// Configure sampling: only log 1 in events + /// that match . + public FileFlow WithSampling(int rate, Func predicate = null) + { + _samplingPolicy = new SamplingPolicy { Rate = rate, Predicate = predicate }; + return this; + } + + /// Callback invoked when an event is dropped due to backpressure. + public FileFlow OnEventDropped(Action callback) + { + _onDrop = callback; + return this; + } + + /// Callback invoked with the archived path after each file rotation. + public FileFlow OnFileRotated(Action callback) + { + _onRotate = callback; + return this; + } + + /// Fan-out: also invoke for every formatted log line. + public FileFlow AddSecondaryWriter(Action writer) + { + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + lock (_secondaryWritersLock) + { + if (_secondaryWriters == null) + { + _secondaryWriters = new List>(); + } + + _secondaryWriters.Add(writer); + } + return this; + } + + /// Register a custom template token (e.g. {mytoken}). + public FileFlow RegisterToken(string name, Action formatter) + { + if (formatter == null) + { + throw new ArgumentNullException("formatter"); + } + + _customTokens[name] = formatter; + return this; + } + + /// Change the minimum log level at runtime (thread-safe). + public void SetMinimumLevel(LogLevel level) + { + MinimumLevel = level; + } + + /// Returns live throughput and health metrics. + public LogStats GetStats() + { + long written = Interlocked.Read(ref BlastedCount); + long dropped = Interlocked.Read(ref DroppedCount); + long bytes = Interlocked.Read(ref _totalBytesWritten); + long rots = Interlocked.Read(ref _totalRotations); + double elapsed = _uptime.Elapsed.TotalSeconds; + double wps = elapsed > 0 ? written / elapsed : 0; + return new LogStats(written, dropped, rots, bytes, wps); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + SamplingPolicy sp = _samplingPolicy; + if (sp != null && !sp.ShouldLog(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + return Task.FromResult(TryEnqueue(logEvent)); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + WriteResult result = WriteResult.Success; + SamplingPolicy sp = _samplingPolicy; + + foreach (LogEvent e in logEvents.ToArray()) + { + if (e.Level < MinimumLevel) + { + continue; + } + + if (sp != null && !sp.ShouldLog(e)) + { + continue; + } + + if (TryEnqueue(e) == WriteResult.Dropped) + { + result = WriteResult.Dropped; + } + } + + return Task.FromResult(result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private WriteResult TryEnqueue(LogEvent log) + { + long size = EstimateSize(log); + long current = Interlocked.Read(ref _currentMemoryBytes); + + if (current + size > _maxMemoryBytes) + { + Interlocked.Increment(ref DroppedCount); + Action drop = _onDrop; + if (drop != null) + { + drop(log); + } + + return WriteResult.Dropped; + } + + if (!_queue.TryAdd(log)) + { + Interlocked.Increment(ref DroppedCount); + Action drop = _onDrop; + if (drop != null) + { + drop(log); + } + + return WriteResult.Dropped; + } + + Interlocked.Add(ref _currentMemoryBytes, size); + Interlocked.Increment(ref BlastedCount); + return WriteResult.Success; + } + + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + Stopwatch sw = Stopwatch.StartNew(); + while (_queue.Count > 0 + && !cancellationToken.IsCancellationRequested + && sw.Elapsed < TimeSpan.FromSeconds(10)) + { + Thread.Sleep(5); + } + + lock (_fileLock) + { + foreach (OpenFile of in _openFiles.Values) + { + try { of.Writer.Flush(); } catch { /* ignore */ } + } + } + + return Task.FromResult(0); + } + catch + { + return Task.FromResult(0); + } + } + + + public override async ValueTask DisposeAsync() + { + if (!IsEnabled) + { + return; + } + + IsEnabled = false; + + _queue.CompleteAdding(); + _cts.Cancel(); + + _writerThread.Join(2000); + _compressionSignal.Release(); + _compressionThread.Join(2000); + + lock (_fileLock) + { + foreach (var outputFile in _openFiles.Values) + { + if (_outputFormat == FileOutputFormat.Xml && outputFile.HasXmlHeader) + { + try + { + outputFile.Writer.WriteLine(""); + } + catch { } + } + + outputFile.Dispose(); + } + + _openFiles.Clear(); + } + + _cts.Dispose(); + _compressionSignal.Dispose(); + _queue.Dispose(); + + await base.DisposeAsync().ConfigureAwait(false); + } + private void WriterThreadBody() + { + try + { + while (!_queue.IsCompleted) + { + LogEvent e; + try + { + e = _queue.Take(_cts.Token); + } + catch (OperationCanceledException) { break; } + catch (InvalidOperationException) { break; } + + WriteLogEvent(e); + + // Drain additional items + int extra = 0; + LogEvent next; + while (extra < _batchSize && _queue.TryTake(out next)) + { + WriteLogEvent(next); + extra++; + } + } + } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] Writer thread error: " + ex.Message); + } + finally + { + // Drain remaining events before the thread exits + LogEvent remaining; + while (_queue.TryTake(out remaining)) + { + WriteLogEvent(remaining); + } + + // Final flush + lock (_fileLock) + { + foreach (OpenFile of in _openFiles.Values) + { + try { of.Writer.Flush(); } catch { /* ignore */ } + } + } + } + } + + private void WriteToConsoleError(string text) + { + if (IgnoreConsoleErrors) + { + return; + } + + Console.Error.WriteLine(text); + } + + private void WriteLogEvent(LogEvent log) + { + long size = 0; + try + { + size = EstimateSize(log); + } + catch + { + size = 512; + } + + Interlocked.Add(ref _currentMemoryBytes, -size); + + string line; + try + { + StringBuilder sb = StringBuilderPool.Rent(); + try + { + switch (_outputFormat) + { + case FileOutputFormat.Json: + FormatJson(log, sb, false); + break; + case FileOutputFormat.StructuredJson: + FormatJson(log, sb, true); + break; + case FileOutputFormat.Xml: + FormatXml(log, sb); + break; + case FileOutputFormat.Csv: + FormatCsv(log, sb); + break; + default: + FormatText(log, sb); + break; + } + line = sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } + } + catch (Exception ex) + { + try + { + line = "[FileFlow] Format error: " + ex.Message + + " | Original level=" + log.Level + + " message=" + (log.Message.Length > 0 ? log.Message.ToString() : "(empty)"); + } + catch + { + line = "[FileFlow] Format error (unrecoverable)"; + } + } + + try + { + string path = GenerateFilePath(GetCurrentDate(), log); + + lock (_fileLock) + { + try + { + EnsureFileOpen(path, log); + + if (ShouldRotate(path, line.Length)) + { + string archived = RotateFile(path); + EnsureFileOpen(path, log); + if (archived != null) + { + Action onRotate = _onRotate; + if (onRotate != null) + { + try { onRotate(archived); } + catch { /* Do nothing */ } + } + } + } + + OpenFile of; + if (_openFiles.TryGetValue(path, out of)) + { + of.Writer.WriteLine(line); + of.Size += line.Length + Environment.NewLine.Length; + } + } + catch (Exception ex) + { + try + { + WriteToConsoleError("[FileFlow] Write error for '" + path + "': " + ex.Message); + } + catch { /* Do nothing */ } + } + } + + Interlocked.Add(ref _totalBytesWritten, line.Length + 1); + } + catch (Exception ex) + { + try + { + WriteToConsoleError("[FileFlow] WriteLogEvent error: " + ex.Message); + } + catch { /* Do nothing */ } + } + + // Fan-out to secondary writers + try + { + if (_secondaryWriters != null) + { + lock (_secondaryWritersLock) + { + foreach (Action writer in _secondaryWriters) + { + try { writer(log, line); } + catch + { + // Do nothing + } + } + } + } + } + catch + { + // Do nothing + } + } + + + private void FormatText(LogEvent log, StringBuilder sb) + { + foreach (Action action in _compiledTemplate) + { + action(log, sb); + } + } + + + private void FormatJson(LogEvent log, StringBuilder sb, bool structured) + { + sb.Append('{'); + + // Timestamp + sb.Append("\"timestamp\":\""); + AppendJsonEscaped(LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb); + sb.Append("\","); + + // Level + sb.Append("\"level\":\""); + string lvlStr; + sb.Append(LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString()); + sb.Append("\","); + + if (structured) + { + // Monotonic hex correlation ID + int cid = Interlocked.Increment(ref _correlationSeed); + sb.Append("\"correlationId\":\""); + sb.Append(cid.ToString("x8")); + sb.Append("\","); + + sb.Append("\"host\":\""); + AppendJsonEscaped(Environment.MachineName, sb); + sb.Append("\","); + + sb.Append("\"pid\":"); + sb.Append(Process.GetCurrentProcess().Id); + sb.Append(','); + } + + // Category + sb.Append("\"category\":\""); + if (!string.IsNullOrEmpty(log.Category)) + { + AppendJsonEscaped(log.Category, sb); + } + + sb.Append("\","); + + // Message + sb.Append("\"message\":\""); + if (log.Message.Length > 0) + { + AppendJsonEscaped(log.Message.ToString(), sb); + } + + sb.Append('"'); + + // Exception + if (log.Exception != null) + { + sb.Append(",\"exception\":\""); + AppendJsonEscaped(log.Exception.ToString(), sb); + sb.Append('"'); + } + + // Properties (enrichers + event properties) + bool hasEnrichers = _enrichers.Count > 0; + bool hasProps = log.Properties.Count > 0; + if (hasEnrichers || hasProps) + { + sb.Append(",\"properties\":{"); + bool first = true; + + foreach (KeyValuePair> kv in _enrichers) + { + if (!first) + { + sb.Append(','); + } + + first = false; + sb.Append('"'); + AppendJsonEscaped(kv.Key, sb); + sb.Append("\":\""); + object val = kv.Value(log); + AppendJsonEscaped(val != null ? val.ToString() : "null", sb); + sb.Append('"'); + } + + foreach (var property in log.Properties.ToArray()) + { + if (!first) + { + sb.Append(','); + } + + first = false; + sb.Append('"'); + AppendJsonEscaped(property.Key, sb); + sb.Append("\":"); + if (property.Value == null) + { + sb.Append("null"); + } + else + { + sb.Append('"'); + AppendJsonEscaped(property.Value.ToString(), sb); + sb.Append('"'); + } + } + + sb.Append('}'); + } + + sb.Append('}'); + } + + private static void AppendJsonEscaped(string value, StringBuilder sb) + { + if (value == null) + { + return; + } + + foreach (char c in value) + { + switch (c) + { + case '"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + default: + if (char.IsControl(c)) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("x4")); + } + else + { + sb.Append(c); + } + + break; + } + } + } + + private void RepairXmlIfNeeded(string path) + { + try + { + if (!File.Exists(path)) + { + return; + } + + const string footer = ""; + const int tailSize = 512; + + using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + if (fs.Length < footer.Length) + { + return; + } + + int readSize = (int)Math.Min(tailSize, fs.Length); + fs.Seek(-readSize, SeekOrigin.End); + + byte[] buffer = new byte[readSize]; + fs.Read(buffer, 0, readSize); + + string tail = Encoding.UTF8.GetString(buffer); + + if (!tail.Contains(footer)) + { + fs.Seek(0, SeekOrigin.End); + using (var sw = new StreamWriter(fs, Encoding.UTF8, 1024, true)) + { + sw.WriteLine(); + sw.WriteLine(footer); + sw.Flush(); + } + } + } + } + catch + { + // Never throw during logging + } + } + + private void FormatXml(LogEvent log, StringBuilder sb) + { + sb.Append(""); + + string lvlStr; + AppendXmlElement("timestamp", LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb); + AppendXmlElement("level", LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString(), sb); + + if (!string.IsNullOrEmpty(log.Category)) + { + AppendXmlElement("category", log.Category, sb); + } + + sb.Append(""); + if (log.Message.Length > 0) + { + AppendXmlEscaped(log.Message.ToString(), sb); + } + + sb.Append(""); + + if (log.Exception != null) + { + AppendXmlElement("exception", log.Exception.ToString(), sb); + } + + bool hasEnrichers = _enrichers.Count > 0; + bool hasProps = log.Properties.Count > 0; + if (hasEnrichers || hasProps) + { + sb.Append(""); + + foreach (KeyValuePair> kv in _enrichers) + { + sb.Append(""); + object val = kv.Value(log); + AppendXmlEscaped(val != null ? val.ToString() : "null", sb); + sb.Append(""); + } + + foreach (var property in log.Properties.ToArray()) + { + if (string.IsNullOrEmpty(property.Key)) + { + continue; + } + + sb.Append(""); + if (property.Value != null) + { + AppendXmlEscaped(property.Value.ToString(), sb); + } + else + { + sb.Append("null"); + } + + sb.Append(""); + } + + sb.Append(""); + } + + sb.Append(""); + } + + private static void AppendXmlElement(string tag, string content, StringBuilder sb) + { + sb.Append('<').Append(tag).Append('>'); + AppendXmlEscaped(content, sb); + sb.Append("'); + } + + private static void AppendXmlEscaped(string value, StringBuilder sb) + { + if (value == null) + { + return; + } + + foreach (char c in value) + { + switch (c) + { + case '&': sb.Append("&"); break; + case '<': sb.Append("<"); break; + case '>': sb.Append(">"); break; + case '"': sb.Append("""); break; + case '\'': sb.Append("'"); break; + default: + if (char.IsControl(c) && c != '\r' && c != '\n' && c != '\t') + { + sb.Append('?'); + } + else + { + sb.Append(c); + } + + break; + } + } + } + + + private void FormatCsv(LogEvent log, StringBuilder sb) + { + string lvlStr; + + AppendCsvField(LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb); + sb.Append(','); + AppendCsvField(LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString(), sb); + sb.Append(','); + AppendCsvField(log.Category ?? string.Empty, sb); + sb.Append(','); + AppendCsvField(log.Message.Length > 0 ? log.Message.ToString() : string.Empty, sb); + sb.Append(','); + AppendCsvField(log.Exception != null ? log.Exception.ToString() : string.Empty, sb); + sb.Append(','); + + // Properties column: key=value; key=value + sb.Append('"'); + bool first = true; + foreach (KeyValuePair> kv in _enrichers) + { + if (!first) + { + sb.Append("; "); + } + + first = false; + AppendCsvInner(kv.Key, sb); + sb.Append('='); + object val = kv.Value(log); + AppendCsvInner(val != null ? val.ToString() : "null", sb); + } + foreach (var property in log.Properties.ToArray()) + { + if (!first) + { + sb.Append("; "); + } + + first = false; + AppendCsvInner(property.Key ?? string.Empty, sb); + sb.Append('='); + AppendCsvInner(property.Value != null ? property.Value.ToString() : "null", sb); + } + sb.Append('"'); + } + + private static void AppendCsvField(string value, StringBuilder sb) + { + if (value == null) + { + value = string.Empty; + } + + bool needsQuote = value.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0; + if (needsQuote) + { + sb.Append('"'); + } + + AppendCsvInner(value, sb); + if (needsQuote) + { + sb.Append('"'); + } + } + + private static void AppendCsvInner(string value, StringBuilder sb) + { + if (value == null) + { + return; + } + + foreach (char c in value) + { + if (c == '"') + { + // RFC 4180: escape quote by doubling + sb.Append('"'); + } + + sb.Append(c); + } + } + + private void EnsureFileOpen(string path, LogEvent logEvent) + { + try + { + OpenFile existing; + if (_openFiles.TryGetValue(path, out existing)) + { + try + { + if (File.Exists(path)) + { + return; + } + } + catch + { + // File.Exists failed (permissions, etc.) – assume file is gone + } + + // File was deleted or check failed; dispose stale handle + try { existing.Dispose(); } catch { /* ignore */ } + _openFiles.Remove(path); + } + + FileStream fs = null; + StreamWriter sw = null; + try + { + string dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + fs = new FileStream( + path, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete, + FileBufferSize, + FileOptions.SequentialScan); + + sw = new StreamWriter(fs, Encoding.UTF8, WriterBufferSize); + sw.AutoFlush = false; + + OpenFile of = new OpenFile(fs, sw, GetCurrentDate()); + _openFiles[path] = of; + + // From here on, resources are owned by _openFiles; clear locals + // so the finally block does not double-dispose. + fs = null; + sw = null; + + if (_outputFormat == FileOutputFormat.Csv && of.Stream.Length == 0) + { + try + { + of.Writer.Write(CsvHeader); + of.HasCsvHeader = true; + } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] CSV header write error for '" + path + "': " + ex.Message); + } + } + + if (_outputFormat == FileOutputFormat.Xml) + { + try + { + if (of.Stream.Length == 0) + { + of.Writer.WriteLine(""); + of.Writer.WriteLine(""); + of.HasXmlHeader = true; + } + else + { + of.Writer.Flush(); + RepairXmlIfNeeded(path); + of.HasXmlHeader = true; + } + } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] XML header write error for '" + path + "': " + ex.Message); + } + } + } + catch (Exception ex) + { + // Clean up partially-allocated resources that were never + // handed off to an OpenFile entry. + try { sw?.Dispose(); } catch { /* ignore */ } + try { fs?.Dispose(); } catch { /* ignore */ } + + // Remove any entry that may be in a broken state + OpenFile broken; + if (_openFiles.TryGetValue(path, out broken)) + { + try { broken.Dispose(); } catch { /* ignore */ } + _openFiles.Remove(path); + } + + WriteToConsoleError("[FileFlow] Failed to open '" + path + "': " + ex.Message); + } + } + catch (Exception ex) + { + // Outermost safety net – never let this method take down the process + try + { + WriteToConsoleError("[FileFlow] EnsureFileOpen unhandled error: " + ex.Message); + } + catch { /* ignore */ } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldRotate(string path, long additionalBytes) + { + OpenFile of; + if (!_openFiles.TryGetValue(path, out of)) + { + return false; + } + + return of.Size + additionalBytes > _maxFileSize + || of.Date.Date != GetCurrentDate() + || Interlocked.Read(ref _currentMemoryBytes) > (long)(_maxMemoryBytes * 0.9); + } + + private string RotateFile(string path) + { + OpenFile of; + if (_openFiles.TryGetValue(path, out of)) + { + if (_outputFormat == FileOutputFormat.Xml && of.HasXmlHeader) + { + try + { + of.Writer.WriteLine(""); + of.Writer.Flush(); + } + catch { } + } + + of.Dispose(); + _openFiles.Remove(path); + } + + if (!File.Exists(path)) + { + return null; + } + + string archived = ArchiveFile(path); + Interlocked.Increment(ref _totalRotations); + return archived; + } + + private string ArchiveFile(string filePath) + { + try + { + string dir = Path.GetDirectoryName(filePath); + string fileName = Path.GetFileName(filePath); + int extIdx = fileName.LastIndexOf(_fileExtension, StringComparison.OrdinalIgnoreCase); + string baseName = extIdx >= 0 ? fileName.Substring(0, extIdx) : fileName; + int maxFiles = _retention.MaxRolledFiles > 0 ? _retention.MaxRolledFiles : 999; + + for (int i = maxFiles - 1; i >= 1; i--) + { + string src = Path.Combine(dir, baseName + "_" + i + _fileExtension); + string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension); + if (!File.Exists(src)) + { + continue; + } + + if (File.Exists(dst)) + { + _compressionQueue.Enqueue(dst); + _compressionSignal.Release(1); + File.Delete(dst); + } + File.Move(src, dst); + } + + string archive = Path.Combine(dir, baseName + "_1" + _fileExtension); + if (File.Exists(archive)) + { + File.Delete(archive); + } + + File.Move(filePath, archive); + return archive; + } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] Archive error '" + filePath + "': " + ex.Message); + return null; + } + } + + private void CompressionThreadBody() + { + while (true) + { + try + { + _compressionSignal.Wait(_cts.Token); + } + catch (OperationCanceledException) + { + DrainCompressionQueue(); + return; + } + + DrainCompressionQueue(); + } + } + + private void DrainCompressionQueue() + { + string path; + while (_compressionQueue.TryDequeue(out path)) + { + try { CompressFile(path); } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] Compress error '" + path + "': " + ex.Message); + } + } + } + + private void CompressFile(string path) + { + if (_compressionFormat == CompressionFormat.None || !File.Exists(path)) + { + return; + } + + string outPath = path + ".gz"; + + if (File.Exists(outPath)) + { + string ts = DateTime.UtcNow.ToString("yyMMdd_HHmmss_fff"); + File.Move(outPath, path + "_" + ts + ".gz"); + } + + const int bufSize = 65536; + byte[] buffer = new byte[bufSize]; + + using (FileStream src = File.OpenRead(path)) + using (FileStream dst = File.Create(outPath)) + using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal)) + { + int read; + while ((read = src.Read(buffer, 0, bufSize)) > 0) + { + gz.Write(buffer, 0, read); + } + } + } + + private void PeriodicFlushLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + try + { + long mem = Interlocked.Read(ref _currentMemoryBytes); + int delay = mem > (long)(_maxMemoryBytes * 0.8) + ? 250 + : (int)_flushInterval.TotalMilliseconds; + + if (_cts.Token.WaitHandle.WaitOne(delay)) + { + break; + } + + OpenFile[] snapshot; + lock (_fileLock) + { + snapshot = new OpenFile[_openFiles.Count]; + _openFiles.Values.CopyTo(snapshot, 0); + } + + foreach (OpenFile of in snapshot) + { + try { of.Writer.Flush(); } catch { /* ignore */ } + } + } + catch (ThreadInterruptedException) { break; } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] Flush error: " + ex.Message); + } + } + } + + private void RetentionLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + if (_cts.Token.WaitHandle.WaitOne(TimeSpan.FromMinutes(15))) + { + break; + } + + ApplyRetention(); + } + } + + private void ApplyRetention() + { + try + { + DirectoryInfo dir = new DirectoryInfo(_directory); + if (!dir.Exists) + { + return; + } + + FileInfo[] files = dir.GetFiles("*" + _fileExtension) + .OrderByDescending(f => f.LastWriteTimeUtc) + .ToArray(); + + long totalBytes = 0; + int kept = 0; + + foreach (FileInfo f in files) + { + bool tooOld = _retention.MaxAgeDays > 0 + && (DateTime.UtcNow - f.LastWriteTimeUtc).TotalDays > _retention.MaxAgeDays; + bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles; + bool tooLarge = _retention.MaxTotalArchiveBytes > 0 + && totalBytes + f.Length > _retention.MaxTotalArchiveBytes; + + if (tooOld || tooMany || tooLarge) + { + try { f.Delete(); } catch { /* ignore */ } + } + else + { + totalBytes += f.Length; + kept++; + } + } + } + catch (Exception ex) + { + WriteToConsoleError("[FileFlow] Retention error: " + ex.Message); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private DateTime GetCurrentDate() + { + return _timestampMode == TimestampMode.Local ? DateTime.Now.Date : DateTime.UtcNow.Date; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GenerateFilePath(DateTime date, LogEvent log) + { + string prefix = _filePrefix; + + if (_useCategoryRouting && !string.IsNullOrEmpty(log.Category)) + { + prefix += "_" + log.Category; + } + + if (_logLevelsForSeparateFiles.Count > 0 && _logLevelsForSeparateFiles.Contains(log.Level)) + { + string lvlStr; + prefix += "_" + (LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString()); + } + + return Path.Combine( + _directory, + prefix + "_" + Environment.MachineName + "_" + date.ToString("yyyyMMdd") + _fileExtension); + } + + private void SetFileExtension(FileOutputFormat fmt) + { + switch (fmt) + { + case FileOutputFormat.Json: + case FileOutputFormat.StructuredJson: + _fileExtension = ".json"; break; + case FileOutputFormat.Xml: + _fileExtension = ".xml"; break; + case FileOutputFormat.Csv: + _fileExtension = ".csv"; break; + default: + _fileExtension = ".log"; break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long EstimateSize(LogEvent log) + { + long s = 200L + + log.Message.Length * 2 + + (log.Category != null ? log.Category.Length * 2 : 0) + + log.Properties.Count * 40; + if (log.Exception != null) + { + s += log.Exception.ToString().Length * 2; + } + + return s; + } + + private void CompileTemplate(string template) + { + _compiledTemplate = new List>(); + int pos = 0; + + while (pos < template.Length) + { + int open = template.IndexOf('{', pos); + if (open < 0) + { + string lit = template.Substring(pos); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + break; + } + + if (open > pos) + { + string lit = template.Substring(pos, open - pos); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + } + + int close = template.IndexOf('}', open); + if (close < 0) + { + string lit = template.Substring(open); + _compiledTemplate.Add((_, sb) => sb.Append(lit)); + break; + } + + string token = template.Substring(open + 1, close - (open + 1)); + _compiledTemplate.Add(ResolveToken(token)); + pos = close + 1; + } + } + + private Action ResolveToken(string token) + { + switch (token.ToLowerInvariant()) + { + case "ts": + return (log, sb) => + sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("yyyy-MM-dd HH:mm:ss.fff")); + case "tz": + return (log, sb) => + sb.Append(_timestampMode == TimestampMode.Local + ? TimeZoneInfo.Local.StandardName : "UTC"); + case "host": + return (log, sb) => sb.Append(Environment.MachineName); + case "category": + return (log, sb) => { if (log.Category != null) { sb.Append(log.Category); } }; + case "thread": + return (log, sb) => sb.Append(Thread.CurrentThread.ManagedThreadId); + case "logtype": + return (log, sb) => + { + string s; + sb.Append(LevelStrings.TryGetValue(log.Level, out s) ? s : log.Level.ToString()); + }; + case "message": + return (log, sb) => + { + if (log.Message.Length > 0) + { + sb.Append(log.Message.ToString()); + } + }; + case "exception": + return (log, sb) => + { + if (log.Exception != null) + { + sb.Append(log.Exception.ToString()); + } + }; + case "props": + return (log, sb) => AppendProperties(log, sb); + case "newline": + return (log, sb) => sb.AppendLine(); + case "pid": + return (log, sb) => sb.Append(Process.GetCurrentProcess().Id); + default: + return BuildCustomOrLiteralToken(token); + } + } + + private Action BuildCustomOrLiteralToken(string token) + { + string name = token; + return (log, sb) => + { + Action custom; + if (_customTokens.TryGetValue(name, out custom)) + { + custom(log, sb); + } + else + { + sb.Append('{').Append(name).Append('}'); + } + }; + } + + private void AppendProperties(LogEvent log, StringBuilder sb) + { + bool hasEnrichers = _enrichers.Count > 0; + bool hasProps = log.Properties.Count > 0; + if (!hasEnrichers && !hasProps) + { + return; + } + + sb.Append(" {"); + bool first = true; + + foreach (KeyValuePair> kv in _enrichers) + { + if (!first) + { + sb.Append(", "); + } + + first = false; + object val = kv.Value(log); + sb.Append(kv.Key).Append('=').Append(val != null ? val.ToString() : "null"); + } + + foreach (var property in log.Properties.ToArray()) + { + if (!first) + { + sb.Append(", "); + } + + first = false; + sb.Append(property.Key).Append('=') + .Append(property.Value != null ? property.Value.ToString() : "null"); + } + + sb.Append('}'); + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/GrayLogFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/GrayLogFlow.cs new file mode 100644 index 0000000..aa24a09 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/GrayLogFlow.cs @@ -0,0 +1,287 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class GraylogFlow : FlowBase + { + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + private const int MaxUdpPacketSize = 8192; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private readonly bool _useTcp; + private TcpClient? _tcpClient; + private NetworkStream? _tcpStream; + private UdpClient? _udpClient; + private readonly BackpressureStrategy _backpressureStrategy; + private readonly string _graylogHostName; + + public GraylogFlow( + string host, + int port = 12201, + bool useTcp = false, + string graylogHostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest) + : base($"Graylog:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _useTcp = useTcp; + _backpressureStrategy = backpressureStrategy; + _graylogHostName = graylogHostName ?? Environment.MachineName; + _batchSize = batchSize <= 0 ? 1 : batchSize; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy switch + { + BackpressureStrategy.Wait => BoundedChannelFullMode.Wait, + BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite, + BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest, + _ => BoundedChannelFullMode.Wait + }, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + + if (!_useTcp) + { + _udpClient = new UdpClient(); + } + + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + if (_useTcp) + { + await EnsureTcpConnectedAsync(cancellationToken); + } + + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"GraylogFlow error: {ex.Message}"); + await Task.Delay(1000, cancellationToken); + _tcpClient?.Dispose(); + _tcpClient = null; + } + } + } + + private async Task EnsureTcpConnectedAsync(CancellationToken cancellationToken) + { + if (_tcpClient != null && _tcpClient.Connected) + { + return; + } + + _tcpClient?.Dispose(); + _tcpClient = new TcpClient(); + await _tcpClient.ConnectAsync(_host, _port); + _tcpStream = _tcpClient.GetStream(); + } + + private static double ToUnixTimeSeconds(DateTime dt) + { + // Make sure the DateTime is UTC + var utc = dt.ToUniversalTime(); + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + return (utc - epoch).TotalSeconds; + } + + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + var unixTimestamp = ToUnixTimeSeconds(dt); + + var gelfMessage = new + { + version = "1.1", + host = _graylogHostName, + short_message = logEvent.Message, + timestamp = unixTimestamp, + level = MapLogLevelToSyslogSeverity(logEvent.Level), + _category = logEvent.Category + }; + + string json = JsonHelper.ToJson(gelfMessage); + byte[] data = Encoding.UTF8.GetBytes(json); + + if (_useTcp) + { + if (_tcpStream != null) + { + await _tcpStream.WriteAsync(data, 0, data.Length, cancellationToken); + await _tcpStream.FlushAsync(cancellationToken); + } + } + else + { + if (_udpClient != null) + { + if (data.Length <= MaxUdpPacketSize) + { + await _udpClient.SendAsync(data, data.Length, _host, _port); + } + else + { + await SendUdpInChunksAsync(data, MaxUdpPacketSize, cancellationToken); + } + } + } + } + } + + private async Task SendUdpInChunksAsync(byte[] data, int chunkSize, CancellationToken cancellationToken) + { + int offset = 0; + byte[] buffer = ArrayPool.Shared.Rent(chunkSize); + + try + { + while (offset < data.Length) + { + int size = Math.Min(chunkSize, data.Length - offset); + Buffer.BlockCopy(data, offset, buffer, 0, size); + if (_udpClient != null) + { + await _udpClient.SendAsync(buffer, size, _host, _port); + } + + offset += size; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private int MapLogLevelToSyslogSeverity(LogLevel level) + { + return level switch + { + LogLevel.Trace => 7, + LogLevel.Debug => 7, + LogLevel.Information => 6, + LogLevel.Warning => 4, + LogLevel.Error => 3, + LogLevel.Critical => 2, + _ => 6 + }; + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _tcpStream?.Dispose(); + _tcpClient?.Dispose(); + _udpClient?.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/HttpFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/HttpFlow.cs new file mode 100644 index 0000000..8575b34 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/HttpFlow.cs @@ -0,0 +1,285 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows; + +/// +/// HTTP flow for sending logs to remote endpoints with batching and retry logic +/// +public sealed class HttpFlow : FlowBase +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + private const int ChannelCapacity = 2048; + private readonly int _batchSize; + private const int MaxRetries = 3; + + private readonly Channel _channel; + private readonly Task _writerTask; + private readonly CancellationTokenSource _cts; + + private readonly HttpClient _httpClient; + private readonly string _endpoint; + private readonly bool _ownHttpClient; + private readonly TimeSpan _batchInterval; + private readonly Dictionary? _headers; + + public HttpFlow( + string endpoint, + HttpClient? httpClient = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Information, + TimeSpan? batchInterval = null, + Dictionary? headers = null) + : base($"Http:{endpoint}", minimumLevel) + { + _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + _batchInterval = batchInterval ?? TimeSpan.FromSeconds(5); + _headers = headers; + + if (httpClient == null) + { + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + _ownHttpClient = true; + } + else + { + _httpClient = httpClient; + _ownHttpClient = false; + } + + _batchSize = batchSize <= 0 ? 1 : batchSize; + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _writerTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + + try + { + await _writerTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected + } + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (!cancellationToken.IsCancellationRequested) + { + var hasMore = true; + + // Collect batch + while (batch.Count < _batchSize && hasMore) + { + if (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + } + else + { + hasMore = false; + } + } + + // Send batch if we have events + if (batch.Count > 0) + { + await SendBatchWithRetryAsync(batch, cancellationToken).ConfigureAwait(false); + batch.Clear(); + } + + // Wait for either new events or batch interval + if (_channel.Reader.Count == 0) + { + try + { + await Task.Delay(_batchInterval, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected if cancellation was requested during delay + break; + } + } + } + } + catch (OperationCanceledException) + { + // Expected when shutting down + } + catch (Exception ex) + { + Console.Error.WriteLine($"HttpFlow error: {ex.Message}"); + } + } + + private async Task SendBatchWithRetryAsync(List batch, CancellationToken cancellationToken) + { + var payload = SerializeBatch(batch); + + // Serialize payload to JSON string + var jsonPayload = JsonHelper.ToJson(payload); + + for (int retry = 0; retry < MaxRetries; retry++) + { + try + { + using (var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json")) + using (var request = new HttpRequestMessage(HttpMethod.Post, _endpoint) { Content = content }) + { + if (_headers != null) + { + foreach (var header in _headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + return; // Success + } + + // Last retry: mark as dropped + if (retry == MaxRetries - 1) + { + Interlocked.Add(ref DroppedCount, batch.Count); + } + } + } + catch + { + if (retry == MaxRetries - 1) + { + Interlocked.Add(ref DroppedCount, batch.Count); + } + } + + // Exponential backoff + if (retry < MaxRetries - 1) + { + await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, retry)), cancellationToken).ConfigureAwait(false); + } + } + } + + private object[] SerializeBatch(List batch) + { + var payload = new object[batch.Count]; + + for (int i = 0; i < batch.Count; i++) + { + var logEvent = batch[i]; + var dto = new Dictionary + { + ["timestamp"] = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"), + ["level"] = logEvent.Level.ToString(), + ["message"] = logEvent.Message.ToString(), + ["category"] = logEvent.Category, + ["threadId"] = logEvent.ThreadId + }; + + if (logEvent.TraceId != default) + { + dto["traceId"] = logEvent.TraceId.ToString(); + } + + if (logEvent.SpanId != default) + { + dto["spanId"] = logEvent.SpanId.ToString(); + } + + if (logEvent.Exception != null) + { + dto["exception"] = new + { + type = logEvent.Exception.GetType().FullName, + message = logEvent.Exception.Message, + stackTrace = logEvent.Exception.StackTrace + }; + } + + if (logEvent.Properties.Count > 0) + { + var props = new Dictionary(); + foreach (var prop in logEvent.Properties) + { + props[prop.Key] = prop.Value; + } + dto["properties"] = props; + } + + payload[i] = dto; + } + + return payload; + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _writerTask.ConfigureAwait(false); + } + catch { } + + if (_ownHttpClient) + { + _httpClient.Dispose(); + } + + _cts.Dispose(); + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/MemoryFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/MemoryFlow.cs new file mode 100644 index 0000000..1710cd6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/MemoryFlow.cs @@ -0,0 +1,180 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// In-memory flow with circular buffer for diagnostics and testing. +/// Designed for high-speed logging with bounded memory usage. +/// +public sealed class MemoryFlow : FlowBase +{ + private readonly LogEvent[] _buffer; + private readonly int _capacity; + private int _head; + private int _tail; + private int _count; + private readonly object _lock = new(); + + public MemoryFlow( + int capacity = 10000, + LogLevel minimumLevel = LogLevel.Trace) + : base("Memory", minimumLevel) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + _capacity = capacity; + _buffer = new LogEvent[capacity]; + _head = 0; + _tail = 0; + _count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + lock (_lock) + { + _buffer[_tail] = logEvent; + _tail = (_tail + 1) % _capacity; + + if (_count == _capacity) + { + // Buffer is full, overwrite oldest + _head = (_head + 1) % _capacity; + Interlocked.Increment(ref DroppedCount); + } + else + { + _count++; + } + } + + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + // No-op for memory flow + return Task.CompletedTask; + } + + /// + /// Gets all log events currently in the buffer + /// + public LogEvent[] GetEvents() + { + lock (_lock) + { + var events = new LogEvent[_count]; + + for (int i = 0; i < _count; i++) + { + var index = (_head + i) % _capacity; + events[i] = _buffer[index]; + } + + return events; + } + } + + /// + /// Gets events matching the specified level + /// + public LogEvent[] GetEvents(LogLevel level) + { + lock (_lock) + { + var matching = new List(_count); + + for (int i = 0; i < _count; i++) + { + var index = (_head + i) % _capacity; + if (_buffer[index].Level == level) + { + matching.Add(_buffer[index]); + } + } + + return matching.ToArray(); + } + } + + /// + /// Gets the most recent N events + /// + public LogEvent[] GetRecentEvents(int count) + { + lock (_lock) + { + var actualCount = Math.Min(count, _count); + var events = new LogEvent[actualCount]; + + for (int i = 0; i < actualCount; i++) + { + var index = (_tail - actualCount + i + _capacity) % _capacity; + events[i] = _buffer[index]; + } + + return events; + } + } + + /// + /// Clears all events from the buffer + /// + public void Clear() + { + lock (_lock) + { + Array.Clear(_buffer, 0, _buffer.Length); + _head = 0; + _tail = 0; + _count = 0; + } + } + + /// + /// Gets the current count of events in the buffer + /// + public int Count + { + get + { + lock (_lock) + { + return _count; + } + } + } + + /// + /// Gets whether the buffer is full + /// + public bool IsFull + { + get + { + lock (_lock) + { + return _count == _capacity; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/MicrosoftTeamsFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/MicrosoftTeamsFlow.cs new file mode 100644 index 0000000..0670040 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/MicrosoftTeamsFlow.cs @@ -0,0 +1,180 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// logging flow that sends messages to Microsoft Teams via an incoming webhook. + /// + public sealed class MicrosoftTeamsFlow : FlowBase, IAsyncDisposable + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _workerTask; + private readonly CancellationTokenSource _cts; + private readonly HttpClient _httpClient; + private readonly string _webhookUrl; + + public MicrosoftTeamsFlow( + string webhookUrl, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Information) + : base("MicrosoftTeams", minimumLevel) + { + _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl)); + _httpClient = new HttpClient(); + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _workerTask = Task.Run(() => ProcessQueueAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + private async Task ProcessQueueAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"MicrosoftTeamsFlow error: {ex.Message}"); + } + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var payload = new + { + // Teams expects a "text" field with Markdown or simple message + text = BuildMessage(logEvent) + }; + + var json = JsonHelper.ToJson(payload); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(_webhookUrl, content, cancellationToken); + } + } + + private static string BuildMessage(LogEvent logEvent) + { + var sb = new StringBuilder(); + sb.AppendLine($"**Level:** {logEvent.Level}"); + if (!string.IsNullOrWhiteSpace(logEvent.Category)) + { + sb.AppendLine($"**Category:** {logEvent.Category}"); + } + + sb.AppendLine($"**Timestamp:** {LogEvent.GetDateTime(logEvent.Timestamp):yyyy-MM-dd HH:mm:ss.fff}"); + sb.AppendLine($"**Message:** {logEvent.Message}"); + + if (logEvent.Exception != null) + { + sb.AppendLine("**Exception:**"); + sb.AppendLine($"```\n{logEvent.Exception.GetType().FullName}: {logEvent.Exception.Message}\n{logEvent.Exception.StackTrace}\n```"); + } + + if (logEvent.Properties.Count > 0) + { + sb.AppendLine("**Properties:**"); + foreach (var prop in logEvent.Properties) + { + sb.AppendLine($"`{prop.Key}` = `{prop.Value?.ToString() ?? "null"}`"); + } + } + + return sb.ToString(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/RedisFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RedisFlow.cs new file mode 100644 index 0000000..0e78797 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RedisFlow.cs @@ -0,0 +1,401 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) + /// and optionally appends them to a Redis List (LPUSH) for persistence. + /// + /// Uses raw TCP + RESP protocol, so there arent additional dependencies + /// + /// Features: + /// - Reconnect with exponential back-off on connection failure + /// - Optional LPUSH to a list key with LTRIM to cap list length + /// - Optional password authentication (AUTH command) + /// - Optional DB selection (SELECT command) + /// - Background writer thread (non-blocking callers) + /// + public sealed class RedisFlow : FlowBase + { + private readonly string _host; + private readonly int _port; + private readonly string _password; + private readonly int _database; + private readonly string _channel; // PUBLISH channel (Pub/Sub) + private readonly string _listKey; // LPUSH list key (null = disabled) + private readonly int _maxListLength; // LTRIM cap (0 = unlimited) + + private readonly BlockingCollection _queue; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _writerThread; + + private TcpClient _tcp; + private NetworkStream _stream; + private readonly object _connLock = new object(); + + private long _totalPublished; + + public RedisFlow( + string host = "localhost", + int port = 6379, + string password = null, + int database = 0, + string channel = "eonacat:logs", + string listKey = null, + int maxListLength = 10000, + LogLevel minimumLevel = LogLevel.Trace) + : base("Redis:" + host + ":" + port, minimumLevel) + { + if (host == null) + { + throw new ArgumentNullException("host"); + } + + if (channel == null) + { + throw new ArgumentNullException("channel"); + } + + _host = host; + _port = port; + _password = password; + _database = database; + _channel = channel; + _listKey = listKey; + _maxListLength = maxListLength; + + _queue = new BlockingCollection(new ConcurrentQueue(), 16384); + + _writerThread = new Thread(WriterLoop) + { + IsBackground = true, + Name = "RedisFlow.Writer", + Priority = ThreadPriority.AboveNormal + }; + _writerThread.Start(); + } + + public long TotalPublished { get { return Interlocked.Read(ref _totalPublished); } } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + string json = Serialize(logEvent); + if (_queue.TryAdd(json)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + WriteResult result = WriteResult.Success; + foreach (LogEvent e in logEvents.ToArray()) + { + if (e.Level < MinimumLevel) + { + continue; + } + + if (BlastAsync(e, cancellationToken).GetAwaiter().GetResult() == WriteResult.Dropped) + { + result = WriteResult.Dropped; + } + } + return Task.FromResult(result); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); + while (_queue.Count > 0 && sw.Elapsed < TimeSpan.FromSeconds(5)) + { + Thread.Sleep(5); + } + + return Task.FromResult(0); + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _queue.CompleteAdding(); + _cts.Cancel(); + _writerThread.Join(TimeSpan.FromSeconds(5)); + Disconnect(); + _cts.Dispose(); + _queue.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + + private void WriterLoop() + { + int backoff = 500; + + while (!_queue.IsCompleted) + { + try + { + EnsureConnected(); + backoff = 500; // reset on successful connect + + while (!_queue.IsCompleted) + { + string msg; + try { msg = _queue.Take(_cts.Token); } + catch (OperationCanceledException) { return; } + catch (InvalidOperationException) { return; } + + SendToRedis(msg); + + string extra; + int batch = 0; + while (batch < 64 && _queue.TryTake(out extra)) + { + SendToRedis(extra); + batch++; + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine("[RedisFlow] Error: " + ex.Message + ". Reconnecting in " + backoff + "ms."); + Disconnect(); + try { Thread.Sleep(backoff); } catch (ThreadInterruptedException) { return; } + backoff = Math.Min(backoff * 2, 30000); + } + } + } + + private void SendToRedis(string json) + { + // PUBLISH channel message + SendCommand("PUBLISH", _channel, json); + + // Optional list persistence + if (!string.IsNullOrEmpty(_listKey)) + { + SendCommand("LPUSH", _listKey, json); + if (_maxListLength > 0) + { + SendCommand("LTRIM", _listKey, "0", (_maxListLength - 1).ToString()); + } + } + + Interlocked.Increment(ref _totalPublished); + } + + private void EnsureConnected() + { + lock (_connLock) + { + if (_tcp != null && _tcp.Connected) + { + return; + } + + Disconnect(); + _tcp = new TcpClient(); + _tcp.Connect(_host, _port); + _stream = _tcp.GetStream(); + + if (!string.IsNullOrEmpty(_password)) + { + SendCommandRaw("AUTH", _password); + ReadResp(); // +OK + } + + if (_database != 0) + { + SendCommandRaw("SELECT", _database.ToString()); + ReadResp(); // +OK + } + } + } + + private void Disconnect() + { + lock (_connLock) + { + if (_stream != null) { try { _stream.Dispose(); } catch { } _stream = null; } + if (_tcp != null) { try { _tcp.Dispose(); } catch { } _tcp = null; } + } + } + + // Build RESP array and write + read response (inline, synchronous) + private void SendCommand(params string[] args) + { + SendCommandRaw(args); + ReadResp(); // discard but catch errors + } + + private void SendCommandRaw(params string[] args) + { + var sb = new StringBuilder(); + sb.Append('*').Append(args.Length).Append("\r\n"); + foreach (string arg in args) + { + byte[] bytes = Encoding.UTF8.GetBytes(arg); + sb.Append('$').Append(bytes.Length).Append("\r\n"); + // Append raw bytes via stream after the header + byte[] header = Encoding.ASCII.GetBytes(sb.ToString()); + sb.Clear(); + _stream.Write(header, 0, header.Length); + _stream.Write(bytes, 0, bytes.Length); + _stream.WriteByte(0x0D); // \r + _stream.WriteByte(0x0A); // \n + } + } + + private void ReadResp() + { + // Read one RESP line we only need to consume the response, + // error checking is minimal (connection drop will be caught upstream) + int b = _stream.ReadByte(); + if (b == -1) + { + throw new Exception("Redis connection closed."); + } + + if ((char)b == '-') // error line + { + StringBuilder err = new StringBuilder(); + int c; + while ((c = _stream.ReadByte()) != -1 && (char)c != '\r') + { + err.Append((char)c); + } + + _stream.ReadByte(); // consume \n + throw new Exception("Redis error: " + err); + } + // Consume remainder of the line + while (true) + { + int c = _stream.ReadByte(); + if (c == -1 || (char)c == '\n') + { + break; + } + } + } + + private static string Serialize(LogEvent log) + { + var sb = new StringBuilder(256); + sb.Append("{\"ts\":\""); + sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O")); + sb.Append("\",\"level\":\""); + sb.Append(LevelString(log.Level)); + sb.Append("\",\"host\":\""); + JsonEscape(Environment.MachineName, sb); + sb.Append("\",\"category\":\""); + JsonEscape(log.Category ?? string.Empty, sb); + sb.Append("\",\"message\":\""); + JsonEscape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty, sb); + sb.Append('"'); + + if (log.Exception != null) + { + sb.Append(",\"exception\":\""); + JsonEscape(log.Exception.ToString(), sb); + sb.Append('"'); + } + + if (log.Properties.Count > 0) + { + sb.Append(",\"props\":{"); + bool first = true; + foreach (var kv in log.Properties.ToArray()) + { + if (!first) + { + sb.Append(','); + } + + first = false; + sb.Append('"'); + JsonEscape(kv.Key, sb); + sb.Append("\":\""); + JsonEscape(kv.Value != null ? kv.Value.ToString() : "null", sb); + sb.Append('"'); + } + sb.Append('}'); + } + + sb.Append('}'); + return sb.ToString(); + } + + private static void JsonEscape(string value, StringBuilder sb) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + foreach (char c in value) + { + switch (c) + { + case '"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + default: + if (char.IsControl(c)) { sb.Append("\\u"); sb.Append(((int)c).ToString("x4")); } + else + { + sb.Append(c); + } + + break; + } + } + } + + private static string LevelString(LogLevel level) + { + switch (level) + { + case LogLevel.Trace: return "TRACE"; + case LogLevel.Debug: return "DEBUG"; + case LogLevel.Information: return "INFO"; + case LogLevel.Warning: return "WARN"; + case LogLevel.Error: return "ERROR"; + case LogLevel.Critical: return "CRITICAL"; + default: return level.ToString().ToUpperInvariant(); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/RetryFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RetryFlow.cs new file mode 100644 index 0000000..a38a0ad --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RetryFlow.cs @@ -0,0 +1,67 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class RetryFlow : FlowBase + { + private readonly IFlow _inner; + private readonly int _maxRetries; + private readonly TimeSpan _initialDelay; + private readonly bool _exponentialBackoff; + + public RetryFlow( + IFlow inner, + int maxRetries = 3, + TimeSpan? initialDelay = null, + bool exponentialBackoff = true) + : base($"Retry({_innerName(inner)})", inner.MinimumLevel) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _maxRetries = maxRetries; + _initialDelay = initialDelay ?? TimeSpan.FromMilliseconds(200); + _exponentialBackoff = exponentialBackoff; + } + + public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + int attempt = 0; + TimeSpan delay = _initialDelay; + + while (true) + { + var result = await _inner.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + + if (result == WriteResult.Success || attempt >= _maxRetries) + { + return result; + } + + attempt++; + + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + + if (_exponentialBackoff) + { + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); + } + } + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + => _inner.FlushAsync(cancellationToken); + + public override ValueTask DisposeAsync() + => _inner.DisposeAsync(); + + private static string _innerName(IFlow flow) => flow.Name ?? flow.GetType().Name; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/RollingBufferFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RollingBufferFlow.cs new file mode 100644 index 0000000..2f325ac --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/RollingBufferFlow.cs @@ -0,0 +1,239 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// An in-process circular buffer that retains the last N log events in memory. + /// + public sealed class RollingBufferFlow : FlowBase + { + private readonly LogEvent[] _ring; + private readonly int _capacity; + private long _head; // next write position (mod capacity) + private long _count; // total ever written (not capped) + private readonly object _ringLock = new object(); + + private readonly LogLevel _triggerLevel; + private readonly IFlow _triggerTarget; + private readonly int _preContextLines; + + /// Maximum number of events to retain. + /// Minimum level to store in the buffer. + /// + /// When a log event reaches this level or above, the current buffer + /// contents are immediately forwarded to . + /// Set to LogLevel.None (or omit) to disable. + /// + /// + /// Flow to forward the buffered context to when the trigger fires. + /// Can be null even when is set. + /// + /// + /// How many buffered lines to forward before the triggering event. + /// Defaults to entire buffer (int.MaxValue). + /// + public RollingBufferFlow( + int capacity = 500, + LogLevel minimumLevel = LogLevel.Trace, + LogLevel triggerLevel = LogLevel.Error, + IFlow triggerTarget = null, + int preContextLines = int.MaxValue) + : base("RollingBuffer", minimumLevel) + { + if (capacity < 1) + { + throw new ArgumentOutOfRangeException("capacity", "Must be >= 1."); + } + + _capacity = capacity; + _ring = new LogEvent[capacity]; + _triggerLevel = triggerLevel; + _triggerTarget = triggerTarget; + _preContextLines = preContextLines < 0 ? 0 : preContextLines; + } + + /// Returns a snapshot of the buffer from oldest to newest. + public LogEvent[] GetAll() + { + lock (_ringLock) + { + long total = Math.Min(_count, _capacity); + if (total == 0) + { + return new LogEvent[0]; + } + + LogEvent[] result = new LogEvent[total]; + long start = (_count > _capacity) ? _head : 0; + + for (long i = 0; i < total; i++) + { + result[i] = _ring[(start + i) % _capacity]; + } + + return result; + } + } + + /// Returns the N most recent events. + public LogEvent[] GetRecent(int n) + { + lock (_ringLock) + { + long total = Math.Min(_count, _capacity); + long take = Math.Min(n, total); + if (take <= 0) + { + return new LogEvent[0]; + } + + LogEvent[] result = new LogEvent[take]; + long start = (_head - take + _capacity * 2) % _capacity; + + for (long i = 0; i < take; i++) + { + result[i] = _ring[(start + i) % _capacity]; + } + + return result; + } + } + + /// Returns all events at or above . + public LogEvent[] GetByLevel(LogLevel level) + { + LogEvent[] all = GetAll(); + List filtered = new List(all.Length); + foreach (LogEvent e in all) + { + if (e.Level >= level) + { + filtered.Add(e); + } + } + + return filtered.ToArray(); + } + + /// Number of events currently held in the buffer. + public int Count + { + get { lock (_ringLock) { return (int)Math.Min(_count, _capacity); } } + } + + /// Total events ever written (may exceed capacity). + public long TotalWritten { get { return Interlocked.Read(ref _count); } } + + /// Clears all stored events. + public void Clear() + { + lock (_ringLock) + { + Array.Clear(_ring, 0, _capacity); + _head = 0; + _count = 0; + } + } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + bool triggered = false; + lock (_ringLock) + { + _ring[_head % _capacity] = logEvent; + _head = (_head + 1) % _capacity; + _count++; + + if (_triggerTarget != null + && logEvent.Level >= _triggerLevel + && _triggerLevel != LogLevel.None) + { + triggered = true; + } + } + + Interlocked.Increment(ref BlastedCount); + + if (triggered) + { + ForwardToTarget(logEvent); + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (LogEvent e in logEvents.ToArray()) + { + BlastAsync(e, cancellationToken); + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(0); + + public override ValueTask DisposeAsync() + { + IsEnabled = false; + Clear(); + return base.DisposeAsync(); + } + + private void ForwardToTarget(LogEvent triggeringEvent) + { + if (_triggerTarget == null) + { + return; + } + + try + { + // Grab context window + int take = _preContextLines == int.MaxValue ? _capacity : _preContextLines; + LogEvent[] context = GetRecent(take); + + foreach (LogEvent ev in context) + { + if (ev.Equals(triggeringEvent)) + { + continue; // avoid duplicate + } + + _triggerTarget.BlastAsync(ev).GetAwaiter().GetResult(); + } + + // Always forward the triggering event last + _triggerTarget.BlastAsync(triggeringEvent).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.Error.WriteLine("[RollingBufferFlow] Trigger forward error: " + ex.Message); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SignalRFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SignalRFlow.cs new file mode 100644 index 0000000..fc200a4 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SignalRFlow.cs @@ -0,0 +1,317 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. + /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. + /// + /// A lightweight alternative to the SignalR client library + /// + /// On the server side you need a minimal hub endpoint that accepts POST: + /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } + /// + /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and + /// render events in real time. + /// + public sealed class SignalRFlow : FlowBase + { + private readonly string _hubUrl; + private readonly string _hubMethod; + private readonly HttpClient _http; + private readonly bool _ownsHttpClient; + private readonly int _batchSize; + private readonly TimeSpan _batchInterval; + + private readonly List _pending = new List(); + private readonly object _lock = new object(); + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Thread _senderThread; + + private long _totalSent; + + public SignalRFlow( + string hubUrl, + string hubMethod = "ReceiveLog", + HttpClient httpClient = null, + int batchSize = 20, + int batchIntervalMs = 500, + LogLevel minimumLevel = LogLevel.Information) + : base("SignalR:" + hubUrl, minimumLevel) + { + if (hubUrl == null) + { + throw new ArgumentNullException("hubUrl"); + } + + _hubUrl = hubUrl.TrimEnd('/'); + _hubMethod = hubMethod ?? "ReceiveLog"; + _batchSize = batchSize < 1 ? 1 : batchSize; + _batchInterval = TimeSpan.FromMilliseconds(batchIntervalMs < 50 ? 50 : batchIntervalMs); + + if (httpClient == null) + { + _http = new HttpClient(); + _http.Timeout = TimeSpan.FromSeconds(10); + _ownsHttpClient = true; + } + else + { + _http = httpClient; + _ownsHttpClient = false; + } + + _senderThread = new Thread(SenderLoop) + { + IsBackground = true, + Name = "SignalRFlow.Sender", + Priority = ThreadPriority.BelowNormal + }; + _senderThread.Start(); + } + + public long TotalSent { get { return Interlocked.Read(ref _totalSent); } } + + public override Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + string json = Serialize(logEvent); + bool sendNow = false; + + lock (_lock) + { + _pending.Add(json); + if (_pending.Count >= _batchSize) + { + sendNow = true; + } + } + + if (sendNow) + { + DispatchBatch(); + } + + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + public override Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return Task.FromResult(WriteResult.FlowDisabled); + } + + foreach (LogEvent e in logEvents.ToArray()) + { + if (IsLogLevelEnabled(e)) + { + BlastAsync(e, cancellationToken); + } + } + + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + DispatchBatch(); + return Task.FromResult(0); + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _cts.Cancel(); + _senderThread.Join(TimeSpan.FromSeconds(5)); + DispatchBatch(); + if (_ownsHttpClient) + { + _http.Dispose(); + } + + _cts.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + + private void SenderLoop() + { + while (!_cts.Token.IsCancellationRequested) + { + try { Thread.Sleep(_batchInterval); } + catch (ThreadInterruptedException) { break; } + DispatchBatch(); + } + } + + private void DispatchBatch() + { + List batch; + lock (_lock) + { + if (_pending.Count == 0) + { + return; + } + + batch = new List(_pending); + _pending.Clear(); + } + + ThreadPool.QueueUserWorkItem(_ => + { + try { SendBatch(batch); } + catch (Exception ex) + { + Console.Error.WriteLine("[SignalRFlow] Send error: " + ex.Message); + Interlocked.Add(ref DroppedCount, batch.Count); + } + }); + } + + private void SendBatch(List jsonEvents) + { + // Build envelope: { "target": "ReceiveLog", "arguments": [ [...events...] ] } + var sb = new StringBuilder(jsonEvents.Count * 200 + 64); + sb.Append("{\"target\":\""); + JsonEscape(_hubMethod, sb); + sb.Append("\",\"arguments\":[["); + for (int i = 0; i < jsonEvents.Count; i++) + { + if (i > 0) + { + sb.Append(','); + } + + sb.Append(jsonEvents[i]); + } + sb.Append("]]}"); + + string payload = sb.ToString(); + StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); + HttpResponseMessage resp = _http.PostAsync(_hubUrl + "/send", content).GetAwaiter().GetResult(); + + if (resp.IsSuccessStatusCode) + { + Interlocked.Add(ref _totalSent, jsonEvents.Count); + } + else + { + Console.Error.WriteLine("[SignalRFlow] HTTP " + (int)resp.StatusCode + " from hub."); + Interlocked.Add(ref DroppedCount, jsonEvents.Count); + } + } + + private static string Serialize(LogEvent log) + { + var sb = new StringBuilder(256); + sb.Append('{'); + sb.Append("\"ts\":\""); + sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O")); + sb.Append("\",\"level\":\""); + sb.Append(LevelString(log.Level)); + sb.Append("\",\"category\":\""); + JsonEscape(log.Category ?? string.Empty, sb); + sb.Append("\",\"message\":\""); + JsonEscape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty, sb); + sb.Append('"'); + + if (log.Exception != null) + { + sb.Append(",\"exception\":\""); + JsonEscape(log.Exception.GetType().Name + ": " + log.Exception.Message, sb); + sb.Append('"'); + } + + if (log.Properties.Count > 0) + { + sb.Append(",\"props\":{"); + bool first = true; + foreach (var kv in log.Properties.ToArray()) + { + if (!first) + { + sb.Append(','); + } + + first = false; + sb.Append('"'); + JsonEscape(kv.Key, sb); + sb.Append("\":\""); + JsonEscape(kv.Value != null ? kv.Value.ToString() : "null", sb); + sb.Append('"'); + } + sb.Append('}'); + } + + sb.Append('}'); + return sb.ToString(); + } + + private static void JsonEscape(string value, StringBuilder sb) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + foreach (char c in value) + { + switch (c) + { + case '"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + default: + if (char.IsControl(c)) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("x4")); + } + else + { + sb.Append(c); + } + + break; + } + } + } + + private static string LevelString(LogLevel level) + { + switch (level) + { + case LogLevel.Trace: return "TRACE"; + case LogLevel.Debug: return "DEBUG"; + case LogLevel.Information: return "INFO"; + case LogLevel.Warning: return "WARN"; + case LogLevel.Error: return "ERROR"; + case LogLevel.Critical: return "CRITICAL"; + default: return level.ToString().ToUpperInvariant(); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SlackFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SlackFlow.cs new file mode 100644 index 0000000..a23b20e --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SlackFlow.cs @@ -0,0 +1,176 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + /// + /// logging flow that sends messages to a Slack channel via webhook. + /// + public sealed class SlackFlow : FlowBase, IAsyncDisposable + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _workerTask; + private readonly CancellationTokenSource _cts; + private readonly HttpClient _httpClient; + private readonly string _webhookUrl; + + public SlackFlow( + string webhookUrl, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Information) + : base("Slack", minimumLevel) + { + _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl)); + _httpClient = new HttpClient(); + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _workerTask = Task.Run(() => ProcessQueueAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + private async Task ProcessQueueAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"SlackFlow error: {ex.Message}"); + } + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var payload = new + { + text = BuildMessage(logEvent) + }; + + var json = JsonHelper.ToJson(payload); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(_webhookUrl, content, cancellationToken); + } + } + + private static string BuildMessage(LogEvent logEvent) + { + var sb = new StringBuilder(); + sb.AppendLine($"*Level:* {logEvent.Level}"); + if (!string.IsNullOrWhiteSpace(logEvent.Category)) + { + sb.AppendLine($"*Category:* {logEvent.Category}"); + } + + sb.AppendLine($"*Timestamp:* {LogEvent.GetDateTime(logEvent.Timestamp):yyyy-MM-dd HH:mm:ss.fff}"); + sb.AppendLine($"*Message:* {logEvent.Message}"); + + if (logEvent.Exception != null) + { + sb.AppendLine("*Exception:*"); + sb.AppendLine($"```{logEvent.Exception.GetType().FullName}: {logEvent.Exception.Message}\n{logEvent.Exception.StackTrace}```"); + } + + if (logEvent.Properties.Count > 0) + { + sb.AppendLine("*Properties:*"); + foreach (var prop in logEvent.Properties) + { + sb.AppendLine($"`{prop.Key}` = `{prop.Value?.ToString() ?? "null"}`"); + } + } + + return sb.ToString(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SnmpTrapFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SnmpTrapFlow.cs new file mode 100644 index 0000000..d6dcea3 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SnmpTrapFlow.cs @@ -0,0 +1,69 @@ +using EonaCat.LogStack.Core; +using System; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + public class SnmpTrapFlow : FlowBase + { + private readonly string _host; + private readonly int _port; + private readonly string _oid; + private readonly UdpClient _udpClient; + + public SnmpTrapFlow(string host, int port = 162, string oid = "1.3.6.1.4.1.99999.1337.1.1.1", LogLevel minimumLevel = LogLevel.Trace) : base($"SNMP:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _oid = oid; + _udpClient = new UdpClient(); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + var snmpTrapMessage = FormatSnmpTrapMessage(logEvent); + var data = Encoding.ASCII.GetBytes(snmpTrapMessage); + + try + { + _udpClient.Send(data, data.Length, _host, _port); + return Task.FromResult(WriteResult.Success); + } + catch + { + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + } + + private string FormatSnmpTrapMessage(LogEvent logEvent) + { + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"Trap OID: {_oid}"); + stringBuilder.Append(" Timestamp: ").Append(logEvent.Timestamp); + stringBuilder.Append(" Level: ").Append(logEvent.Level); + stringBuilder.Append(" Message: ").Append(logEvent.Message); + return stringBuilder.ToString(); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + // SNMP traps are sent immediately, so no flushing needed. + return Task.CompletedTask; + } + + public override ValueTask DisposeAsync() + { + _udpClient?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SplunkFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SplunkFlow.cs new file mode 100644 index 0000000..ef86426 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SplunkFlow.cs @@ -0,0 +1,203 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.Flows +{ + public sealed class SplunkFlow : FlowBase + { + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _splunkUrl; + private readonly string _token; + private readonly string _sourcetype; + private readonly string _hostName; + + private readonly BackpressureStrategy _backpressureStrategy; + private readonly HttpClient _httpClient; + + public SplunkFlow( + string splunkUrl, + string token, + string sourcetype = "splunk_logs", + string hostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest) + : base($"Splunk:{splunkUrl}", minimumLevel) + { + _splunkUrl = splunkUrl ?? throw new ArgumentNullException(nameof(splunkUrl)); + _token = token ?? throw new ArgumentNullException(nameof(token)); + _sourcetype = sourcetype; + _hostName = hostName ?? Environment.MachineName; + _backpressureStrategy = backpressureStrategy; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy switch + { + BackpressureStrategy.Wait => BoundedChannelFullMode.Wait, + BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite, + BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest, + _ => BoundedChannelFullMode.Wait + }, + SingleReader = true, + SingleWriter = false + }; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Splunk {_token}"); + + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"SplunkFlow error: {ex.Message}"); + await Task.Delay(1000, cancellationToken); + } + } + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var splunkEvent = new + { + time = ToUnixTimeSeconds(LogEvent.GetDateTime(logEvent.Timestamp)), + host = _hostName, + sourcetype = _sourcetype, + @event = new + { + level = logEvent.Level.ToString(), + category = logEvent.Category, + message = logEvent.Message + } + }; + + string json = JsonHelper.ToJson(splunkEvent); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + try + { + await _httpClient.PostAsync(_splunkUrl, content, cancellationToken); + } + catch + { + // ignore errors + } + } + } + + // Helper to convert DateTime to Unix timestamp (works in .NET 4.8.x) + private static double ToUnixTimeSeconds(DateTime dt) + { + var utc = dt.ToUniversalTime(); + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + return (utc - epoch).TotalSeconds; + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs new file mode 100644 index 0000000..8d44b09 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs @@ -0,0 +1,320 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ServiceMonitoring +{ + public enum ServiceType + { + TCP = 0, + UDP, + HTTP, + HTTPS, + File + } + + public class ServiceStatus + { + public string ServiceName { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string Status { get; set; } + public DateTime LastChecked { get; set; } + public ServiceType ServiceType { get; set; } + public string AdditionalInfo { get; set; } + } + + public sealed class StatusFlow : FlowBase + { + private readonly List _servicesToMonitor; + private readonly TimeSpan _checkInterval; + private readonly string _statusDirectory; + private readonly CancellationTokenSource _cts; + private readonly Action _statusChangeTrigger; + + /// + /// Log fileSize (default: 10 MB) + /// + public int MaxLogFileSize { get; set; } = 10 * 1024 * 1024; // 10 MB + + /// + /// Max Log files (default: 5) + /// + public int MaxLogFiles { get; set; } = 5; + + /// + /// Default interval in minutes (default: 5) + /// + public int DefaultIntervalCheckInMinutes { get; set; } = 5; + + public StatusFlow( + List servicesToMonitor, + TimeSpan? checkInterval, + string statusDirectory, + Action statusChangeTrigger, + LogLevel minimumLevel = LogLevel.Trace + ) : base("StatusFlow", minimumLevel) + { + _servicesToMonitor = servicesToMonitor; + + if (checkInterval == null) + { + checkInterval = TimeSpan.FromMinutes(DefaultIntervalCheckInMinutes); + } + + _checkInterval = checkInterval.Value; + _statusChangeTrigger = statusChangeTrigger; + + if (string.IsNullOrWhiteSpace(statusDirectory)) + { + statusDirectory = "./logs/status"; + } + + // Resolve relative path + if (statusDirectory.StartsWith("./", StringComparison.Ordinal)) + { + statusDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, statusDirectory.Substring(2)); + } + + Directory.CreateDirectory(statusDirectory); + _statusDirectory = statusDirectory; + + _cts = new CancellationTokenSource(); + StartMonitoring(); + } + + public void StartMonitoring() + { + Task.Run(async () => + { + while (IsEnabled && !_cts.Token.IsCancellationRequested) + { + await MonitorServicesAsync(); + await Task.Delay(_checkInterval, _cts.Token); + } + }); + } + + private async Task MonitorServicesAsync() + { + foreach (var service in _servicesToMonitor) + { + bool isServiceAvailable = service.ServiceType switch + { + ServiceType.TCP => await IsTcpServiceAvailableAsync(service.Host, service.Port), + ServiceType.UDP => await IsUdpServiceAvailableAsync(service.Host, service.Port), + ServiceType.HTTP => await IsHttpServiceAvailableAsync(service.Host), + ServiceType.HTTPS => await IsHttpsServiceAvailableAsync(service), + ServiceType.File => await IsFileAvailableAsync(service.Host), + _ => false + }; + + if (isServiceAvailable != (service.Status == "Available")) + { + service.Status = isServiceAvailable ? "Available" : "Unavailable"; + service.LastChecked = DateTime.UtcNow; + + // Trigger action when service status changes + _statusChangeTrigger?.Invoke(service); + + // Log the status + LogServiceStatusToFile(service); + } + } + } + + public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref DroppedCount); + return WriteResult.NoBlastZone; + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref DroppedCount); + return WriteResult.NoBlastZone; + } + + private async Task IsTcpServiceAvailableAsync(string host, int port) + { + try + { + using (var tcpClient = new TcpClient()) + { + await tcpClient.ConnectAsync(host, port); + return true; + } + } + catch + { + return false; + } + } + + private async Task IsUdpServiceAvailableAsync(string host, int port) + { + try + { + using (var udpClient = new UdpClient()) + { + var timeout = TimeSpan.FromSeconds(5); + + var message = Encoding.ASCII.GetBytes("ping"); + var sendTask = udpClient.SendAsync(message, message.Length, host, port); + + var completedTask = await Task.WhenAny(sendTask, Task.Delay(timeout)); + if (completedTask == sendTask) + { + var receiveTask = udpClient.ReceiveAsync(); + var completedReceiveTask = await Task.WhenAny(receiveTask, Task.Delay(timeout)); + return completedReceiveTask == receiveTask; + } + return false; + } + } + catch + { + return false; + } + } + + private async Task IsHttpServiceAvailableAsync(string host) + { + try + { + var request = (HttpWebRequest)WebRequest.Create($"http://{host}"); + request.Method = "HEAD"; + using (var response = await request.GetResponseAsync()) + { + return true; + } + } + catch + { + return false; + } + } + + public async Task IsHttpsServiceAvailableAsync(ServiceStatus service) + { + try + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None) + { + service.AdditionalInfo = $"Certificate Subject: {cert.Subject}\n" + + $"Certificate Issuer: {cert.Issuer}\n" + + $"Certificate Expiry: {cert.GetExpirationDateString()}\n" + + $"SSL Policy Errors: {sslPolicyErrors}\n"; + + foreach (var chainElement in chain.ChainElements) + { + service.AdditionalInfo += $"Chain Element: {chainElement.Certificate.Subject}, {chainElement.Certificate.Issuer}\n"; + } + } + return sslPolicyErrors == System.Net.Security.SslPolicyErrors.None; + } + }; + + using (var client = new HttpClient(handler)) + { + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, $"https://{service.Host}")); + return response.IsSuccessStatusCode; + } + } + catch (Exception ex) + { + service.AdditionalInfo = $"Error: {ex.Message}"; + return false; + } + } + + private async Task IsFileAvailableAsync(string filePath) + { + try + { + return File.Exists(filePath); + } + catch + { + return false; + } + } + + private void LogServiceStatusToFile(ServiceStatus service) + { + string statusMessage = $"{service.ServiceName} ({service.Host}:{service.Port}) - Status: {service.Status}, Last Checked: {service.LastChecked}"; + + if (!string.IsNullOrEmpty(service.AdditionalInfo)) + { + statusMessage += $"\nAdditional Info: {service.AdditionalInfo}"; + } + + RollOverLogFileIfNeeded(); + + try + { + string logFilePath = Path.Combine(_statusDirectory, "status_log.txt"); + using (var writer = new StreamWriter(logFilePath, append: true)) + { + writer.WriteLine(statusMessage); + } + } + catch (Exception ex) + { + Console.WriteLine($"StatusFlow: Error writing to file: {ex.Message}"); + } + } + + private void RollOverLogFileIfNeeded() + { + try + { + string logFilePath = Path.Combine(_statusDirectory, "status_log.txt"); + + if (File.Exists(logFilePath) && new FileInfo(logFilePath).Length > MaxLogFileSize) + { + var logFiles = Directory.GetFiles(_statusDirectory, "status_log_*.txt").OrderBy(f => f).ToList(); + + if (logFiles.Count >= MaxLogFiles) + { + File.Delete(logFiles[0]); + logFiles.RemoveAt(0); + } + + string newLogFilePath = Path.Combine(_statusDirectory, $"status_log_{DateTime.UtcNow:yyyyMMdd_HHmmss}.txt"); + File.Move(logFilePath, newLogFilePath); + } + } + catch (Exception ex) + { + Console.WriteLine($"StatusFlow: Error handling log file rollover: {ex.Message}"); + } + } + + public override async ValueTask DisposeAsync() + { + _cts.Cancel(); + await base.DisposeAsync(); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogTcpFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogTcpFlow.cs new file mode 100644 index 0000000..6e8c766 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogTcpFlow.cs @@ -0,0 +1,270 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class SyslogTcpFlow : FlowBase + { + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private TcpClient? _tcpClient; + private Stream _stream; + private readonly bool _useTls; + private readonly RemoteCertificateValidationCallback _certValidationCallback; + private readonly X509CertificateCollection _clientCertificates; + private readonly BackpressureStrategy _backpressureStrategy; + + public SyslogTcpFlow( + string host, + int port = 514, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + : base($"SyslogTCP:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _backpressureStrategy = backpressureStrategy; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + + _useTls = useTls; + _certValidationCallback = certValidationCallback ?? DefaultCertificateValidation; + _clientCertificates = clientCertificates; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy == BackpressureStrategy.Wait + ? BoundedChannelFullMode.Wait + : backpressureStrategy == BackpressureStrategy.DropNewest + ? BoundedChannelFullMode.DropWrite + : BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + var sb = new StringBuilder(8192); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await EnsureConnectedAsync(cancellationToken); + + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, sb, cancellationToken); + batch.Clear(); + sb.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"SyslogTcpFlow error: {ex.Message}"); + await Task.Delay(1000, cancellationToken); + _tcpClient?.Dispose(); + _tcpClient = null; + } + } + } + + private async Task EnsureConnectedAsync(CancellationToken cancellationToken) + { + if (_tcpClient != null && _tcpClient.Connected) + { + return; + } + + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + + if (_tcpClient != null) + { + _tcpClient.Dispose(); + _tcpClient = null; + } + + _tcpClient = new TcpClient(); + _tcpClient.NoDelay = true; + + await _tcpClient.ConnectAsync(_host, _port).ConfigureAwait(false); + + var networkStream = _tcpClient.GetStream(); + + if (_useTls) + { + var sslStream = new SslStream( + networkStream, + false, + _certValidationCallback); + + sslStream.AuthenticateAsClient( + _host, + _clientCertificates, + SslProtocols.Tls12, + checkCertificateRevocation: true); + + _stream = sslStream; + } + else + { + _stream = networkStream; + } + } + + private static bool DefaultCertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None; + } + + private async Task SendBatchAsync(List batch, StringBuilder sb, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + FormatSyslogEvent(logEvent, sb); + sb.AppendLine(); + } + + if (_stream != null) + { + var data = Encoding.UTF8.GetBytes(sb.ToString()); + + await _stream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FormatSyslogEvent(LogEvent logEvent, StringBuilder sb) + { + // Simple RFC 3164-style format: timestamp hostname tag: message + // Here we use facility=1 (user-level messages) and map severity from log level + int severity = logEvent.Level switch + { + LogLevel.Trace => 7, + LogLevel.Debug => 7, + LogLevel.Information => 6, + LogLevel.Warning => 4, + LogLevel.Error => 3, + LogLevel.Critical => 2, + _ => 6 + }; + int facility = 1; // user-level messages + int pri = facility * 8 + severity; + + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + sb.Append('<').Append(pri).Append('>'); + sb.Append(dt.ToString("MMM dd HH:mm:ss")); // RFC 3164 timestamp + sb.Append(" ").Append(Environment.MachineName); + sb.Append(" ").Append(string.IsNullOrEmpty(logEvent.Category) ? "SyslogTcpFlow" : logEvent.Category); + sb.Append(": ").Append(logEvent.Message); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogUdpFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogUdpFlow.cs new file mode 100644 index 0000000..37898ea --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/SyslogUdpFlow.cs @@ -0,0 +1,238 @@ +using EonaCat.LogStack.Core; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class SyslogUdpFlow : FlowBase + { + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + private const int MaxUdpPacketSize = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private UdpClient? _udpClient; + private readonly BackpressureStrategy _backpressureStrategy; + + public SyslogUdpFlow( + string host, + int port = 514, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest) + : base($"SyslogUDP:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _backpressureStrategy = backpressureStrategy; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy switch + { + BackpressureStrategy.Wait => BoundedChannelFullMode.Wait, + BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite, + BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest, + _ => BoundedChannelFullMode.Wait + }, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _udpClient = new UdpClient(); + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + var sb = new StringBuilder(8192); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, sb, cancellationToken); + batch.Clear(); + sb.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"SyslogUdpFlow error: {ex.Message}"); + await Task.Delay(500, cancellationToken); + } + } + } + + private async Task SendBatchAsync(List batch, StringBuilder sb, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + FormatSyslogEvent(logEvent, sb); + sb.AppendLine(); + } + + if (_udpClient == null) + { + return; + } + + var data = Encoding.UTF8.GetBytes(sb.ToString()); + + if (data.Length <= MaxUdpPacketSize) + { + try + { + await _udpClient.SendAsync(data, data.Length, _host, _port); + } + catch + { + // UDP send errors are ignored + } + } + else + { + await SendUdpInChunksAsync(data, MaxUdpPacketSize, cancellationToken); + } + } + + private async Task SendUdpInChunksAsync(byte[] data, int chunkSize, CancellationToken cancellationToken) + { + int offset = 0; + byte[] buffer = ArrayPool.Shared.Rent(chunkSize); + + try + { + while (offset < data.Length) + { + int size = Math.Min(chunkSize, data.Length - offset); + Buffer.BlockCopy(data, offset, buffer, 0, size); + await _udpClient.SendAsync(buffer, size, _host, _port); + offset += size; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FormatSyslogEvent(LogEvent logEvent, StringBuilder sb) + { + int severity = logEvent.Level switch + { + LogLevel.Trace => 7, + LogLevel.Debug => 7, + LogLevel.Information => 6, + LogLevel.Warning => 4, + LogLevel.Error => 3, + LogLevel.Critical => 2, + _ => 6 + }; + int facility = 1; + int pri = facility * 8 + severity; + + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + sb.Append('<').Append(pri).Append('>'); + sb.Append(dt.ToString("MMM dd HH:mm:ss")); + sb.Append(" ").Append(Environment.MachineName); + sb.Append(" ").Append(string.IsNullOrEmpty(logEvent.Category) ? "SyslogUdpFlow" : logEvent.Category); + sb.Append(": ").Append(logEvent.Message); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _udpClient?.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/TcpFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/TcpFlow.cs new file mode 100644 index 0000000..4b588e0 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/TcpFlow.cs @@ -0,0 +1,289 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows; + +public sealed class TcpFlow : FlowBase +{ + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private TcpClient? _tcpClient; + private Stream _stream; + private readonly bool _useTls; + private readonly RemoteCertificateValidationCallback _certValidationCallback; + private readonly X509CertificateCollection _clientCertificates; + private readonly BackpressureStrategy _backpressureStrategy; + + public TcpFlow( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + : base($"TCP:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _backpressureStrategy = backpressureStrategy; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + + _useTls = useTls; + _certValidationCallback = certValidationCallback ?? DefaultCertificateValidation; + _clientCertificates = clientCertificates; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy == BackpressureStrategy.Wait + ? BoundedChannelFullMode.Wait + : backpressureStrategy == BackpressureStrategy.DropNewest + ? BoundedChannelFullMode.DropWrite + : BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public async Task SendFileAsync(string filePath, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) + { + return WriteResult.Failed; + } + + try + { + // Ensure TCP connection + await EnsureConnectedAsync(cancellationToken); + + // Send file in chunks + using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + { + await _stream.WriteAsync(buffer, 0, bytesRead, cancellationToken); + await _stream.FlushAsync(cancellationToken); + } + } + + return WriteResult.Success; + } + catch (Exception exception) + { + Console.Error.WriteLine($"TcpFlow error: Error while sending file: {exception.Message}"); + return WriteResult.Failed; + } + } + + private static bool DefaultCertificateValidation( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None; + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + var sb = new StringBuilder(8192); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await EnsureConnectedAsync(cancellationToken); + + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, sb, cancellationToken); + batch.Clear(); + sb.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"TcpFlow error: {ex.Message}"); + await Task.Delay(1000, cancellationToken); // Retry after delay + _tcpClient?.Dispose(); + _tcpClient = null; + } + } + } + + private async Task EnsureConnectedAsync(CancellationToken cancellationToken) + { + if (_tcpClient != null && _tcpClient.Connected) + { + return; + } + + _stream?.Dispose(); + _tcpClient?.Dispose(); + _tcpClient = null; + + _tcpClient = new TcpClient { NoDelay = true }; // lower latency + await _tcpClient.ConnectAsync(_host, _port).ConfigureAwait(false); + + var networkStream = _tcpClient.GetStream(); + + if (_useTls) + { + var sslStream = new SslStream( + networkStream, + false, + _certValidationCallback); + + await sslStream.AuthenticateAsClientAsync( + _host, + _clientCertificates, + SslProtocols.Tls12, + checkCertificateRevocation: true).ConfigureAwait(false); + + _stream = sslStream; + } + else + { + _stream = networkStream; + } + } + + private async Task SendBatchAsync(List batch, StringBuilder sb, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + FormatLogEvent(logEvent, sb); + sb.AppendLine(); + } + + if (_stream != null) + { + var data = Encoding.UTF8.GetBytes(sb.ToString()); + await _stream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FormatLogEvent(LogEvent logEvent, StringBuilder sb) + { + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss.fff")); + sb.Append(" ["); + sb.Append(logEvent.Level.ToString().ToUpperInvariant()); + sb.Append("] "); + if (!string.IsNullOrEmpty(logEvent.Category)) + { + sb.Append(logEvent.Category); + sb.Append(": "); + } + sb.Append(logEvent.Message); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/TelegramFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/TelegramFlow.cs new file mode 100644 index 0000000..e298664 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/TelegramFlow.cs @@ -0,0 +1,183 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// logging flow that sends messages to a Telegram chat via a bot. + /// + public sealed class TelegramFlow : FlowBase, IAsyncDisposable + { + private const int ChannelCapacity = 4096; + private readonly int _batchSize; + + private readonly Channel _channel; + private readonly Task _workerTask; + private readonly CancellationTokenSource _cts; + private readonly HttpClient _httpClient; + private readonly string _botToken; + private readonly string _chatId; + + public TelegramFlow( + string botToken, + string chatId, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Information) + : base("Telegram", minimumLevel) + { + _botToken = botToken ?? throw new ArgumentNullException(nameof(botToken)); + _chatId = chatId ?? throw new ArgumentNullException(nameof(chatId)); + _httpClient = new HttpClient(); + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _workerTask = Task.Run(() => ProcessQueueAsync(_cts.Token)); + } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + private async Task ProcessQueueAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + try + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + { + while (_channel.Reader.TryRead(out var logEvent)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"TelegramFlow error: {ex.Message}"); + } + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + var message = BuildMessage(logEvent); + + var url = $"https://api.telegram.org/bot{_botToken}/sendMessage"; + + var payload = new + { + chat_id = _chatId, + text = message, + parse_mode = "Markdown" + }; + + var json = JsonHelper.ToJson(payload); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(url, content, cancellationToken); + } + } + + private static string BuildMessage(LogEvent logEvent) + { + var sb = new StringBuilder(); + sb.Append($"*{logEvent.Level}* | {logEvent.Category}\n"); + sb.Append($"`{LogEvent.GetDateTime(logEvent.Timestamp):yyyy-MM-dd HH:mm:ss.fff}`\n"); + sb.Append(logEvent.Message); + + if (logEvent.Exception != null) + { + sb.Append($"\n*Exception:* `{logEvent.Exception.GetType().FullName}`\n"); + sb.Append($"`{logEvent.Exception.Message}`\n"); + } + + if (logEvent.Properties.Count > 0) + { + sb.Append("\n*Properties:*"); + foreach (var prop in logEvent.Properties) + { + sb.Append($"\n`{prop.Key}` = `{prop.Value?.ToString() ?? "null"}`"); + } + } + + return sb.ToString(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try + { + await _workerTask.ConfigureAwait(false); + } + catch { } + + _httpClient.Dispose(); + _cts.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/ThrottledFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ThrottledFlow.cs new file mode 100644 index 0000000..09f99a7 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ThrottledFlow.cs @@ -0,0 +1,303 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// A decorator flow that applies per-level rate limiting (token bucket) to any + /// inner flow. Prevents log storms from overwhelming downstream sinks (e.g. Slack, + /// HTTP, email) while ensuring that at least one event of each pattern gets through. + /// + /// Also supports deduplication: identical messages within a window are collapsed + /// into a single entry with a repeat-count. + /// + public sealed class ThrottledFlow : FlowBase + { + private sealed class Bucket + { + public double Tokens; + public DateTime LastRefill; + public readonly double Capacity; + public readonly double RefillPerSecond; + + public Bucket(double capacity, double refillPerSecond) + { + Capacity = capacity; + RefillPerSecond = refillPerSecond; + Tokens = capacity; + LastRefill = DateTime.UtcNow; + } + + /// Returns true and consumes a token if available. + public bool TryConsume() + { + DateTime now = DateTime.UtcNow; + double elapsed = (now - LastRefill).TotalSeconds; + Tokens = Math.Min(Capacity, Tokens + elapsed * RefillPerSecond); + LastRefill = now; + + if (Tokens >= 1.0) { Tokens -= 1.0; return true; } + return false; + } + } + + private sealed class DedupEntry + { + public int Count; + public DateTime FirstSeen; + public LogEvent LastEvent; + } + + private readonly IFlow _inner; + private readonly int _burstCapacity; + private readonly double _refillPerSecond; + private readonly bool _deduplicate; + private readonly TimeSpan _dedupWindow; + private readonly int _dedupMaxKeys; + + private readonly Dictionary _buckets + = new Dictionary(); + private readonly Dictionary _dedupMap + = new Dictionary(StringComparer.Ordinal); + private readonly object _lock = new object(); + private long _throttledCount; + + /// The downstream flow to protect. + /// + /// Max events that can be emitted in a burst per level (token bucket capacity). + /// + /// + /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. + /// + /// + /// If true, identical messages within are collapsed. + /// The suppressed count is appended to the message when the window expires. + /// + /// Deduplication window (default 60 s). + /// Maximum number of distinct messages tracked (default 1000). + /// Minimum level this flow processes. + public ThrottledFlow( + IFlow inner, + int burstCapacity = 10, + double refillPerSecond = 1.0, + bool deduplicate = false, + TimeSpan dedupWindow = default(TimeSpan), + int dedupMaxKeys = 1000, + LogLevel minimumLevel = LogLevel.Trace) + : base("Throttled:" + (inner != null ? inner.GetType().Name : "null"), minimumLevel) + { + if (inner == null) + { + throw new ArgumentNullException("inner"); + } + + _inner = inner; + _burstCapacity = burstCapacity < 1 ? 1 : burstCapacity; + _refillPerSecond = refillPerSecond <= 0 ? 1.0 : refillPerSecond; + _deduplicate = deduplicate; + _dedupWindow = dedupWindow == default(TimeSpan) ? TimeSpan.FromSeconds(60) : dedupWindow; + _dedupMaxKeys = dedupMaxKeys < 1 ? 1 : dedupMaxKeys; + + // Pre-create buckets for all defined levels + foreach (LogLevel level in Enum.GetValues(typeof(LogLevel))) + { + _buckets[level] = new Bucket(_burstCapacity, _refillPerSecond); + } + } + + + /// Events throttled (dropped by rate limit or dedup) so far. + public long ThrottledCount { get { return Interlocked.Read(ref _throttledCount); } } + + public override async Task BlastAsync( + LogEvent logEvent, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return WriteResult.LevelFiltered; + } + + lock (_lock) + { + // deduplication pass + if (_deduplicate) + { + string key = MakeDedupKey(logEvent); + DedupEntry entry; + + // Flush expired entries to avoid unbounded growth + if (_dedupMap.Count >= _dedupMaxKeys) + { + PurgeExpiredDedupEntries(); + } + + if (_dedupMap.TryGetValue(key, out entry)) + { + TimeSpan age = DateTime.UtcNow - entry.FirstSeen; + if (age < _dedupWindow) + { + entry.Count++; + entry.LastEvent = logEvent; + Interlocked.Increment(ref _throttledCount); + return WriteResult.Dropped; + } + else + { + // Window expired: flush the suppressed count as a synthetic event + if (entry.Count > 1) + { + FlushDedupEntry(key, entry); + } + + _dedupMap.Remove(key); + } + } + + // First occurrence + _dedupMap[key] = new DedupEntry + { + Count = 1, + FirstSeen = DateTime.UtcNow, + LastEvent = logEvent + }; + } + + // token bucket pass + Bucket bucket; + if (!_buckets.TryGetValue(logEvent.Level, out bucket)) + { + bucket = new Bucket(_burstCapacity, _refillPerSecond); + _buckets[logEvent.Level] = bucket; + } + + if (!bucket.TryConsume()) + { + Interlocked.Increment(ref _throttledCount); + Interlocked.Increment(ref DroppedCount); + return WriteResult.Dropped; + } + } + + WriteResult result = await _inner.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + Interlocked.Increment(ref BlastedCount); + return result; + } + + public override async Task BlastBatchAsync( + ReadOnlyMemory logEvents, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + WriteResult result = WriteResult.Success; + foreach (LogEvent e in logEvents.ToArray()) + { + WriteResult r = await BlastAsync(e, cancellationToken).ConfigureAwait(false); + if (r == WriteResult.Dropped) + { + result = WriteResult.Dropped; + } + } + return result; + } + + public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + // Flush all pending dedup entries + lock (_lock) + { + List keys = new List(_dedupMap.Keys); + foreach (string key in keys) + { + DedupEntry entry; + if (_dedupMap.TryGetValue(key, out entry) && entry.Count > 1) + { + FlushDedupEntry(key, entry); + } + + _dedupMap.Remove(key); + } + } + return _inner.FlushAsync(cancellationToken); + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + await FlushAsync().ConfigureAwait(false); + await base.DisposeAsync().ConfigureAwait(false); + } + + private static string MakeDedupKey(LogEvent log) + { + // Key = level + category + first 200 chars of message (ignore dynamic parts like timestamps) + string msg = log.Message.Length > 0 ? log.Message.ToString() : string.Empty; + if (msg.Length > 200) + { + msg = msg.Substring(0, 200); + } + + return log.Level + "|" + (log.Category ?? string.Empty) + "|" + msg; + } + + private void FlushDedupEntry(string key, DedupEntry entry) + { + // Build a synthetic event that summarises the suppressed repeats + string original = entry.LastEvent.Message.Length > 0 + ? entry.LastEvent.Message.ToString() + : string.Empty; + + string summary = original + " [repeated " + (entry.Count - 1) + " more times in " + + (int)_dedupWindow.TotalSeconds + "s window]"; + + LogEvent synth = new LogEvent + { + Level = entry.LastEvent.Level, + Category = entry.LastEvent.Category, + Timestamp = entry.LastEvent.Timestamp, + Message = new StringSegment(summary), + Exception = entry.LastEvent.Exception + }; + + try { _inner.BlastAsync(synth).GetAwaiter().GetResult(); } + catch { /* best-effort */ } + } + + private void PurgeExpiredDedupEntries() + { + List expired = new List(); + DateTime cutoff = DateTime.UtcNow - _dedupWindow; + + foreach (KeyValuePair kv in _dedupMap) + { + if (kv.Value.FirstSeen < cutoff) + { + expired.Add(kv.Key); + } + } + + foreach (string k in expired) + { + DedupEntry entry; + if (_dedupMap.TryGetValue(k, out entry) && entry.Count > 1) + { + FlushDedupEntry(k, entry); + } + + _dedupMap.Remove(k); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/UdpFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/UdpFlow.cs new file mode 100644 index 0000000..b8dbdc6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/UdpFlow.cs @@ -0,0 +1,207 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class UdpFlow : FlowBase +{ + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private readonly BackpressureStrategy _backpressureStrategy; + private readonly TimeSpan _flushInterval; + private readonly Task _flushTask; + + public UdpFlow( + string host, + int port, + int flushIntervalInMilliseconds = 2000, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest) + : base($"UDP:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _backpressureStrategy = backpressureStrategy; + _flushInterval = TimeSpan.FromMilliseconds(flushIntervalInMilliseconds); + + _batchSize = batchSize <= 0 ? 1 : batchSize; + + _udpClient = new UdpClient(); + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy switch + { + BackpressureStrategy.Wait => BoundedChannelFullMode.Wait, + BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite, + BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest, + _ => BoundedChannelFullMode.Wait + }, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + + if (flushIntervalInMilliseconds > 0) + { + _flushTask = Task.Run(() => PeriodicFlushAsync(_cts.Token)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + var sb = new StringBuilder(8192); + + try + { + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, sb, cancellationToken); + batch.Clear(); + sb.Clear(); + } + } + + if (batch.Count > 0) + { + await SendBatchAsync(batch, sb, CancellationToken.None); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"UdpFlow error: {ex.Message}"); + } + } + + private async Task SendBatchAsync(List batch, StringBuilder sb, CancellationToken cancellationToken) + { + foreach (var logEvent in batch) + { + FormatLogEvent(logEvent, sb); + sb.AppendLine(); + } + + var data = Encoding.UTF8.GetBytes(sb.ToString()); + await _udpClient.SendAsync(data, data.Length, _host, _port); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FormatLogEvent(LogEvent logEvent, StringBuilder sb) + { + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss.fff")); + sb.Append(" ["); + sb.Append(logEvent.Level.ToString().ToUpperInvariant()); + sb.Append("] "); + if (!string.IsNullOrEmpty(logEvent.Category)) + { + sb.Append(logEvent.Category); + sb.Append(": "); + } + sb.Append(logEvent.Message); + } + + private async Task PeriodicFlushAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + await Task.Delay(_flushInterval, token); + } + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _udpClient.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/WebhookFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/WebhookFlow.cs new file mode 100644 index 0000000..3b3c536 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/WebhookFlow.cs @@ -0,0 +1,85 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class WebhookFlow : FlowBase + { + private readonly string _webhookUrl; + private readonly HttpClient _httpClient; + private readonly int _maxRetries; + private readonly TimeSpan _retryDelay; + + public WebhookFlow(string webhookUrl, LogLevel minimumLevel = LogLevel.Trace, int maxRetries = 3, TimeSpan? retryDelay = null) : base($"Webhook:{webhookUrl}", minimumLevel) + { + _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl)); + _httpClient = new HttpClient(); + _maxRetries = maxRetries; + _retryDelay = retryDelay ?? TimeSpan.FromSeconds(1); + } + + public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return WriteResult.LevelFiltered; + } + + var logPayload = new + { + Timestamp = LogEvent.GetDateTime(logEvent.Timestamp), + Level = logEvent.Level.ToString(), + Message = logEvent.Message, + Category = logEvent.Category, + LogEvent = logEvent + }; + + var jsonPayload = JsonHelper.ToJson(logPayload); + var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + int attempt = 0; + while (attempt < _maxRetries) + { + try + { + var response = await _httpClient.PostAsync(_webhookUrl, content, cancellationToken); + + if (response.IsSuccessStatusCode) + { + return WriteResult.Success; + } + + attempt++; + await Task.Delay(_retryDelay, cancellationToken); + } + catch (Exception ex) + { + Console.Error.WriteLine($"WebhookFlow error: {ex.Message}"); + attempt++; + await Task.Delay(_retryDelay, cancellationToken); + } + } + + return WriteResult.Dropped; + } + + public override async ValueTask DisposeAsync() + { + _httpClient.Dispose(); + await base.DisposeAsync(); + } + + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/ZabbixFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ZabbixFlow.cs new file mode 100644 index 0000000..447f144 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ZabbixFlow.cs @@ -0,0 +1,235 @@ +using EonaCat.Json; +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class ZabbixFlow : FlowBase + { + private readonly int _batchSize; + private const int ChannelCapacity = 4096; + + private readonly Channel _channel; + private readonly Task _senderTask; + private readonly CancellationTokenSource _cts; + + private readonly string _host; + private readonly int _port; + private TcpClient? _tcpClient; + private NetworkStream? _stream; + private readonly BackpressureStrategy _backpressureStrategy; + private readonly string _zabbixHostName; + private readonly string _zabbixKey; + + public ZabbixFlow( + string host, + int port = 10051, + string zabbixHostName = null, + string zabbixKey = "log_event", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest) + : base($"Zabbix:{host}:{port}", minimumLevel) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _backpressureStrategy = backpressureStrategy; + _zabbixHostName = zabbixHostName ?? Environment.MachineName; + _zabbixKey = zabbixKey ?? "log_event"; + + var channelOptions = new BoundedChannelOptions(ChannelCapacity) + { + FullMode = backpressureStrategy switch + { + BackpressureStrategy.Wait => BoundedChannelFullMode.Wait, + BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite, + BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest, + _ => BoundedChannelFullMode.Wait + }, + SingleReader = true, + SingleWriter = false + }; + + _batchSize = batchSize <= 0 ? 1 : batchSize; + _channel = Channel.CreateBounded(channelOptions); + _cts = new CancellationTokenSource(); + _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) + { + if (!IsEnabled || !IsLogLevelEnabled(logEvent)) + { + return Task.FromResult(WriteResult.LevelFiltered); + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + return Task.FromResult(WriteResult.Success); + } + + Interlocked.Increment(ref DroppedCount); + return Task.FromResult(WriteResult.Dropped); + } + + public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + if (!IsEnabled) + { + return WriteResult.FlowDisabled; + } + + var result = WriteResult.Success; + foreach (var logEvent in logEvents.Span) + { + if (!IsLogLevelEnabled(logEvent)) + { + continue; + } + + if (_channel.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref BlastedCount); + } + else + { + Interlocked.Increment(ref DroppedCount); + result = WriteResult.Dropped; + } + } + + return result; + } + + private async Task ProcessLogEventsAsync(CancellationToken cancellationToken) + { + var batch = new List(_batchSize); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await EnsureConnectedAsync(cancellationToken); + + await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(logEvent); + + if (batch.Count >= _batchSize || _channel.Reader.Count == 0) + { + await SendBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"ZabbixFlow error: {ex.Message}"); + await Task.Delay(1000, cancellationToken); + _tcpClient?.Dispose(); + _tcpClient = null; + } + } + } + + private async Task EnsureConnectedAsync(CancellationToken cancellationToken) + { + if (_tcpClient != null && _tcpClient.Connected) + { + return; + } + + _tcpClient?.Dispose(); + _tcpClient = new TcpClient(); + await _tcpClient.ConnectAsync(_host, _port); + _stream = _tcpClient.GetStream(); + } + + private async Task SendBatchAsync(List batch, CancellationToken cancellationToken) + { + if (_stream == null || batch.Count == 0) + { + return; + } + + foreach (var logEvent in batch) + { + var payload = new + { + request = "sender data", + data = new[] + { + new { + host = _zabbixHostName, + key = _zabbixKey, + value = FormatLogEvent(logEvent) + } + } + }; + + string json = JsonHelper.ToJson(payload); + byte[] jsonBytes = Encoding.UTF8.GetBytes(json); + + // Zabbix protocol header + byte[] header = new byte[13]; // "ZBXD\1" + 8 bytes length + header[0] = (byte)'Z'; + header[1] = (byte)'B'; + header[2] = (byte)'X'; + header[3] = (byte)'D'; + header[4] = 1; + + long length = jsonBytes.Length; + for (int i = 0; i < 8; i++) + { + header[5 + i] = (byte)(length >> (8 * i) & 0xFF); + } + + await _stream.WriteAsync(header, 0, header.Length, cancellationToken); + await _stream.WriteAsync(jsonBytes, 0, jsonBytes.Length, cancellationToken); + await _stream.FlushAsync(cancellationToken); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string FormatLogEvent(LogEvent logEvent) + { + var dt = LogEvent.GetDateTime(logEvent.Timestamp); + string ts = dt.ToString("yyyy-MM-dd HH:mm:ss.fff"); + string category = string.IsNullOrEmpty(logEvent.Category) ? "ZabbixFlow" : logEvent.Category; + return $"{ts} [{logEvent.Level}] {category}: {logEvent.Message}"; + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + _channel.Writer.Complete(); + try { await _senderTask.ConfigureAwait(false); } catch { } + } + + public override async ValueTask DisposeAsync() + { + IsEnabled = false; + _channel.Writer.Complete(); + _cts.Cancel(); + + try { await _senderTask.ConfigureAwait(false); } catch { } + + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/IBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/IBooster.cs new file mode 100644 index 0000000..f291c84 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/IBooster.cs @@ -0,0 +1,26 @@ +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Boosters enrich log events with additional context or transform them before they reach flows. +/// Boosters are designed for zero-allocation where possible. +/// +public interface IBooster +{ + /// + /// Gets the name of this booster for identification + /// + string Name { get; } + + /// + /// Boost a log event with additional data or transforms it. + /// Return false to filter out the event entirely. + /// + /// Builder to modify the log event + /// True to continue processing, false to filter out the event + bool Boost(ref LogEventBuilder builder); +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/IFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/IFlow.cs new file mode 100644 index 0000000..aa4d2ff --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/IFlow.cs @@ -0,0 +1,126 @@ +using EonaCat.LogStack.Core; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.LogStack.Flows; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Flows are output destinations for log events (replacement for "sinks"). +/// Each flow handles writing log events to a specific destination with optimized batching. +/// +public interface IFlow : IAsyncDisposable +{ + /// + /// Gets the name of this flow for identification + /// + string Name { get; } + + /// + /// Minimum log level this flow will process + /// + LogLevel MinimumLevel { get; } + + /// + /// Whether this flow is currently enabled + /// + bool IsEnabled { get; } + + /// + /// Blast a single log event to this flow + /// + Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default); + + /// + /// Blast a batch of log events to this flow (more efficient than single blasts) + /// + Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default); + + /// + /// Flush any buffered log events immediately + /// + Task FlushAsync(CancellationToken cancellationToken = default); +} + +/// +/// Base class for flows with common functionality +/// +public abstract class FlowBase : IFlow +{ + protected FlowBase(string name, LogLevel minimumLevel = LogLevel.Trace) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + MinimumLevel = minimumLevel; + IsEnabled = true; + } + + public string Name { get; } + public LogLevel MinimumLevel { get; protected set; } + public bool IsEnabled { get; protected set; } + + protected long DroppedCount; + protected long BlastedCount; + + protected bool IsLogLevelEnabled(LogEvent logEvent) + { + return logEvent.Level >= MinimumLevel; + } + + public abstract Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default); + + public virtual async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) + { + var result = WriteResult.Success; + var eventsArray = logEvents.ToArray(); + + foreach (var logEvent in eventsArray) + { + var singleResult = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + if (singleResult != WriteResult.Success) + { + result = singleResult; + } + } + + return result; + } + + public abstract Task FlushAsync(CancellationToken cancellationToken = default); + + public virtual async ValueTask DisposeAsync() + { + IsEnabled = false; + await FlushAsync(default).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + /// + /// Gets diagnostic information about this flow + /// + public virtual FlowDiagnostics GetDiagnostics() + { + return new FlowDiagnostics + { + Name = Name, + IsEnabled = IsEnabled, + MinimumLevel = MinimumLevel, + BlastedCount = Interlocked.Read(ref BlastedCount), + DroppedCount = Interlocked.Read(ref DroppedCount) + }; + } +} + +/// +/// Diagnostic information about a flow +/// +public sealed class FlowDiagnostics +{ + public string Name { get; set; } + public bool IsEnabled { get; set; } + public LogLevel MinimumLevel { get; set; } + public long BlastedCount { get; set; } + public long DroppedCount { get; set; } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/LogEvent.cs b/EonaCat.LogStack/EonaCatLoggerCore/LogEvent.cs new file mode 100644 index 0000000..8b822fe --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/LogEvent.cs @@ -0,0 +1,169 @@ +using EonaCat.LogStack.Extensions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.Core; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Represents a single log event with efficient memory management through pooling. +/// This struct is designed to minimize allocations and support high-throughput logging. +/// +public struct LogEvent +{ + public long Timestamp { get; set; } + public LogLevel Level { get; set; } + public string Category { get; set; } + public ReadOnlyMemory Message { get; set; } + public Exception? Exception { get; set; } + public Dictionary Properties { get; set; } + public string CustomData { get; set; } + public int ThreadId { get; set; } + public ActivityTraceId TraceId { get; set; } + public ActivitySpanId SpanId { get; set; } + + /// + /// Estimated memory size in bytes for backpressure calculations + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int EstimateSize() + { + // Base overhead + int size = 64; + + // Message size + size += Message.Length * 2; + + // Category size + size += (Category?.Length ?? 0) * 2; + + // Exception size (estimated) + if (Exception != null) + { + size += 512; + } + + // Properties size + size += Properties.Count * 32; + + return size; + } + + /// + /// Creates a timestamp value from DateTime + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long CreateTimestamp(DateTime dateTime) => dateTime.Ticks; + + /// + /// Converts timestamp back to DateTime + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTime GetDateTime(long timestamp) => new(timestamp); + + + public bool HasProperties => Properties != null && Properties.Count > 0; + public bool HasCustomData => CustomData != null && CustomData.Length > 0; + public bool HasException => Exception != null; + public bool HasCategory => Category != null; +} + +/// +/// Builder for creating LogEvent instances with minimal allocations +/// +public struct LogEventBuilder +{ + private long _timestamp; + private LogLevel _level; + private string? _category; + private ReadOnlyMemory _message; + private Exception? _exception; + private Dictionary? _properties; + private int _threadId; + private ActivityTraceId _traceId; + private ActivitySpanId _spanId; + + public LogEventBuilder() + { + _timestamp = DateTime.UtcNow.Ticks; + _level = LogLevel.Information; + _threadId = Environment.CurrentManagedThreadId; + + var activity = Activity.Current; + _traceId = activity?.TraceId ?? default; + _spanId = activity?.SpanId ?? default; + } + + public string? Category => _category; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithTimestamp(long timestamp) + { + _timestamp = timestamp; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithLevel(LogLevel level) + { + _level = level; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithCategory(string category) + { + _category = category; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithMessage(ReadOnlyMemory message) + { + _message = message; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithMessage(string message) + { + _message = message.AsMemory(); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithException(Exception? exception) + { + _exception = exception; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEventBuilder WithProperty(string key, object? value) + { + _properties ??= new Dictionary(4); + _properties.TryAdd(key, value); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LogEvent Build() + { + return new LogEvent + { + Timestamp = _timestamp, + Level = _level, + Category = _category ?? string.Empty, + Message = _message, + Exception = _exception, + Properties = _properties ?? new Dictionary(), + ThreadId = _threadId, + TraceId = _traceId, + SpanId = _spanId + }; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/LogStats.cs b/EonaCat.LogStack/EonaCatLoggerCore/LogStats.cs new file mode 100644 index 0000000..d0f0e03 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/LogStats.cs @@ -0,0 +1,23 @@ +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.EonaCatLogStackCore +{ + public struct LogStats + { + public long Written; + public long Dropped; + public long Rotations; + public long BytesWritten; + public double WritesPerSecond; + + public LogStats(long written, long dropped, long rotations, long bytesWritten, double writesPerSecond) + { + Written = written; + Dropped = dropped; + Rotations = rotations; + BytesWritten = bytesWritten; + WritesPerSecond = writesPerSecond; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Policies/FileRetentionPolicy.cs b/EonaCat.LogStack/EonaCatLoggerCore/Policies/FileRetentionPolicy.cs new file mode 100644 index 0000000..4f0dd70 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Policies/FileRetentionPolicy.cs @@ -0,0 +1,25 @@ +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.LogStack.EonaCatLogStackCore.Policies +{ + /// Combined retention policy: delete rolled files exceeding any threshold. + public sealed class FileRetentionPolicy + { + /// Maximum number of rolled archive files to keep (0 = unlimited). + public int MaxRolledFiles { get; set; } + + /// Maximum total size of all archives in bytes (0 = unlimited). + public long MaxTotalArchiveBytes { get; set; } + + /// Maximum age of any archive file in days (0 = unlimited). + public int MaxAgeDays { get; set; } + + public FileRetentionPolicy() + { + MaxRolledFiles = 10; + MaxTotalArchiveBytes = 0; + MaxAgeDays = 0; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Policies/SamplingPolicy.cs b/EonaCat.LogStack/EonaCatLoggerCore/Policies/SamplingPolicy.cs new file mode 100644 index 0000000..7f567d5 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Policies/SamplingPolicy.cs @@ -0,0 +1,39 @@ +using EonaCat.LogStack.Core; +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace EonaCat.LogStack.EonaCatLogStackCore.Policies +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// Log only 1-in-N events, optionally filtered by a predicate. + public sealed class SamplingPolicy + { + private long _counter; + + /// Keep 1 out of every events. + public int Rate { get; set; } + + /// Optional predicate. Null = apply to all events. + public Func Predicate { get; set; } + + public SamplingPolicy() + { + Rate = 10; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldLog(LogEvent e) + { + if (Predicate != null && !Predicate(e)) + { + // predicate not matched → always log + return true; + } + + return Interlocked.Increment(ref _counter) % Rate == 0; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLoggerCore/StringBuilderPool.cs b/EonaCat.LogStack/EonaCatLoggerCore/StringBuilderPool.cs new file mode 100644 index 0000000..1125d91 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/StringBuilderPool.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace EonaCat.LogStack.EonaCatLogStackCore +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal static class StringBuilderPool + { + private static readonly ConcurrentBag Pool = new ConcurrentBag(); + private const int InitialCapacity = 4096; + private const int MaxCapacity = 131072; // 128 KB – discard oversized builders + + public static StringBuilder Rent() + { + StringBuilder sb; + if (Pool.TryTake(out sb)) + { + sb.Clear(); + return sb; + } + return new StringBuilder(InitialCapacity); + } + + public static void Return(StringBuilder sb) + { + if (sb.Capacity <= MaxCapacity) + { + sb.Clear(); + Pool.Add(sb); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Extensions/DateTimeExtensions.cs b/EonaCat.LogStack/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..e48dab1 --- /dev/null +++ b/EonaCat.LogStack/Extensions/DateTimeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace EonaCat.LogStack.Extensions; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class DateTimeExtensions +{ + public static long ToUnixTimestamp(this DateTime dateTime) + { + return (long)(dateTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Extensions/DictionaryExtensions.cs b/EonaCat.LogStack/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..1ca4838 --- /dev/null +++ b/EonaCat.LogStack/Extensions/DictionaryExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EonaCat.LogStack.Extensions +{ + internal static class DictionaryExtensions + { + public static bool TryAdd(this IDictionary dict, TKey key, TValue value) + { + if (!dict.ContainsKey(key)) + { + dict[key] = value; + return true; + } + return false; + } + } +} diff --git a/EonaCat.LogStack/Extensions/ExceptionExtensions.cs b/EonaCat.LogStack/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..bb4339d --- /dev/null +++ b/EonaCat.LogStack/Extensions/ExceptionExtensions.cs @@ -0,0 +1,101 @@ +using EonaCat.Json; +using System; +using System.Collections; +using System.Diagnostics; + +namespace EonaCat.LogStack.Extensions; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class ExceptionExtensions +{ + public static string FormatExceptionToMessage(this Exception exception, string module = null, string method = null) + { + if (exception == null) + { + return string.Empty; + } + + var st = new StackTrace(exception, true); + var frame = st.GetFrame(0); + int fileLine = -1; + string filename = "Unknown"; + + if (frame != null) + { + fileLine = frame.GetFileLineNumber(); + filename = frame.GetFileName(); + } + + var sb = new StringBuilderChill(); + + sb.AppendLine(); + sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} on {Environment.MachineName} ---"); + if (!string.IsNullOrEmpty(module)) + { + sb.AppendLine(" Module : " + module); + } + + if (!string.IsNullOrEmpty(method)) + { + sb.AppendLine(" Method : " + method); + } + + sb.Append(" Type : ").AppendLine(exception.GetType().ToString()); + sb.Append(" Data : ").AppendLine(exception.Data != null && exception.Data.Count > 0 + ? FormatExceptionData(exception.Data) + : "(none)"); + sb.Append(" Inner : ").AppendLine(exception.InnerException != null + ? FormatInnerException(exception.InnerException) + : "(null)"); + sb.Append(" Message : ").AppendLine(exception.Message); + sb.Append(" Source : ").AppendLine(exception.Source); + sb.Append(" StackTrace : ").AppendLine(exception.StackTrace); + sb.Append(" Line : ").AppendLine(fileLine.ToString()); + sb.Append(" File : ").AppendLine(filename); + sb.Append(" ToString : ").AppendLine(exception.ToString()); + sb.AppendLine("---"); + + return sb.ToString(); + } + + private static string FormatExceptionData(IDictionary data) + { + var sb = new StringBuilderChill(); + + foreach (DictionaryEntry entry in data) + { + if (entry.Key != null) + { + sb.Append(" | ") + .Append(entry.Key); + } + + if (entry.Value != null) + { + sb.Append(": ") + .AppendLine(entry.Value.ToString()); + } + } + + return sb.ToString(); + } + + private static string FormatInnerException(Exception innerException) + { + var sb = new StringBuilderChill(); + + sb.AppendLine(innerException.GetType().ToString()) + .AppendLine(" Message : " + innerException.Message) + .AppendLine(" Source : " + innerException.Source) + .AppendLine(" StackTrace : " + innerException.StackTrace) + .AppendLine(" ToString : " + innerException) + .Append(" Data : ") + .AppendLine(innerException.Data != null && innerException.Data.Count > 0 + ? FormatExceptionData(innerException.Data) + : "(none)"); + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Extensions/ObjectExtensions.cs b/EonaCat.LogStack/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..9c068b4 --- /dev/null +++ b/EonaCat.LogStack/Extensions/ObjectExtensions.cs @@ -0,0 +1,843 @@ +using EonaCat.Json; +using EonaCat.Json.Serialization; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace EonaCat.LogStack.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public enum DumpFormat + { + Json, + Xml, + Tree + } + + public static class ObjectExtensions + { + /// + /// Executes an action on the object if it satisfies a predicate. + /// + public static T If(this T obj, Func predicate, Action action) + { + if (obj != null && predicate(obj)) + { + action(obj); + } + + return obj; + } + + /// + /// Executes an action on the object if it does NOT satisfy a predicate. + /// + public static T IfNot(this T obj, Func predicate, Action action) + { + if (obj != null && !predicate(obj)) + { + action(obj); + } + + return obj; + } + + /// + /// Executes a function on an object if not null, returns object itself. + /// Useful for chaining. + /// + public static T Tap(this T obj, Action action) + { + if (obj != null) + { + action(obj); + } + + return obj; + } + + /// + /// Returns true if object implements a given interface. + /// + public static bool Implements(this object obj) + { + if (obj == null) + { + return false; + } + + return typeof(TInterface).IsAssignableFrom(obj.GetType()); + } + + /// + /// Dumps any object to a string in JSON, XML, or detailed tree format. + /// + /// Object to dump + /// "json" (default), "xml", or "tree" + /// For JSON: include private/internal fields. Ignored for tree format + /// Optional max depth for tree dump. Null = no limit + /// Optional max items to display in collections. Null = show all + /// String representation of the object + public static string Dump(this object currentObject, DumpFormat format = DumpFormat.Json, bool detailed = false, int? maxDepth = null, int? maxCollectionItems = null) + { + if (currentObject == null) + { + return "null"; + } + + try + { + switch (format) + { + case DumpFormat.Xml: + return DumpXml(currentObject); + case DumpFormat.Tree: + return DumpTree(currentObject, maxDepth, maxCollectionItems); + case DumpFormat.Json: + default: + return DumpJson(currentObject, detailed); + } + } + catch (Exception ex) + { + return $"Error dumping object: {ex.Message}"; + } + } + + /// + /// Returns a default value if the object is null. + /// + public static T OrDefault(this T obj, T defaultValue = default) => + obj == null ? defaultValue : obj; + + /// + /// Returns the object if not null; otherwise executes a function to get a fallback. + /// + public static T OrElse(this T obj, Func fallback) => + obj != null ? obj : fallback(); + + /// + /// Converts an object to JSON string with optional formatting. + /// + public static string ToJson(this object obj, bool indented = false) + { + try + { + return obj == null + ? string.Empty + : !indented ? Json.JsonHelper.ToJson(obj, Formatting.None) : Json.JsonHelper.ToJson(obj, Formatting.Indented); + } + catch + { + return string.Empty; + } + } + + /// + /// Converts an object to string safely, returns empty string if null. + /// + public static string SafeToString(this object obj) => + obj?.ToString() ?? string.Empty; + + /// + /// Checks if an object is null or default. + /// + public static bool IsNullOrDefault(this T obj) => + EqualityComparer.Default.Equals(obj, default); + + /// + /// Safely casts an object to a specific type, returns default if cast fails. + /// + public static T SafeCast(this object obj) + { + if (obj is T variable) + { + return variable; + } + + return default; + } + + /// + /// Safely tries to convert object to integer. + /// + public static int ToInt(this object obj, int defaultValue = 0) + { + if (obj == null) + { + return defaultValue; + } + + return int.TryParse(obj.ToString(), out var val) ? val : defaultValue; + } + + /// + /// Safely tries to convert object to long. + /// + public static long ToLong(this object obj, long defaultValue = 0) + { + if (obj == null) + { + return defaultValue; + } + + return long.TryParse(obj.ToString(), out var val) ? val : defaultValue; + } + + /// + /// Safely tries to convert object to double. + /// + public static double ToDouble(this object obj, double defaultValue = 0) + { + if (obj == null) + { + return defaultValue; + } + + return double.TryParse(obj.ToString(), out var val) ? val : defaultValue; + } + + /// + /// Safely tries to convert object to bool. + /// + public static bool ToBool(this object obj, bool defaultValue = false) + { + if (obj == null) + { + return defaultValue; + } + + return bool.TryParse(obj.ToString(), out var val) ? val : defaultValue; + } + + /// + /// Checks if an object is of a specific type. + /// + public static bool IsType(this object obj) => obj is T; + + /// + /// Executes an action if the object is not null. + /// + public static void IfNotNull(this T obj, Action action) + { + if (obj != null) + { + action(obj); + } + } + + /// + /// Executes an action if the object is null. + /// + public static void IfNull(this T obj, Action action) + { + if (obj == null) + { + action(); + } + } + + /// + /// Wraps the object into a single-item enumerable. + /// + public static IEnumerable AsEnumerable(this T obj) + { + if (obj != null) + { + yield return obj; + } + } + + /// + /// Safely returns a string representation with max length truncation. + /// + public static string ToSafeString(this object obj, int maxLength) + { + string str = obj.SafeToString(); + return str.Length <= maxLength ? str : str.Substring(0, maxLength); + } + + /// + /// Returns object hash code safely (0 if null). + /// + public static int SafeHashCode(this object obj) => + obj?.GetHashCode() ?? 0; + + /// + /// Returns the object or throws a custom exception if null. + /// + public static T OrThrow(this T obj, Func exceptionFactory) + { + if (obj == null) + { + throw exceptionFactory(); + } + + return obj; + } + + private static string DumpJson(object currentObject, bool isDetailed) + { + var settings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + Formatting = Formatting.Indented + }; + + if (isDetailed) + { + settings.ContractResolver = new DefaultContractResolver + { + IgnoreSerializableAttribute = false, + IgnoreSerializableInterface = false + }; + } + + return JsonHelper.ToJson(currentObject, settings); + } + + private static string DumpXml(object currentObject) + { + try + { + var xmlSerializer = new XmlSerializer(currentObject.GetType()); + using (var stringWriter = new StringWriter()) + { + xmlSerializer.Serialize(stringWriter, currentObject); + return stringWriter.ToString(); + } + } + catch (Exception ex) + { + return $"XML serialization failed: {ex.Message}"; + } + } + + private static string DumpTree(object currentObject, int? maxDepth, int? maxCollectionItems) + { + var stringBuilder = new StringBuilder(); + var visitedHashSet = new HashSet(new ReferenceEqualityComparer()); + DumpTreeInternal(currentObject, stringBuilder, 0, visitedHashSet, maxDepth, maxCollectionItems); + return stringBuilder.ToString(); + } + + private static void DumpTreeInternal(object currentObject, StringBuilder stringBuilder, int indent, HashSet visited, int? maxDepth, int? maxCollectionItems) + { + string indentation = new string(' ', indent * 2); + + if (currentObject == null) + { + stringBuilder.AppendLine($"{indentation}null"); + return; + } + + Type type = currentObject.GetType(); + string typeName = type.FullName; + + if (IsPrimitive(type)) + { + stringBuilder.AppendLine($"{indentation}{currentObject} ({typeName})"); + return; + } + + if (visited.Contains(currentObject)) + { + stringBuilder.AppendLine($"{indentation}<>"); + return; + } + + if (maxDepth.HasValue && indent >= maxDepth.Value) + { + stringBuilder.AppendLine($"{indentation}<>"); + return; + } + + visited.Add(currentObject); + + if (currentObject is IEnumerable enumerable && !(currentObject is string)) + { + int count = 0; + + foreach (var _ in enumerable) + { + count++; + } + + if (maxCollectionItems.HasValue && count > maxCollectionItems.Value) + { + stringBuilder.AppendLine($"{indentation}{typeName} [<<{count} items, collapsed>>]"); + return; + } + + stringBuilder.AppendLine($"{indentation}{typeName} ["); + + foreach (var item in enumerable) + { + DumpTreeInternal(item, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems); + } + stringBuilder.AppendLine($"{indentation}]"); + } + else + { + stringBuilder.AppendLine($"{indentation}{typeName} {{"); + var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var members = type.GetFields(flags); + + foreach (var field in members) + { + object value = null; + + try + { + value = field.GetValue(currentObject); + } + catch + { + value = "<>"; + } + + stringBuilder.Append($"{indentation} {field.Name} = "); + DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems); + } + + var properties = type.GetProperties(flags); + + foreach (var current in properties) + { + if (current.GetIndexParameters().Length > 0) + { + continue; + } + + object value = null; + try { value = current.GetValue(currentObject); } catch { value = "<>"; } + stringBuilder.Append($"{indentation} {current.Name} = "); + DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems); + } + + stringBuilder.AppendLine($"{indentation}}}"); + } + } + + private static bool IsPrimitive(Type type) + { + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(DateTime) + || type == typeof(DateTimeOffset) + || type == typeof(Guid) + || type == typeof(TimeSpan); + } + + private class ReferenceEqualityComparer : IEqualityComparer + { + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + + public static void ForEach(this IEnumerable items, Action action) + { + if (items == null || action == null) + { + return; + } + + foreach (var item in items) + { + action(item); + } + } + + /// Check if collection is null or empty + public static bool IsNullOrEmpty(this IEnumerable items) => items == null || !items.Any(); + + /// Check if collection has items + public static bool HasItems(this IEnumerable items) => !items.IsNullOrEmpty(); + + /// Safe get by index + public static T SafeGet(this IList list, int index, T defaultValue = default) + { + if (list == null || index < 0 || index >= list.Count) + { + return defaultValue; + } + + return list[index]; + } + + /// Convert collection to delimited string + public static string ToDelimitedString(this IEnumerable items, string delimiter = ", ") + { + return items == null ? "" : string.Join(delimiter, items); + } + + public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); + + public static string Truncate(this string s, int maxLength) + { + if (string.IsNullOrEmpty(s)) + { + return s; + } + + return s.Length <= maxLength ? s : s.Substring(0, maxLength); + } + + public static bool ContainsIgnoreCase(this string s, string value) => + s?.IndexOf(value ?? "", StringComparison.OrdinalIgnoreCase) >= 0; + + public static string OrDefault(this string s, string defaultValue) => + string.IsNullOrEmpty(s) ? defaultValue : s; + + public static bool IsWeekend(this DateTime date) => + date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; + + public static DateTime StartOfDay(this DateTime date) => + date.Date; + + public static DateTime EndOfDay(this DateTime date) => + date.Date.AddDays(1).AddTicks(-1); + + public static IDisposable BeginLoggingScope(this ILogger logger, object context) + { + if (logger == null || context == null) + { + return null; + } + + return logger.BeginScope(context.ToDictionary()); + } + + public static void LogExecutionTime(this ILogger logger, Action action, string operationName) + { + if (logger == null || action == null) + { + return; + } + + var sw = System.Diagnostics.Stopwatch.StartNew(); + action(); + sw.Stop(); + logger.LogInformation("{Operation} executed in {ElapsedMilliseconds}ms", operationName, sw.ElapsedMilliseconds); + } + + /// + /// Converts a Unix timestamp, expressed as the number of seconds since the Unix epoch, to a local DateTime + /// value. + /// + /// The returned DateTime is expressed in the local time zone. To obtain a UTC DateTime, + /// use DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime instead. + /// The Unix timestamp representing the number of seconds that have elapsed since 00:00:00 UTC on 1 January + /// 1970. + /// A DateTime value that represents the local date and time equivalent of the specified Unix timestamp. + public static DateTime FromUnixTimestamp(this long timestamp) => + DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + + /// + /// Executes the specified task without waiting for its completion and handles any exceptions that occur during + /// its execution. + /// + /// Use this method to start a task when you do not need to await its completion but want + /// to ensure that exceptions are observed. This method should be used with caution, as exceptions may be + /// handled asynchronously and may not be propagated to the calling context. Avoid using this method for tasks + /// that must complete before continuing execution. + /// The task to execute in a fire-and-forget manner. Cannot be null. + /// An optional callback that is invoked if the task throws an exception. If not provided, exceptions are + /// written to the console. + public static async void FireAndForget(this Task task, Action onError = null) + { + if (task == null) + { + return; + } + + try { await task; } + catch (Exception ex) + { + if (onError != null) + { + onError(ex); + } + else + { + Console.WriteLine("FireAndForget Exception: " + ex.FormatExceptionToMessage()); + } + } + } + + /// Check if object has property + public static bool HasProperty(this object obj, string name) => + obj != null && obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null; + + /// Get property value safely + public static object GetPropertyValue(this object obj, string name) + { + if (obj == null) + { + return null; + } + + var prop = obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + return prop?.GetValue(obj); + } + + /// + /// Creates a dictionary containing the public and non-public instance properties and fields of the specified + /// object. + /// + /// Indexed properties are excluded from the resulting dictionary. Both public and + /// non-public instance members are included. If multiple members share the same name, property values will + /// overwrite field values with the same name. + /// The object whose properties and fields are to be included in the dictionary. Can be null. + /// A dictionary with the names and values of the object's properties and fields. If the object is null, returns + /// an empty dictionary. + public static Dictionary ToDictionary(this object obj) + { + if (obj == null) + { + return new Dictionary(); + } + + var dict = new Dictionary(); + var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + foreach (var prop in obj.GetType().GetProperties(flags)) + { + if (prop.GetIndexParameters().Length > 0) + { + continue; + } + + dict[prop.Name] = prop.GetValue(obj); + } + foreach (var field in obj.GetType().GetFields(flags)) + { + dict[field.Name] = field.GetValue(obj); + } + return dict; + } + + /// + /// Converts any object to a human-readable log string, including collections and nested objects. + /// + public static string ToLogString(this object obj, int maxDepth = 3, int currentDepth = 0) + { + if (obj == null) + { + return "null"; + } + + if (currentDepth >= maxDepth) + { + return "..."; + } + + // Handle strings separately + if (obj is string str) + { + return str; + } + + // Handle IEnumerable + if (obj is IEnumerable enumerable) + { + var items = new List(); + foreach (var item in enumerable) + { + items.Add(item.ToLogString(maxDepth, currentDepth + 1)); + } + return "[" + string.Join(", ", items) + "]"; + } + + // Handle primitive types + var type = obj.GetType(); + if (type.IsPrimitive || obj is decimal || obj is DateTime || obj is Guid) + { + return obj.ToString(); + } + + // Handle objects with properties + try + { + var props = type.GetProperties(); + var sb = new StringBuilder("{"); + bool first = true; + foreach (var p in props) + { + if (!first) + { + sb.Append(", "); + } + + var val = p.GetValue(obj); + sb.Append($"{p.Name}={val.ToLogString(maxDepth, currentDepth + 1)}"); + first = false; + } + sb.Append("}"); + return sb.ToString(); + } + catch + { + return obj.ToString(); + } + } + + /// + /// Checks if an object is considered "empty": null, empty string, empty collection. + /// + public static bool IsEmpty(this object obj) + { + if (obj == null) + { + return true; + } + + if (obj is string str) + { + return string.IsNullOrWhiteSpace(str); + } + + if (obj is ICollection col) + { + return col.Count == 0; + } + + if (obj is IEnumerable enumerable) + { + return !enumerable.Cast().Any(); + } + + return false; + } + + /// + /// Executes an action if the object is not null and not empty. + /// + public static void IfNotEmpty(this T obj, Action action) + { + if (!obj.IsEmpty()) + { + action(obj); + } + } + + /// + /// Returns a default value if the object is null or empty. + /// + public static T OrDefaultIfEmpty(this T obj, T defaultValue) + { + return obj.IsEmpty() ? defaultValue : obj; + } + + /// + /// Returns true if the object is numeric (int, float, double, decimal, long, etc.). + /// + public static bool IsNumeric(this object obj) + { + if (obj == null) + { + return false; + } + + return double.TryParse(obj.ToString(), out _); + } + + /// + /// Converts an object to a numeric double, returns default if conversion fails. + /// + public static double ToDoubleSafe(this object obj, double defaultValue = 0) + { + if (obj == null) + { + return defaultValue; + } + + return double.TryParse(obj.ToString(), out var d) ? d : defaultValue; + } + + /// + /// Converts an object to a numeric int, returns default if conversion fails. + /// + public static int ToIntSafe(this object obj, int defaultValue = 0) + { + if (obj == null) + { + return defaultValue; + } + + return int.TryParse(obj.ToString(), out var i) ? i : defaultValue; + } + + /// + /// Returns the type name of an object safely. + /// + public static string GetTypeName(this object obj) => + obj?.GetType().Name ?? "null"; + + /// + /// Executes a function if object is not null and returns a fallback value otherwise. + /// + public static TResult Map(this T obj, Func mapper, TResult fallback = default) + { + if (obj == null) + { + return fallback; + } + + return mapper(obj); + } + + /// + /// Masks sensitive strings (like passwords, tokens). Keeps first and last 2 characters visible. + /// + public static string MaskSensitive(this string str) + { + if (string.IsNullOrEmpty(str) || str.Length <= 4) + { + return "****"; + } + + int len = str.Length - 4; + return str.Substring(0, 2) + new string('*', len) + str.Substring(str.Length - 2, 2); + } + + /// + /// Masks sensitive data in any object property that matches a keyword. + /// + public static void MaskProperties(this object obj, params string[] keywords) + { + if (obj == null || keywords == null || keywords.Length == 0) + { + return; + } + + var props = obj.GetType().GetProperties(); + foreach (var p in props) + { + if (!p.CanRead || !p.CanWrite) + { + continue; + } + + if (keywords.Any(k => p.Name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0)) + { + var val = p.GetValue(obj) as string; + if (!string.IsNullOrEmpty(val)) + { + p.SetValue(obj, val.MaskSensitive()); + } + } + } + } + } +} diff --git a/EonaCat.LogStack/Extensions/OffsetStream.cs b/EonaCat.LogStack/Extensions/OffsetStream.cs new file mode 100644 index 0000000..d045ac1 --- /dev/null +++ b/EonaCat.LogStack/Extensions/OffsetStream.cs @@ -0,0 +1,260 @@ +using System; +using System.IO; + +namespace EonaCat.LogStack.Extensions; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class OffsetStream : Stream +{ + private const int BufferSize = 4096; + + public OffsetStream(Stream stream, long offset = 0, long length = 0, bool readOnly = false, bool ownStream = false) + { + if (stream.CanSeek) + { + if (offset > stream.Length) + { + throw new EndOfStreamException(); + } + + BaseStreamOffset = offset; + + if (length > stream.Length - offset) + { + throw new EndOfStreamException(); + } + + if (length == 0) + { + Length1 = stream.Length - offset; + } + else + { + Length1 = length; + } + } + else + { + BaseStreamOffset = 0; + Length1 = length; + } + + BaseStream = stream; + ReadOnly = readOnly; + OwnStream = ownStream; + } + + public override bool CanRead => BaseStream.CanRead; + + public override bool CanSeek => BaseStream.CanSeek; + + public override bool CanWrite => BaseStream.CanWrite && !ReadOnly; + + public override long Length => Length1; + + public override long Position + { + get => Position1; + + set + { + if (value > Length1) + { + throw new EndOfStreamException(); + } + + if (!BaseStream.CanSeek) + { + throw new NotSupportedException("Cannot seek stream."); + } + + Position1 = value; + } + } + + public long BaseStreamOffset { get; private set; } + + public Stream BaseStream { get; } + public long Length1 { get; set; } + public long Position1 { get; set; } + + public bool ReadOnly { get; } + + public bool Disposed { get; set; } + + public bool OwnStream { get; } + + protected override void Dispose(bool disposing) + { + if (Disposed) + { + return; + } + + if (disposing) + { + if (OwnStream & (BaseStream != null)) + { + BaseStream.Dispose(); + } + } + + Disposed = true; + + base.Dispose(disposing); + } + + public override void Flush() + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + BaseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count < 1) + { + throw new ArgumentOutOfRangeException("Count cannot be less than 1."); + } + + if (Position1 >= Length1) + { + return 0; + } + + if (count > Length1 - Position1) + { + count = Convert.ToInt32(Length1 - Position1); + } + + if (BaseStream.CanSeek) + { + BaseStream.Position = BaseStreamOffset + Position1; + } + + var bytesRead = BaseStream.Read(buffer, offset, count); + Position1 += bytesRead; + + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (!BaseStream.CanSeek) + { + throw new IOException("Stream is not seekable."); + } + + long pos; + + switch (origin) + { + case SeekOrigin.Begin: + pos = offset; + break; + + case SeekOrigin.Current: + pos = Position1 + offset; + break; + + case SeekOrigin.End: + pos = Length1 + offset; + break; + + default: + pos = 0; + break; + } + + if (pos < 0 || pos >= Length1) + { + throw new EndOfStreamException("OffsetStream reached begining/end of stream."); + } + + Position1 = pos; + + return pos; + } + + public override void SetLength(long value) + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + BaseStream.SetLength(BaseStreamOffset + value); + Length1 = value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + if (count < 1) + { + return; + } + + var pos = Position1 + count; + + if (pos > Length1) + { + throw new EndOfStreamException("OffsetStream reached end of stream."); + } + + if (BaseStream.CanSeek) + { + BaseStream.Position = BaseStreamOffset + Position1; + } + + BaseStream.Write(buffer, offset, count); + Position1 = pos; + } + + public void Reset(long offset, long length, long position) + { + BaseStreamOffset = offset; + Length1 = length; + Position1 = position; + } + + public void WriteTo(Stream stream) + { + WriteTo(stream, BufferSize); + } + + public void WriteTo(Stream stream, int bufferSize) + { + if (!BaseStream.CanSeek) + { + throw new IOException("Stream is not seekable."); + } + + if (Length1 < bufferSize) + { + bufferSize = Convert.ToInt32(Length1); + } + + var previousPosition = Position1; + Position1 = 0; + + try + { + CopyTo(stream, bufferSize); + } + finally + { + Position1 = previousPosition; + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Helpers/ColorHelper.cs b/EonaCat.LogStack/Helpers/ColorHelper.cs new file mode 100644 index 0000000..9d5059f --- /dev/null +++ b/EonaCat.LogStack/Helpers/ColorHelper.cs @@ -0,0 +1,140 @@ +using System; +using System.Drawing; +using System.Globalization; + +namespace EonaCat.LogStack.Helpers; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public static class ColorHelper +{ + public static string ColorToHexString(Color c) + { + return "#" + c.R.ToString("X2") + c.G.ToString("X2") + c.B.ToString("X2"); + } + + public static string ColorToRGBString(Color c) + { + return "RGB(" + c.R + "," + c.G + "," + c.B + ")"; + } + + public static Color ConsoleColorToColor(this ConsoleColor consoleColor) + { + switch (consoleColor) + { + case ConsoleColor.Black: + return Color.Black; + + case ConsoleColor.DarkBlue: + return HexStringToColor("#000080"); + + case ConsoleColor.DarkGreen: + return HexStringToColor("#008000"); + + case ConsoleColor.DarkCyan: + return HexStringToColor("#008080"); + + case ConsoleColor.DarkRed: + return HexStringToColor("#800000"); + + case ConsoleColor.DarkMagenta: + return HexStringToColor("#800080"); + + case ConsoleColor.DarkYellow: + return HexStringToColor("#808000"); + + case ConsoleColor.Gray: + return HexStringToColor("#C0C0C0"); + + case ConsoleColor.DarkGray: + return HexStringToColor("#808080"); + + case ConsoleColor.Blue: + return Color.Blue; + + case ConsoleColor.Green: + return Color.Lime; + + case ConsoleColor.Cyan: + return Color.Cyan; + + case ConsoleColor.Red: + return Color.Red; + + case ConsoleColor.Magenta: + return Color.Magenta; + + case ConsoleColor.Yellow: + return Color.Yellow; + + case ConsoleColor.White: + return Color.White; + + default: + throw new NotSupportedException(); + } + } + + public static Color HexStringToColor(string htmlColor, bool requireHexSpecified = false, int defaultAlpha = 0xFF) + { + return Color.FromArgb(HexColorToArgb(htmlColor, requireHexSpecified, defaultAlpha)); + } + + public static int HexColorToArgb(string htmlColor, bool requireHexSpecified = false, int defaultAlpha = 0xFF) + { + if (string.IsNullOrEmpty(htmlColor)) + { + throw new ArgumentNullException(nameof(htmlColor)); + } + + if (!htmlColor.StartsWith("#") && requireHexSpecified) + { + throw new ArgumentException($"Provided parameter '{htmlColor}' is not valid"); + } + + htmlColor = htmlColor.TrimStart('#'); + + + var symbolCount = htmlColor.Length; + var value = int.Parse(htmlColor, NumberStyles.HexNumber); + switch (symbolCount) + { + case 3: // RGB short hand + { + return (defaultAlpha << 24) + | (value & 0xF) + | ((value & 0xF) << 4) + | ((value & 0xF0) << 4) + | ((value & 0xF0) << 8) + | ((value & 0xF00) << 8) + | ((value & 0xF00) << 12) + ; + } + case 4: // RGBA short hand + { + // Inline alpha swap + return ((value & 0xF) << 24) + | ((value & 0xF) << 28) + | ((value & 0xF0) >> 4) + | (value & 0xF0) + | (value & 0xF00) + | ((value & 0xF00) << 4) + | ((value & 0xF000) << 4) + | ((value & 0xF000) << 8) + ; + } + case 6: // RGB complete definition + { + return (defaultAlpha << 24) | value; + } + case 8: // RGBA complete definition + { + // Alpha swap + return ((value & 0xFF) << 24) | (value >> 8); + } + default: + throw new FormatException("Invalid HTML Color"); + } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Helpers/EnumHelper.cs b/EonaCat.LogStack/Helpers/EnumHelper.cs new file mode 100644 index 0000000..efcfab7 --- /dev/null +++ b/EonaCat.LogStack/Helpers/EnumHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Helpers; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal static class EnumHelper + where T : struct +{ + static EnumHelper() + { + var names = Enum.GetNames(typeof(T)); + var values = (T[])Enum.GetValues(typeof(T)); + + Names = new Dictionary(names.Length); + Values = new Dictionary(names.Length * 2); + + for (var i = 0; i < names.Length; i++) + { + Names[values[i]] = names[i]; + Values[names[i]] = values[i]; + Values[names[i].ToLower()] = values[i]; + } + } + + public static Dictionary Names { get; } + + public static Dictionary Values { get; } + + public static string ToString(T value) + { + return Names.TryGetValue(value, out var result) ? result : Convert.ToInt64(value).ToString(); + } + + public static bool TryParse(string input, bool ignoreCase, out T value) + { + if (string.IsNullOrEmpty(input)) + { + value = default; + return false; + } + + return Values.TryGetValue(ignoreCase ? input.ToLower() : input, out value); + } + + internal static T Parse(string input, bool ignoreCase, T defaultValue) + { + return TryParse(input, ignoreCase, out var result) ? result : defaultValue; + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs new file mode 100644 index 0000000..85cbd00 --- /dev/null +++ b/EonaCat.LogStack/LogBuilder.cs @@ -0,0 +1,1076 @@ +using EonaCat.LogStack.Boosters; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.EonaCatLogStackCore.Policies; +using EonaCat.LogStack.Flows; +using ServiceMonitoring; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace EonaCat.LogStack.Configuration; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Fluent builder for configuring the logger with flows and boosters +/// +public sealed class LogBuilder +{ + private readonly string _category; + private LogLevel _minimumLevel = LogLevel.Trace; + private TimestampMode _timestampMode = TimestampMode.Utc; + private readonly List _flows = new(); + private readonly List _boosters = new(); + + public event EventHandler OnLog; + + public LogBuilder(string category = "Application") + { + _category = category ?? throw new ArgumentNullException(nameof(category)); + } + + /// + /// Sets the minimum log level + /// + public LogBuilder WithMinimumLevel(LogLevel level) + { + _minimumLevel = level; + return this; + } + + /// + /// Sets the timestamp mode + /// + public LogBuilder WithTimestampMode(TimestampMode mode) + { + _timestampMode = mode; + return this; + } + + /// + /// Adds console output + /// + public LogBuilder WriteToConsole( + LogLevel minimumLevel = LogLevel.Trace, + bool useColors = true) + { + _flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode)); + return this; + } + + /// + /// Adds diagnostics + /// + public LogBuilder WriteDiagnostics( + TimeSpan snapshotInterval = default(TimeSpan), + bool injectIntoEvents = false, + bool writeSnapshotEvents = true, + string snapshotCategory = "Diagnostics", + IFlow forwardTo = null, + LogLevel minimumLevel = LogLevel.Trace, + Func> customMetrics = null) + { + _flows.Add(new DiagnosticsFlow( + snapshotInterval, + injectIntoEvents, + writeSnapshotEvents, + snapshotCategory, + forwardTo, + minimumLevel, + customMetrics)); + return this; + } + + /// + /// Adds file output + /// + public LogBuilder WriteToFile( + string directory, + string filePrefix = "log", + long maxFileSize = 100 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + int batchSize = 1, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + FileOutputFormat outputFormat = FileOutputFormat.Text, + CompressionFormat compression = CompressionFormat.GZip, + string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") + { + _flows.Add(new FileFlow( + directory, + filePrefix, + maxFileSize, + fileRetentionPolicy, + flushIntervalInMilliSeconds, + batchSize, + minimumLevel, + useCategoryRouting, + logLevelsForSeparateFiles, + _timestampMode, + backpressureStrategy, + outputFormat, + compression, + template)); + return this; + } + + public LogBuilder WriteToEncryptedFile( + string directory, + string filePrefix = "log", + string password = "EonaCat", + long maxFileSize = 100 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new EncryptedFileFlow( + directory, + password, + filePrefix, + maxFileSize, + flushIntervalInMilliSeconds, + minimumLevel, + _timestampMode)); + return this; + } + + /// + /// Write to a rolling buffer + /// + /// Maximum number of events to retain. + /// Minimum level to store in the buffer. + /// + /// When a log event reaches this level or above, the current buffer + /// contents are immediately forwarded to . + /// Set to LogLevel.None (or omit) to disable. + /// + /// + /// Flow to forward the buffered context to when the trigger fires. + /// Can be null even when is set. + /// + /// + /// How many buffered lines to forward before the triggering event. + /// Defaults to entire buffer (int.MaxValue). + /// + /// + public LogBuilder WriteToRollingBuffer( + int capacity = 500, + LogLevel minimumLevel = LogLevel.Trace, + LogLevel triggerLevel = LogLevel.Error, + IFlow triggerTarget = null, + int preContextLines = int.MaxValue) + { + _flows.Add(new RollingBufferFlow( + capacity, + minimumLevel, + triggerLevel, + triggerTarget, + preContextLines)); + return this; + } + + /// + /// Decrypt a file which is encrypted by EonaCat Logger + /// + /// encrypted file source path + /// destination path for decrypted file + /// password used by encryption + /// + public static bool DecryptFile(string encryptedPath, string outputPath, string password) + { + return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password); + } + + /// + /// Adds Database output + /// + public LogBuilder WriteToDatabase( + Func connectionFactory, + string tableName = "logs", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DatabaseFlow( + connectionFactory, + tableName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Snmp traps + /// + public LogBuilder WriteToSnmpTrap(string host, int port = 162, string oid = "1.3.6.1.4.1.9999", LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SnmpTrapFlow( + host, + port, + oid, + minimumLevel)); + return this; + } + + /// + /// Adds Discord + /// + public LogBuilder WriteToDiscord( + string webHookUrl, + string botName = "EonaCatBot", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DiscordFlow( + webHookUrl, + botName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds ElasticSearch + /// + public LogBuilder WriteToElasticSearch( + string elasticSearchUrl, + string indexName = "EonaCatIndex", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ElasticSearchFlow( + elasticSearchUrl, + indexName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Telegram + /// + public LogBuilder WriteToTelegram(string botToken, string chatId = "EonaCat", int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new TelegramFlow( + botToken, + chatId, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a tamper-evident audit trail. + /// + /// Each audit entry is hash-chained: every line stores a SHA-256 of the previous + /// line's hash + the current entry body, so deletion or modification of any past + /// entry invalidates all subsequent hashes. + /// + /// Use to verify file integrity at any time. + /// + /// Directory where the .audit file is written. + /// File name prefix (default: "audit"). + /// + /// Which severity levels are recorded in the audit trail: + /// + /// – every log event (default) + /// – Warning, Error, Critical + /// – Error and Critical only + /// – Critical only + /// + /// + /// Minimum for audit capture. + /// Whether structured properties are appended to each entry. + public LogBuilder WriteToAudit( + string directory, + string filePrefix = "audit", + AuditLevel auditLevel = AuditLevel.All, + LogLevel minimumLevel = LogLevel.Trace, + bool includeProperties = true) + { + _flows.Add(new AuditFlow( + directory, + filePrefix, + auditLevel, + minimumLevel, + includeProperties)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SlackFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MicrosoftTeamsFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a TCP flow. + /// + public LogBuilder WriteToTcp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new TcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + + + /// + /// Adds a Retry flow to retry failed log writes. + /// + public LogBuilder WriteToRetry( + IFlow primaryFlow, + int maxRetries = 3, + TimeSpan? initialDelay = null, + bool exponentialBackoff = true) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + _flows.Add(new RetryFlow( + primaryFlow, + maxRetries, + initialDelay ?? TimeSpan.FromMilliseconds(200), + exponentialBackoff)); + + return this; + } + + /// + /// Adds an EventLog flow for sending logs to a remote destination. + /// + public LogBuilder WriteToEventLogFlow( + string destination, + int port = 514, + LogLevel minimumLevel = LogLevel.Trace, + int bufferSize = 100, + TimeSpan? flushInterval = null, + bool useTls = false, + RemoteCertificateValidationCallback? certificateValidationCallback = null, + X509CertificateCollection? clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(destination)) + { + throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); + } + + _flows.Add(new EventLogFlow( + destination, + port, + minimumLevel, + bufferSize, + flushInterval ?? TimeSpan.FromSeconds(5), + useTls, + certificateValidationCallback, + clientCertificates)); + return this; + } + + /// + /// Adds a Failover flow that switches to a secondary flow if the primary fails. + /// + public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + if (secondaryFlow == null) + { + throw new ArgumentNullException(nameof(secondaryFlow)); + } + + _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow)); + + return this; + } + + /// + /// Adds a Syslog TCP flow. + /// + public LogBuilder WriteToSyslogTcp( + string host, + int port = 514, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new SyslogTcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + /// + /// Adds a Status Monitoring flow. + /// + public LogBuilder WriteToStatusFlow( + List servicesToMonitor, + TimeSpan? checkInterval = null, + string statusDirectory = null, + Action statusChangedTrigger = null) + { + _flows.Add(new StatusFlow( + servicesToMonitor, + checkInterval, + statusDirectory, + statusChangedTrigger)); + + return this; + } + + /// + /// Adds Syslog Tcp + /// + public LogBuilder WriteToSyslogUdp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SyslogUdpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Zabbix + /// + public LogBuilder WriteToZabbixFlow( + string host, + int port = 10051, + string zabbixHostname = null, + string zabbixKey = "log_event", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new ZabbixFlow( + host, + port, + zabbixHostname, + zabbixKey, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Graylog + /// + public LogBuilder WriteToGraylogFlow( + string host, + int port = 12201, + bool useTcp = false, + string graylogHostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new GraylogFlow( + host, + port, + useTcp, + graylogHostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) + /// and optionally appends them to a Redis List (LPUSH) for persistence. + /// + /// Uses raw TCP + RESP protocol, so there arent additional dependencies + /// + /// Features: + /// - Reconnect with exponential back-off on connection failure + /// - Optional LPUSH to a list key with LTRIM to cap list length + /// - Optional password authentication (AUTH command) + /// - Optional DB selection (SELECT command) + /// - Background writer thread (non-blocking callers) + /// + public LogBuilder RedisFlow(string host = "localhost", + int port = 6379, + string password = null, + int database = 0, + string channel = "eonacat:logs", + string listKey = null, + int maxListLength = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new RedisFlow( + host, + port, + password, + database, + channel, + listKey, + maxListLength, + minimumLevel)); + return this; + } + + /// + /// Wraps any flow with token-bucket rate limiting and optional message deduplication. + /// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms. + /// + /// The downstream flow to protect. + /// + /// Max events that can be emitted in a burst per level (token bucket capacity). + /// + /// + /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. + /// + /// + /// If true, identical messages within are collapsed. + /// The suppressed count is appended to the message when the window expires. + /// + /// Deduplication window (default 60 s). + /// Maximum number of distinct messages tracked (default 1000). + /// Minimum level this flow processes. + public LogBuilder WriteToThrottled(IFlow inner, + int burstCapacity = 10, + double refillPerSecond = 1.0, + bool deduplicate = false, + TimeSpan dedupWindow = default(TimeSpan), + int dedupMaxKeys = 1000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ThrottledFlow( + inner, + burstCapacity, + refillPerSecond, + deduplicate, + dedupWindow, + dedupMaxKeys, + minimumLevel)); + return this; + } + + /// + /// Adds Splunk + /// + public LogBuilder WriteToSplunkFlow( + string splunkUrl, + string token, + string sourceType = "splunk_logs", + string hostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SplunkFlow( + splunkUrl, + token, + sourceType, + hostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. + /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. + /// + /// A lightweight alternative to the SignalR client library + /// + /// On the server side you need a minimal hub endpoint that accepts POST: + /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } + /// + /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and + /// render events in real time. + /// + public LogBuilder WriteToSignalR( + string hubUrl, + string hubMethod = "ReceiveLog", + HttpClient httpClient = null, + int batchSize = 20, + int batchIntervalMs = 500, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SignalRFlow( + hubUrl, + hubMethod, + httpClient, + batchSize, + batchIntervalMs, + minimumLevel)); + return this; + } + + /// + /// Sends log events as HTML email digests via SMTP. + /// Batches events for before sending, + /// unless flushOnCritical is true (Critical events bypass batching). + /// + public LogBuilder WriteToEmail( + string smtpHost, + int smtpPort = 587, + bool useSsl = true, + string username = null, + string password = null, + string from = null, + string to = null, + string subjectPrefix = "[EonaCatLogStack]", + int digestMinutes = 5, + bool flushOnCritical = true, + int maxEventsPerDigest = 100, + string headerName = null, + LogLevel minimumLevel = LogLevel.Error) + { + _flows.Add(new EmailFlow( + smtpHost, + smtpPort, + useSsl, + username, + password, + from, + to, + subjectPrefix, + digestMinutes, + flushOnCritical, + maxEventsPerDigest, + headerName, + minimumLevel)); + return this; + } + + /// + /// Adds Udp + /// + public LogBuilder WriteToUdp( + string host, + int port, + int flushIntervalInMilliseconds = 1000, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new UdpFlow( + host, + port, + flushIntervalInMilliseconds, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds in-memory buffer output + /// + public LogBuilder WriteToMemory( + int capacity = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MemoryFlow(capacity, minimumLevel)); + return this; + } + + /// + /// Adds HTTP endpoint output + /// + public LogBuilder WriteToHttp( + string endpoint, + HttpClient? httpClient = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + TimeSpan? batchInterval = null, + Dictionary? headers = null) + { + _flows.Add(new HttpFlow( + endpoint, + httpClient, + batchSize, + minimumLevel, + batchInterval, + headers)); + return this; + } + + /// + /// Adds a custom flow + /// + public LogBuilder WriteTo(IFlow flow) + { + _flows.Add(flow ?? throw new ArgumentNullException(nameof(flow))); + return this; + } + + /// + /// Boost logs with machine name + /// + public LogBuilder BoostWithMachineName() + { + _boosters.Add(new MachineNameBooster()); + return this; + } + + /// + /// Boost logs with process ID + /// + public LogBuilder BoostWithProcessId() + { + _boosters.Add(new ProcessIdBooster()); + return this; + } + + /// + /// Boost logs with the current date (yyyy-MM-dd) + /// + public LogBuilder BoostWithDate() + { + _boosters.Add(new DateBooster()); + return this; + } + + /// + /// Boost logs with the current time (HH:mm:ss.fff) + /// + public LogBuilder BoostWithTime() + { + _boosters.Add(new TimeBooster()); + return this; + } + + /// + /// Boost logs with the current timestamp ticks + /// + public LogBuilder BoostWithTicks() + { + _boosters.Add(new TicksBooster()); + return this; + } + + /// + /// Boost logs with the process start time + /// + public LogBuilder BoostWithProcStart() + { + _boosters.Add(new ProcStartBooster()); + return this; + } + + /// + /// Boost logs with the uptime of the process in seconds + /// + public LogBuilder BoostWithUptime() + { + _boosters.Add(new UptimeBooster()); + return this; + } + + /// + /// Boost logs with the current thread name + /// + public LogBuilder BoostWithThreadName() + { + _boosters.Add(new ThreadNameBooster()); + return this; + } + + /// + /// Boost logs with memory usage in MB + /// + public LogBuilder BoostWithMemory() + { + _boosters.Add(new MemoryBooster()); + return this; + } + + /// + /// Boost logs with the operating system description + /// + public LogBuilder BoostWithOS() + { + _boosters.Add(new OSBooster()); + return this; + } + + /// + /// Boost logs with the runtime/framework description + /// + public LogBuilder BoostWithFramework() + { + _boosters.Add(new FrameworkBooster()); + return this; + } + + /// + /// Boost logs with the application name and base directory + /// + public LogBuilder BoostWithApp() + { + _boosters.Add(new AppBooster()); + return this; + } + + /// + /// Boost logs with the current user name + /// + public LogBuilder BoostWithUser() + { + _boosters.Add(new UserBooster()); + return this; + } + + /// + /// Boost logs with the current thread ID + /// + public LogBuilder BoostWithThreadId() + { + _boosters.Add(new ThreadIdBooster()); + return this; + } + + + /// + /// Boost logs with custom text + /// + public LogBuilder BoostWithCustomText(string key, string value) + { + _boosters.Add(new CustomTextBooster(key, value)); + return this; + } + + /// + /// Boost logs with environment name + /// + public LogBuilder BoostWithEnvironment(string environmentName) + { + _boosters.Add(new EnvironmentBooster(environmentName)); + return this; + } + + /// + /// Boost logs with application name and version + /// + public LogBuilder BoostWithApplication(string applicationName, string? version = null) + { + _boosters.Add(new ApplicationBooster(applicationName, version)); + return this; + } + + /// + /// Boost logs with correlation ID from Activity + /// + public LogBuilder BoostWithCorrelationId() + { + _boosters.Add(new CorrelationIdBooster()); + return this; + } + + /// + /// Adds a custom booster + /// + public LogBuilder Boost(IBooster booster) + { + _boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster))); + return this; + } + + /// + /// Adds a callback-based booster + /// + public LogBuilder Boost(string name, Func> callback) + { + _boosters.Add(new CallbackBooster(name, callback)); + return this; + } + + /// + /// Builds the configured logger + /// + public EonaCatLogStack Build() + { + var logger = new EonaCatLogStack(_category, _minimumLevel, _timestampMode); + logger.OnLog += (sender, message) => OnLog?.Invoke(sender, message); + + foreach (var flow in _flows) + { + logger.AddFlow(flow); + } + + foreach (var booster in _boosters) + { + logger.AddBooster(booster); + } + + return logger; + } + + /// + /// Creates a default logger with console and file output + /// + public static EonaCatLogStack CreateDefault( + string category = "Application", + string? logDirectory = null) + { + var directory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "logs"); + + return new LogBuilder(category) + .WithMinimumLevel(LogLevel.Information) + .WriteToConsole() + .WriteToFile(directory) + .BoostWithMachineName() + .BoostWithProcessId() + .Build(); + } + + /// + /// Get a flow by name + /// + /// + public IFlow GetFlow(string name) + { + lock (_flows) + { + var flow = _flows.Find(x => x.Name == name); + return flow; + } + } + + /// + /// Get a flow by type + /// + /// + public IFlow GetFlow(Type type) + { + lock (_flows) + { + var flow = _flows.Find(x => x.GetType() == type); + return flow; + } + } + + /// + /// Add a flow to the logBuilder + /// + /// + public void AddFlow(IFlow flow) + { + lock (_flows) + { + _flows.Add(flow); + } + } + + /// + /// Removes a flow from the logBuilder + /// + /// To be removed flow + public void RemoveFlow(IFlow flow) + { + lock (_flows) + { + _flows.Remove(flow); + } + } + + /// + /// Removes a flow from the logBuilder by name + /// + /// To be removed flow name + public void RemoveFlow(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + lock (_flows) { _flows.RemoveAll(f => f.Name == name); } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/LogMessage.cs b/EonaCat.LogStack/LogMessage.cs new file mode 100644 index 0000000..80ec85b --- /dev/null +++ b/EonaCat.LogStack/LogMessage.cs @@ -0,0 +1,17 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class LogMessage + { + public LogLevel Level { get; set; } + public Exception Exception { get; set; } + public string Message { get; set; } + public string Origin { get; set; } + public string Category { get; set; } + } +} \ No newline at end of file diff --git a/EonaCat.LogStack/LoggerDiagnostics.cs b/EonaCat.LogStack/LoggerDiagnostics.cs new file mode 100644 index 0000000..80ed0e6 --- /dev/null +++ b/EonaCat.LogStack/LoggerDiagnostics.cs @@ -0,0 +1,22 @@ +using EonaCat.LogStack.Core; +using EonaCat.LogStack.Flows; +using System.Collections.Generic; + +namespace EonaCat.LogStack; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Diagnostic information about the logger +/// +public class LoggerDiagnostics +{ + public string Category { get; set; } + public LogLevel MinimumLevel { get; set; } + public long TotalLogged { get; set; } + public long TotalDropped { get; set; } + public int FlowCount { get; set; } + public int BoosterCount { get; set; } + public List Flows { get; set; } +} \ No newline at end of file diff --git a/EonaCat.LogStack/Server.cs b/EonaCat.LogStack/Server.cs new file mode 100644 index 0000000..43e77bc --- /dev/null +++ b/EonaCat.LogStack/Server.cs @@ -0,0 +1,602 @@ +using System.Collections.Concurrent; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Linq; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Server +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Minimum log level the server will persist. Entries below this level are silently dropped. + /// + public enum ServerLogLevel { Trace = 0, Debug = 1, Information = 2, Warning = 3, Error = 4, Critical = 5 } + + /// + /// Live counters exposed via . + /// + public class ServerMetrics + { + public long TotalReceived; + public long TotalWritten; + public long TotalDropped; + public long TotalBytes; + public long ActiveTcpConnections; + public DateTime StartedAt = DateTime.UtcNow; + public TimeSpan Uptime => DateTime.UtcNow - StartedAt; + } + + /// + /// Full configuration for the log server. All fields have sensible defaults. + /// + public class ServerOptions + { + /// Enable UDP listener (default: true). + public bool UseUdp { get; set; } = true; + + /// + /// Enable TCP listener (default: true). + /// Both TCP and UDP can run simultaneously on the same port. + /// + public bool UseTcp { get; set; } = true; + + /// + /// Enable a minimal HTTP ingest endpoint (default: false). + /// POST /ingest - accepts a single JSON entry or a JSON array. + /// GET /metrics - returns a JSON metrics snapshot. + /// + public bool UseHttp { get; set; } = false; + + /// Port for UDP/TCP listeners (default: 5555). + public int Port { get; set; } = 5555; + + /// Port for the HTTP ingest endpoint (default: 5556). + public int HttpPort { get; set; } = 5556; + + /// IP address to bind to (null = IPAddress.Any). + public IPAddress? BindAddress { get; set; } + + /// + /// Only persist entries at or above this level (default: Trace = everything). + /// Plain-text entries (non-JSON) always pass the level filter. + /// + public ServerLogLevel MinimumLevel { get; set; } = ServerLogLevel.Trace; + + /// Max log file size before rolling over (default: 200 MB). + public long MaxLogFileSize { get; set; } = 200L * 1024 * 1024; + + /// Days to keep daily log directories (default: 30). + public int LogRetentionDays { get; set; } = 30; + + /// Maximum total size of the logs directory before oldest dirs are purged (default: 10 GB). + public long MaxLogDirectorySize { get; set; } = 10L * 1024 * 1024 * 1024; + + /// + /// Max messages accepted per second per remote endpoint (0 = disabled). + /// Excess messages are dropped and counted in . + /// + public int RateLimitPerSecond { get; set; } = 0; + + /// Root directory where log files are written (default: "logs"). + public string LogsRootDirectory { get; set; } = "logs"; + } + + public class Server + { + private TcpListener? _tcpListener; + private UdpClient? _udpListener; + private HttpListener? _httpListener; + + private CancellationTokenSource _cts = new(); + private bool _isRunning; + private readonly ServerOptions _options; + + private readonly ConcurrentDictionary _rateLimiter = new(); + + private const int UdpBufferSize = 65507; + private const int TcpReadBufferChars = 8192; + + /// Live connection and throughput counters. + public ServerMetrics Metrics { get; } = new(); + + /// Raised after a log line has been written to disk. + public event Action? LogWritten; + + /// Raised when a log line is dropped (level filter or rate limit). + public event Action? LogDropped; + + /// Create a server using explicit . + public Server(ServerOptions options) + { + _options = options; + } + + /// + /// Backwards-compatible constructor matching the original API. + /// When is true UDP is enabled; TCP is also enabled by default. + /// + public Server(bool useUdp = true, int logRetentionDays = 30, long maxLogDirectorySize = 10L * 1024 * 1024 * 1024) + { + _options = new ServerOptions + { + UseUdp = useUdp, + UseTcp = true, + LogRetentionDays = logRetentionDays, + MaxLogDirectorySize = maxLogDirectorySize + }; + } + + /// Start all configured transports and begin accepting log entries. + public async Task Start(IPAddress? ipAddress = null, int port = 5555) + { + if (_isRunning) + { + Console.WriteLine("[EonaCat Server] Already running."); + return; + } + + _cts = new CancellationTokenSource(); + _isRunning = true; + Metrics.StartedAt = DateTime.UtcNow; + + var bind = ipAddress ?? _options.BindAddress ?? IPAddress.Any; + var tasks = new List(); + + if (_options.UseTcp) + { + _tcpListener = new TcpListener(bind, port); + _tcpListener.Start(); + Console.WriteLine($"[EonaCat Server] TCP listener on {bind}:{port}"); + tasks.Add(ListenTcpAsync(_cts.Token)); + } + + if (_options.UseUdp) + { + _udpListener = new UdpClient(port); + Console.WriteLine($"[EonaCat Server] UDP listener on port {port}"); + tasks.Add(ListenUdpAsync(_cts.Token)); + } + + if (_options.UseHttp) + { + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add($"http://*:{_options.HttpPort}/"); + _httpListener.Start(); + Console.WriteLine($"[EonaCat Server] HTTP ingest on port {_options.HttpPort}"); + tasks.Add(ListenHttpAsync(_cts.Token)); + } + + if (tasks.Count == 0) + { + Console.WriteLine("[EonaCat Server] WARNING: No transports are enabled. Check ServerOptions."); + _isRunning = false; + return; + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// Gracefully stop all transports. + public void Stop() + { + if (!_isRunning) + { + return; + } + + _cts.Cancel(); + _tcpListener?.Stop(); + _udpListener?.Close(); + _httpListener?.Stop(); + _cts.Dispose(); + _isRunning = false; + + Console.WriteLine( + $"[EonaCat Server] Stopped. " + + $"Received={Metrics.TotalReceived} Written={Metrics.TotalWritten} " + + $"Dropped={Metrics.TotalDropped} Bytes={Metrics.TotalBytes} " + + $"Uptime={Metrics.Uptime:hh\\:mm\\:ss}"); + } + + private async Task ListenTcpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + var client = await _tcpListener!.AcceptTcpClientAsync().ConfigureAwait(false); + Interlocked.Increment(ref Metrics.ActiveTcpConnections); + _ = Task.Run(() => HandleTcpClientAsync(client, ct), ct); + } + } + catch (OperationCanceledException) + { + // Do nothing + } + catch (Exception ex) { Console.WriteLine($"[TCP] Fatal: {ex.Message}"); } + finally { Console.WriteLine("[EonaCat Server] TCP listener stopped."); } + } + + private async Task HandleTcpClientAsync(TcpClient client, CancellationToken ct) + { + var remote = client.Client.RemoteEndPoint?.ToString() ?? "unknown"; + try + { + using (client) + using (var stream = client.GetStream()) + using (var reader = new StreamReader(stream, Encoding.UTF8)) + { + var buffer = new char[TcpReadBufferChars]; + var sb = new StringBuilder(); + int bytesRead; + + while ((bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) + { + sb.Append(buffer, 0, bytesRead); + } + + var raw = sb.ToString(); + Interlocked.Add(ref Metrics.TotalBytes, Encoding.UTF8.GetByteCount(raw)); + await DispatchAsync(raw, remote).ConfigureAwait(false); + } + } + catch (Exception ex) when (!ct.IsCancellationRequested) + { + Console.WriteLine($"[TCP] {remote}: {ex.Message}"); + } + finally + { + Interlocked.Decrement(ref Metrics.ActiveTcpConnections); + } + } + + private async Task ListenUdpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + var result = await _udpListener!.ReceiveAsync().ConfigureAwait(false); + var remote = result.RemoteEndPoint.ToString(); + Interlocked.Add(ref Metrics.TotalBytes, result.Buffer.Length); + var raw = Encoding.UTF8.GetString(result.Buffer); + await DispatchAsync(raw, remote).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Do nothing + } + catch (Exception ex) { Console.WriteLine($"[UDP] Fatal: {ex.Message}"); } + finally { Console.WriteLine("[EonaCat Server] UDP listener stopped."); } + } + + private async Task ListenHttpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + var context = await _httpListener!.GetContextAsync().ConfigureAwait(false); + _ = Task.Run(() => HandleHttpContextAsync(context, ct), ct); + } + } + catch (HttpListenerException) when (ct.IsCancellationRequested) { } + catch (Exception ex) { Console.WriteLine($"[HTTP] Fatal: {ex.Message}"); } + finally { Console.WriteLine("[EonaCat Server] HTTP listener stopped."); } + } + + private async Task HandleHttpContextAsync(HttpListenerContext ctx, CancellationToken ct) + { + var req = ctx.Request; + var res = ctx.Response; + var remote = req.RemoteEndPoint?.ToString() ?? "http"; + var path = req.Url?.AbsolutePath.TrimEnd('/') ?? ""; + + try + { + // GET /metrics + if (req.HttpMethod == "GET" && path == "/metrics") + { + var json = JsonSerializer.Serialize(new + { + totalReceived = Metrics.TotalReceived, + totalWritten = Metrics.TotalWritten, + totalDropped = Metrics.TotalDropped, + totalBytes = Metrics.TotalBytes, + activeTcpConnections = Metrics.ActiveTcpConnections, + uptimeSeconds = (long)Metrics.Uptime.TotalSeconds, + startedAt = Metrics.StartedAt.ToString("o") + }); + await WriteJsonResponseAsync(res, json, 200, ct).ConfigureAwait(false); + return; + } + + // POST /ingest + if (req.HttpMethod == "POST" && path == "/ingest") + { + using var reader = new StreamReader(req.InputStream, req.ContentEncoding); + var body = await reader.ReadToEndAsync().ConfigureAwait(false); + Interlocked.Add(ref Metrics.TotalBytes, Encoding.UTF8.GetByteCount(body)); + await DispatchAsync(body, remote).ConfigureAwait(false); + await WriteJsonResponseAsync(res, "{\"accepted\":true}", 202, ct).ConfigureAwait(false); + return; + } + + res.StatusCode = 404; + } + catch (Exception ex) + { + Console.WriteLine($"[HTTP] Handler error: {ex.Message}"); + try + { + res.StatusCode = 500; + } + catch + { + // Do nothing + } + } + finally + { + try { res.Close(); } catch { } + } + } + + private static async Task WriteJsonResponseAsync(HttpListenerResponse res, string json, int statusCode, CancellationToken ct) + { + var bytes = Encoding.UTF8.GetBytes(json); + res.StatusCode = statusCode; + res.ContentType = "application/json; charset=utf-8"; + res.ContentLength64 = bytes.Length; + await res.OutputStream.WriteAsync(bytes, 0, bytes.Length, ct).ConfigureAwait(false); + } + + + private async Task DispatchAsync(string raw, string remote) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return; + } + + Interlocked.Increment(ref Metrics.TotalReceived); + + // Rate limiting + if (_options.RateLimitPerSecond > 0 && IsRateLimited(remote)) + { + Interlocked.Increment(ref Metrics.TotalDropped); + LogDropped?.Invoke(raw); + return; + } + + // Try structured JSON path first + var entries = TryParseStructured(raw); + if (entries.Count == 0) + { + // Plain text - always passes level filter + await ProcessLogAsync(raw).ConfigureAwait(false); + return; + } + + foreach (var (line, level) in entries) + { + if (level < _options.MinimumLevel) + { + Interlocked.Increment(ref Metrics.TotalDropped); + LogDropped?.Invoke(line); + continue; + } + await ProcessLogAsync(line).ConfigureAwait(false); + } + } + + private List<(string line, ServerLogLevel level)> TryParseStructured(string raw) + { + var trimmed = raw.Trim(); + if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '[')) + { + return new List<(string, ServerLogLevel)>(); + } + + var result = new List<(string, ServerLogLevel)>(); + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + foreach (var el in doc.RootElement.EnumerateArray()) + { + result.Add(ParseJsonEntry(el)); + } + } + else + { + result.Add(ParseJsonEntry(doc.RootElement)); + } + } + catch + { + // if parsing fails, treat as plain text + } + return result; + } + + private static (string line, ServerLogLevel level) ParseJsonEntry(JsonElement jsonElement) + { + var level = ExtractLevel(jsonElement); + var timestamp = jsonElement.TryGetProperty("timestamp", out var tsProp) ? tsProp.GetString() ?? "" : DateTime.UtcNow.ToString("o"); + var message = jsonElement.TryGetProperty("message", out var mProp) ? mProp.GetString() ?? "" + : jsonElement.TryGetProperty("Message", out var mProp2) ? mProp2.GetString() ?? "" : jsonElement.GetRawText(); + var source = jsonElement.TryGetProperty("source", out var sProp) ? sProp.GetString() ?? "" + : jsonElement.TryGetProperty("application", out var aProp) ? aProp.GetString() ?? "" : ""; + var host = jsonElement.TryGetProperty("host", out var hProp) ? hProp.GetString() ?? "" : ""; + var exception = jsonElement.TryGetProperty("exception", out var eProp) ? eProp.GetString() : null; + var traceId = jsonElement.TryGetProperty("traceId", out var trProp) ? trProp.GetString() : null; + + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"[{timestamp}] [{level.ToString().ToUpper()}]"); + if (!string.IsNullOrEmpty(source)) + { + stringBuilder.Append($" [{source}]"); + } + + if (!string.IsNullOrEmpty(host)) + { + stringBuilder.Append($" host={host}"); + } + + if (!string.IsNullOrEmpty(traceId)) + { + stringBuilder.Append($" trace={traceId}"); + } + + stringBuilder.Append($" {message}"); + if (!string.IsNullOrEmpty(exception)) + { + stringBuilder.Append($"\n EXCEPTION: {exception}"); + } + + return (stringBuilder.ToString(), level); + } + + private static ServerLogLevel ExtractLevel(JsonElement element) + { + string? level = null; + foreach (var key in new[] { "level", "Level", "severity", "Severity" }) + { + if (element.TryGetProperty(key, out var p)) + { + level = p.GetString(); + break; + } + } + + return level?.ToLowerInvariant() switch + { + "trace" or "verbose" => ServerLogLevel.Trace, + "debug" => ServerLogLevel.Debug, + "info" or "information" => ServerLogLevel.Information, + "warn" or "warning" => ServerLogLevel.Warning, + "error" => ServerLogLevel.Error, + "fatal" or "critical" => ServerLogLevel.Critical, + _ => ServerLogLevel.Information + }; + } + + + private bool IsRateLimited(string remote) + { + var now = DateTime.UtcNow; + var entry = _rateLimiter.GetOrAdd(remote, _ => (now, 0)); + + if ((now - entry.window).TotalSeconds >= 1.0) + { + _rateLimiter[remote] = (now, 1); + return false; + } + + if (entry.count >= _options.RateLimitPerSecond) + { + return true; + } + + _rateLimiter[remote] = (entry.window, entry.count + 1); + return false; + } + + protected virtual async Task ProcessLogAsync(string logData) + { + var root = _options.LogsRootDirectory; + Directory.CreateDirectory(root); + + var daily = Path.Combine(root, DateTime.Now.ToString("yyyyMMdd")); + Directory.CreateDirectory(daily); + + var basePath = Path.Combine(daily, "EonaCatLogs"); + var filePath = basePath + ".log"; + + int idx = 1; + while (File.Exists(filePath) && new FileInfo(filePath).Length > _options.MaxLogFileSize) + { + filePath = $"{basePath}_{idx++}.log"; + } + + File.AppendAllText(filePath, logData + Environment.NewLine); + Interlocked.Increment(ref Metrics.TotalWritten); + LogWritten?.Invoke(logData); + + CleanUpOldLogs(); + } + + private void CleanUpOldLogs() + { + var root = _options.LogsRootDirectory; + if (!Directory.Exists(root)) + { + return; + } + + foreach (var dir in Directory.GetDirectories(root)) + { + try + { + if (new DirectoryInfo(dir).CreationTime < DateTime.Now.AddDays(-_options.LogRetentionDays)) + { + Directory.Delete(dir, true); + Console.WriteLine($"[Retention] Deleted expired directory: {dir}"); + } + } + catch (Exception ex) { Console.WriteLine($"[Retention] Error: {ex.Message}"); } + } + + long total = GetDirectorySize(root); + if (total <= _options.MaxLogDirectorySize) + { + return; + } + + Console.WriteLine($"[Retention] Directory {total / (1024 * 1024)} MB over limit. Purging oldest..."); + foreach (var dir in Directory.GetDirectories(root).OrderBy(d => new DirectoryInfo(d).CreationTime)) + { + try + { + total -= GetDirectorySize(dir); + Directory.Delete(dir, true); + Console.WriteLine($"[Retention] Purged: {dir}"); + if (total <= _options.MaxLogDirectorySize) + { + break; + } + } + catch (Exception ex) { Console.WriteLine($"[Retention] Error purging {dir}: {ex.Message}"); } + } + } + + private static long GetDirectorySize(string directory) + { + long size = 0; + try + { + size += Directory.GetFiles(directory).Sum(f => new FileInfo(f).Length); + foreach (var sub in Directory.GetDirectories(directory)) + { + size += GetDirectorySize(sub); + } + } + catch + { + // ignored + } + return size; + } + } +} diff --git a/EonaCat.LogStack/icon.ico b/EonaCat.LogStack/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..406f2659ac50172f9307c0af9038f473b50b6598 GIT binary patch literal 254014 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 z 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 IMBDrS02~H<8~^|S literal 0 HcmV?d00001 diff --git a/EonaCat.LogStack/libman.json b/EonaCat.LogStack/libman.json new file mode 100644 index 0000000..ceee271 --- /dev/null +++ b/EonaCat.LogStack/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 494f695..ab37379 100644 --- a/LICENSE +++ b/LICENSE @@ -1,73 +1,204 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + https://EonaCat.com/license/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + OF SOFTWARE BY EONACAT (JEROEN SAEY) -1. Definitions. + 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright 2026 EonaCat + Copyright [yyyy] [name of copyright owner] -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index ebfa0aa..8b8e773 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,477 @@ # EonaCat.LogStack -EonaCat.LogStack +**EonaCat.LogStack** flow-based logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency. +It features a rich fluent API for routing log events to dozens of destinations - from console and file to Slack, Discord, Redis, Elasticsearch, and beyond. + +--- + +## Features + +- **Flow-based architecture** - route log events to one or many output destinations simultaneously +- **Booster system** - enrich every log event with contextual metadata (machine name, process ID, thread info, memory, uptime, correlation IDs, and more) +- **Pre-build modifiers** - intercept and mutate log events before they are written +- **Zero-allocation hot path** - `AggressiveInlining` throughout, `StringBuilder` pooling, and `ref`-based builder pattern +- **Async-first** - all flows implement `IAsyncDisposable` and `FlushAsync` +- **Resilience built-in** - retry, failover, throttling, and rolling buffer flows +- **Tamper-evident audit trail** - SHA-256 hash-chained audit files +- **Encrypted file logging** - AES-encrypted log files with a built-in decrypt utility +- **Compression** - GZip-compressed rolled log files +- **Category routing** - split logs into separate files per category or log level +- **Diagnostics** - live counters (total logged, total dropped, per-flow stats) + +--- + +## Supported Targets + +- .NET Standard 2.1 +- .NET 8.0 +- .NET Framework 4.8 + +--- + +## Installation + +```bash +dotnet add package EonaCat.LogStack +``` + +--- + +## Quick Start + +```csharp +await using var logger = LogBuilder.CreateDefault("MyApp"); + +logger.Information("Application started"); +logger.Warning("Low memory warning"); +logger.Error(ex, "Unexpected error occurred"); +``` + +`CreateDefault` creates a logger writing to both the console and a `./logs` directory, enriched with machine name and process ID. + +--- + +## Fluent Configuration + +Build a fully customised logger using `LogBuilder`: + +```csharp +await using var logger = new LogBuilder("MyApp") + .WithMinimumLevel(LogLevel.Debug) + .WithTimestampMode(TimestampMode.Utc) + .WriteToConsole(useColors: true) + .WriteToFile("./logs", filePrefix: "app", maxFileSize: 50 * 1024 * 1024) + .WriteToSlack("https://hooks.slack.com/services/...") + .BoostWithMachineName() + .BoostWithProcessId() + .BoostWithCorrelationId() + .Build(); +``` + +--- + +## Logging Methods + +```csharp +logger.Trace("Verbose trace message"); +logger.Debug("Debug detail"); +logger.Information("Something happened"); +logger.Warning("Potential problem"); +logger.Warning(ex, "Warning with exception"); +logger.Error("Something failed"); +logger.Error(ex, "Error with exception"); +logger.Critical("System is going down"); +logger.Critical(ex, "Critical failure"); + +// With structured properties +logger.Log(LogLevel.Information, "User logged in", + ("UserId", 42), + ("IP", "192.168.1.1")); +``` + +--- + +## Available Flows +### Flows can be extended with custom implementations of `IFlow`, but here are the built-in options: + +| Flow | Method | Description | +|------|--------|-------------| +| Console | `WriteToConsole()` | Colored console output | +| File | `WriteToFile()` | Batched, rotated, compressed file output | +| Encrypted File | `WriteToEncryptedFile()` | AES-encrypted log files | +| Memory | `WriteToMemory()` | In-memory ring buffer | +| Audit | `WriteToAudit()` | Tamper-evident hash-chained audit trail | +| Database | `WriteToDatabase()` | ADO.NET database sink | +| HTTP | `WriteToHttp()` | Generic HTTP endpoint (batched) | +| Webhook | `WriteToWebhook()` | Generic webhook POST | +| Email | `WriteToEmail()` | HTML digest emails via SMTP | +| Slack | `WriteToSlack()` | Slack incoming webhooks | +| Discord | `WriteToDiscord()` | Discord webhooks | +| Microsoft Teams | `WriteToMicrosoftTeams()` | Teams incoming webhooks | +| Telegram | `WriteToTelegram()` | Telegram bot messages | +| SignalR | `WriteToSignalR()` | Real-time SignalR hub push | +| Redis | `RedisFlow()` | Redis Pub/Sub + optional List persistence | +| Elasticsearch | `WriteToElasticSearch()` | Elasticsearch index | +| Splunk | `WriteToSplunkFlow()` | Splunk HEC | +| Graylog | `WriteToGraylogFlow()` | GELF over UDP or TCP | +| Syslog UDP | `WriteToSyslogUdp()` | RFC-5424 Syslog over UDP | +| Syslog TCP | `WriteToSyslogTcp()` | RFC-5424 Syslog over TCP (with optional TLS) | +| TCP | `WriteToTcp()` | Raw TCP (with optional TLS) | +| UDP | `WriteToUdp()` | Raw UDP datagrams | +| SNMP Trap | `WriteToSnmpTrap()` | SNMP v2c traps | +| Zabbix | `WriteToZabbixFlow()` | Zabbix trapper protocol | +| EventLog | `WriteToEventLogFlow()` | Remote event log forwarding | +| Rolling Buffer | `WriteToRollingBuffer()` | Circular buffer with trigger-based flush | +| Throttled | `WriteToThrottled()` | Token-bucket rate limiting + deduplication | +| Retry | `WriteToRetry()` | Automatic retry with exponential back-off | +| Failover | `WriteToFailover()` | Primary/secondary failover | +| Diagnostics | `WriteDiagnostics()` | Periodic diagnostic snapshots | +| Status | `WriteToStatusFlow()` | Service health monitoring | + +--- + +## Available Boosters + +Boosters enrich every log event with additional properties before it reaches any flow. + +```csharp +new LogBuilder("MyApp") + .BoostWithMachineName() // host name + .BoostWithProcessId() // PID + .BoostWithThreadId() // managed thread ID + .BoostWithThreadName() // thread name + .BoostWithUser() // current OS user + .BoostWithApp() // app name and base directory + .BoostWithApplication("MyApp", "2.0.0") // explicit name + version + .BoostWithEnvironment("Production") + .BoostWithOS() // OS description + .BoostWithFramework() // .NET runtime description + .BoostWithMemory() // working set in MB + .BoostWithUptime() // process uptime in seconds + .BoostWithProcStart() // process start time + .BoostWithDate() // current date (yyyy-MM-dd) + .BoostWithTime() // current time (HH:mm:ss.fff) + .BoostWithTicks() // current timestamp ticks + .BoostWithCorrelationId() // Activity.Current correlation ID + .BoostWithCustomText("env", "prod") // arbitrary key/value + .Boost("myBooster", () => new Dictionary { ["key"] = "val" }) + ... +``` + +--- + +## Pre-Build Modifiers + +Modifiers run after boosters and can mutate or cancel a log event before it is dispatched to flows: + +```csharp +logger.AddModifier((ref LogEventBuilder builder) => +{ + builder.WithProperty("RequestId", Guid.NewGuid().ToString()); +}); +``` + +--- + +## Resilience Patterns + +### Retry with exponential back-off +```csharp +.WriteToRetry( + primaryFlow: new HttpFlow("https://logs.example.com"), + maxRetries: 5, + initialDelay: TimeSpan.FromMilliseconds(200), + exponentialBackoff: true) +``` + +### Primary / secondary failover +```csharp +.WriteToFailover( + primaryFlow: new ElasticSearchFlow("https://es-prod:9200"), + secondaryFlow: new FileFlow("./fallback-logs")) +``` + +### Token-bucket throttling with deduplication +```csharp +.WriteToThrottled( + inner: new SlackFlow(webhookUrl), + burstCapacity: 10, + refillPerSecond: 1.0, + deduplicate: true, + dedupWindow: TimeSpan.FromSeconds(60)) +``` + +### Rolling buffer - flush context on error +```csharp +.WriteToRollingBuffer( + capacity: 500, + triggerLevel: LogLevel.Error, + triggerTarget: new FileFlow("./error-context")) +``` + +--- + +## Encrypted File Logging + +```csharp +.WriteToEncryptedFile("./secure-logs", password: "s3cr3t") +``` + +To decrypt later: +```csharp +LogBuilder.DecryptFile( + encryptedPath: "./secure-logs/log.enc", + outputPath: "./secure-logs/log.txt", + password: "s3cr3t"); +``` + +--- + +## Audit Trail + +The audit flow produces a tamper-evident file where every entry is SHA-256 hash-chained. Deleting or modifying any past entry invalidates all subsequent hashes. + +```csharp +.WriteToAudit( + directory: "./audit", + auditLevel: AuditLevel.WarningAndAbove, + includeProperties: true) +``` + +Verify integrity at any time: +```csharp +bool intact = AuditFlow.Verify("./audit/audit.audit"); +``` + +--- + +## Log Message Template + +Both `ConsoleFlow` and `FileFlow` accept a customisable template string: + +``` +[{ts}] [{tz}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props} +``` + +| Token | Description | +|-------|-------------| +| `{ts}` | Timestamp (yyyy-MM-dd HH:mm:ss.fff) | +| `{tz}` | Timezone (UTC or local name) | +| `{host}` | Machine name | +| `{category}` | Logger category | +| `{thread}` | Managed thread ID | +| `{pid}` | Process ID | +| `{logtype}` | Log level label (INFO, WARN, ERROR, …) | +| `{message}` | Log message text | +| `{props}` | Structured properties as key=value pairs | +| `{newline}` | Line break | + +--- + +## Diagnostics + +```csharp +var diag = logger.GetDiagnostics(); +Console.WriteLine($"Logged: {diag.TotalLogged}, Dropped: {diag.TotalDropped}"); +``` + +--- + +## Flushing and Disposal + +```csharp +// Flush all pending events +await logger.FlushAsync(); + +// Dispose (flushes automatically) +await logger.DisposeAsync(); +``` + +--- + +## Events + +```csharp +logger.OnLog += (sender, msg) => +{ + // Fired for every log event that passes filters + Console.WriteLine($"[Event] {msg.Level}: {msg.Message}"); +}; +``` + +--- + +## Custom Flows + +Implement `IFlow` (or extend `FlowBase`) to create your own destination: + +```csharp +public class MyFlow : FlowBase +{ + public MyFlow() : base("MyFlow", LogLevel.Trace) { } + + public override Task BlastAsync(LogEvent logEvent, CancellationToken ct = default) + { + // Write logEvent somewhere + return Task.FromResult(WriteResult.Success); + } + + public override Task FlushAsync(CancellationToken ct = default) => Task.CompletedTask; +} + +// Register with: +new LogBuilder("App").WriteTo(new MyFlow()).Build(); +``` + + + +# EonaCat.LogStack.Server + +A lightweight, multi-transport log server for the EonaCat LogStack ecosystem. + +## Quick start + +```csharp +// Minimal - UDP on port 5555 +var server = new Server(); +await server.Start(); + +// Full control via ServerOptions +var server = new Server(new ServerOptions +{ + UseTcp = true, + UseUdp = true, + UseHttp = true, // enables POST /ingest + GET /metrics + Port = 5555, + HttpPort = 5556, + MinimumLevel = ServerLogLevel.Information, // drop Debug/Trace + RateLimitPerSecond = 100, // per remote endpoint + LogRetentionDays = 30, + MaxLogDirectorySize = 10L * 1024 * 1024 * 1024, // 10 GB + LogsRootDirectory = "logs", +}); + +server.LogWritten += line => Console.WriteLine("[written] " + line); +server.LogDropped += line => Console.WriteLine("[dropped] " + line); + +await server.Start(); +``` + +## Transports + +| Transport | Default | Notes | +|-----------|---------|-------| +| TCP | enabled | Streams until connection closes | +| UDP | enabled | Max 65 507 bytes per packet | +| HTTP | disabled | Enable via `UseHttp = true` | + +TCP and UDP can run simultaneously on the same port. + +## HTTP endpoints (when `UseHttp = true`) + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/ingest` | Accept a JSON log entry or array | +| `GET` | `/metrics` | Return live server metrics as JSON | + +### POST /ingest - single entry +```json +{ "level": "info", "message": "Hello world", "source": "MyApp", "host": "srv-01" } +``` + +### POST /ingest - batch +```json +[ + { "level": "warn", "message": "Disk at 80%", "source": "monitor" }, + { "level": "error", "message": "DB timeout", "source": "api", "exception": "TimeoutException…" } +] +``` + +### GET /metrics response +```json +{ + "totalReceived": 12345, + "totalWritten": 12300, + "totalDropped": 45, + "totalBytes": 4096000, + "activeTcpConnections": 3, + "uptimeSeconds": 3600, + "startedAt": "2026-03-27T08:00:00Z" +} +``` + +## Structured JSON parsing + +If the incoming payload is valid JSON the server parses it and formats each +entry before writing: + +``` +[2026-03-27T09:15:00Z] [ERROR] [MyApp] host=srv-01 trace=abc123 Something went wrong + EXCEPTION: System.TimeoutException: The operation timed out. +``` + +Recognised JSON fields: + +| Field | Aliases | Description | +|-------|---------|-------------| +| `timestamp` | - | ISO-8601 timestamp | +| `level` | `Level`, `severity`, `Severity` | Log level string | +| `message` | `Message` | Log message | +| `source` | `application` | App / service name | +| `host` | - | Hostname | +| `traceId` | - | Distributed trace ID | +| `exception` | - | Exception string | + +Plain-text payloads are written as-is and always bypass the level filter. + +## Log level filtering + +```csharp +MinimumLevel = ServerLogLevel.Warning // only Warning / Error / Critical are stored +``` + +Levels in order: `Trace → Debug → Information → Warning → Error → Critical` + +## Rate limiting + +```csharp +RateLimitPerSecond = 100 // per remote IP:port, 0 = disabled +``` + +Dropped messages are counted in `Metrics.TotalDropped` and raise the `LogDropped` event. + +## Metrics + +```csharp +var m = server.GetMetrics(); +Console.WriteLine($"Written={m.TotalWritten} Dropped={m.TotalDropped} Uptime={m.Uptime}"); +``` + +## Log file layout + +``` +logs/ + 20260327/ + EonaCatLogs.log ← active file (≤ 200 MB) + EonaCatLogs_1.log ← rolled over + 20260326/ + EonaCatLogs.log +``` + +Daily directories older than `LogRetentionDays` are deleted automatically. +The total directory is also capped at `MaxLogDirectorySize`. + +## Events + +```csharp +server.LogWritten += line => NotifyDashboard(line); +server.LogDropped += line => Metrics.Increment("dropped"); +``` + +## Graceful shutdown + +```csharp +Console.CancelKeyPress += (_, e) => { e.Cancel = true; server.Stop(); }; +``` + +`Stop()` prints a throughput summary and disposes all listeners cleanly. \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj new file mode 100644 index 0000000..c65ca1a --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml new file mode 100644 index 0000000..b5105b6 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

\ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml.cs b/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml.cs new file mode 100644 index 0000000..493796e --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Error.cshtml.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EonaCat.LogStack.Web.Pages; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorModel : PageModel +{ + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml new file mode 100644 index 0000000..fd28464 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml @@ -0,0 +1,10 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
\ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml.cs b/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml.cs new file mode 100644 index 0000000..cd35090 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Index.cshtml.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EonaCat.LogStack.Web.Pages; + +public class IndexModel : PageModel +{ + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml new file mode 100644 index 0000000..a92998a --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

\ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml.cs b/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml.cs new file mode 100644 index 0000000..982f554 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Privacy.cshtml.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EonaCat.LogStack.Web.Pages; + +public class PrivacyModel : PageModel +{ + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..bc12b85 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml @@ -0,0 +1,51 @@ + + + + + + @ViewData["Title"] - EonaCat.LogStack.Web + + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2022 - EonaCat.LogStack.Web - Privacy +
+
+ + + + + +@await RenderSectionAsync("Scripts", false) + + \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml.css b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml.css new file mode 100644 index 0000000..a72cbea --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_Layout.cshtml.css @@ -0,0 +1,48 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +a { + color: #0077cc; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; +} diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_ValidationScriptsPartial.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..660f00c --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewImports.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..d239c52 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using EonaCat.LogStack.Web +@namespace EonaCat.LogStack.Web.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewStart.cshtml b/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..1af6e49 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Program.cs b/Testers/EonaCat.LogStack.Test.Web/Program.cs new file mode 100644 index 0000000..709f120 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Program.cs @@ -0,0 +1,483 @@ +using EonaCat.LogStack.Configuration; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.Flows; +using EonaCat.LogStack.LogClient; +//using EonaCat.MemoryGuard; +using EonaCat.Web.RateLimiter; +using EonaCat.Web.RateLimiter.Endpoints.Extensions; +using EonaCat.LogStack.Flows.WindowsEventLog; +using EonaCat.LogStack.LogClient.Models; +using EonaCat.LogStack.Extensions; + +namespace EonaCat.LogStack.Test.Web +{ + public class Program + { + public static async Task Main(string[] args) + { + + // Configure the client + var centralOptions = new LogCentralOptions + { + ServerUrl = "https://localhost:62299", + ApiKey = "716a964de381979df4303bf93fc091d3", + ApplicationName = "MyApp", + ApplicationVersion = "1.0.0", + Environment = "Production", + FlushIntervalSeconds = 5 + }; + + var logClient = new LogCentralClient(centralOptions); + logClient.LogAsync(new LogEntry + { + Timestamp = DateTime.UtcNow, + Level = Core.LogLevel.Critical.ToString(), + Message = "Hello, LogCentral!", + Properties = new Dictionary + { + { "UserId", 123 }, + { "Operation", "TestLogging" } + }.ToJson() + }).ConfigureAwait(false); + Console.ReadKey(); + _ = Task.Run(async () => + { + var logBuilder = new LogBuilder(); + logBuilder.WithTimestampMode(TimestampMode.Local); + logBuilder.WriteToConsole(); + logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Json); + logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Xml); + logBuilder.WriteToTcp("127.0.0.1", 514); + //logBuilder.WriteToEncryptedFile("./logs"); + //logBuilder.WriteDiagnostics(); + logBuilder.WriteToAudit( + directory: "./audit", + auditLevel: AuditLevel.WarningAndAbove, // only Warn+ go to audit trail + includeProperties: true); + //logBuilder.WriteToRollingBuffer(); + var consoleFlow = logBuilder.GetFlow(typeof(ConsoleFlow)); + logBuilder.WriteToThrottled(consoleFlow); + logBuilder.WriteToStatusFlow(new List { new ServiceMonitoring.ServiceStatus { Host = "google.com", Port = 443, ServiceType = ServiceMonitoring.ServiceType.HTTPS } }, checkInterval: TimeSpan.FromSeconds(5)); + //logBuilder.WriteToSignalR(); + //logBuilder.WriteToEmailFlow(); + //logBuilder.WriteToWindowsEventLog(); + logBuilder.WriteToHttp("https://localhost:62299/api/logs/eonacat"); + var logger = logBuilder.Build(); + + logger.AddModifier((ref LogEventBuilder b) => + { + b.WithProperty("User", "Bob"); + }); + + var i = 0; + while (true) + { + i++; + logger.Log($"test to file {i}"); + logger.Log($"test to file {i}", Core.LogLevel.Critical); + logger.Log($"test to file {i}", Core.LogLevel.Debug); + logger.Log($"test to file {i}", Core.LogLevel.Error); + logger.Log($"test to file {i}", Core.LogLevel.Trace); + await Task.Delay(1).ConfigureAwait(false); + } + }); + Console.ReadKey(); + + //var _config = new MemoryGuardConfiguration + //{ + // MonitoringInterval = TimeSpan.FromSeconds(5), + // AnalysisInterval = TimeSpan.FromSeconds(10), + // PredictionInterval = TimeSpan.FromSeconds(15), + // LeakDetectionThreshold = TimeSpan.FromSeconds(5), + // SuspiciousObjectThreshold = TimeSpan.FromSeconds(3), + // BackgroundReportingInterval = TimeSpan.FromMinutes(1.5), + // CaptureStackTraces = true, + // EnableAutoRemediation = true, + // AutoSaveReports = true, + // MemoryPressureThreshold = 500 * 1024 * 1024, // 500MB + // BackgroundAnalysisInterval = TimeSpan.FromMinutes(1), + // OptimizationInterval = TimeSpan.FromMinutes(10), + // PatternDetectionInterval = TimeSpan.FromMinutes(3) + //}; + + //MemoryGuard.Start(_config); + + _ = Task.Run(async () => + { + var logBuilder = new LogBuilder(); + logBuilder.WriteToConsole(); + var logger = logBuilder.Build(); + + logger.AddModifier((ref LogEventBuilder b) => + { + b.WithProperty("User", "Alice"); + }); + + var i = 0; + while (true) + { + i++; + logger.Log($"test to file {i}"); + logger.Log($"test to file {i}", Core.LogLevel.Critical); + logger.Log($"test to file {i}", Core.LogLevel.Debug); + logger.Log($"test to file {i}", Core.LogLevel.Error); + logger.Log($"test to file {i}", Core.LogLevel.Trace); + await Task.Delay(1).ConfigureAwait(false); + } + }); + + var builder = WebApplication.CreateBuilder(args); + int onLogCounter = 0; + var defaultColor = Console.ForegroundColor; + + //_ = Task.Run(() => + //{ + // var logman = LogManager.Instance; + // var i = 0; + // while (true) + // { + // logman.WriteAsync($"Logman test {++i}", ELogType.TRACE).ConfigureAwait(false); + // Task.Delay(10).ConfigureAwait(false); + // } + //}).ConfigureAwait(false); + + // Add services to the container. + //Logger logger = new Logger(); + //logger.UseLocalTime = true; + //logger.LoggerSettings.Id = "TEST"; + //logger.LoggerSettings.LogInfo(); + //logger.LoggerSettings.LogWarning(); + //logger.LoggerSettings.LogError(); + //logger.LoggerSettings.LogCritical(); + //logger.LoggerSettings.LogDebug(); + //logger.LoggerSettings.LogTrace(); + //logger.LoggerSettings.LogTraffic(); + //logger.LoggerSettings.OnLog += LoggerSettings_OnLog; + + //LoggerSettings.CustomHeaderFormatter = ctx => + //{ + // if (ctx.LogType == ELogType.ERROR) + // { + // return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]"; + // } + + // return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]"; + //}; + + //logger.LoggerSettings.CustomHeaderFormatter = null; // remove the lambda + //logger.LoggerSettings.HeaderTokens.AddCustomToken("AppName", x => "[ALL YOUR BASE ARE BELONG TO US!]"); + //logger.LoggerSettings.HeaderFormat = "{AppName} {logtype} {ts}"; + + // Create the adapter + //var adapter = new LogCentralEonaCatAdapter(logger.LoggerSettings, logClient); + //await LogManager.Instance.WriteAsync("LogCentral adapter initialized", ELogType.INFO).ConfigureAwait(false); + + //// Now all EonaCat.LogStack logs will be sent to LogCentral automatically + //await logger.LogAsync("This is a test log message sent to LogCentral!", ELogType.INFO).ConfigureAwait(false); + + //var jsonLogger = new JsonFileLogger("Category", new JsonFileLoggerOptions { FileName = "test.json" }); + //jsonLogger?.SetContext("CorrelationId", "abc-123"); + //jsonLogger?.SetContext("UserId", "john.doe"); + //jsonLogger?.LogInformation("User logged in"); + + //Console.WriteLine(DllInfo.EonaCatVersion); + //Console.Title = VersionHelper.GetInformationalVersion(); + + //void LoggerSettings_OnLog(EonaCatLogMessage message) + //{ + // Console.ForegroundColor = ConsoleColor.Yellow; + // //Console.WriteLine($"LogCounter: {++onLogCounter} {message}"); + // Console.ForegroundColor = defaultColor; + //} + + //var options = new FileLoggerOptions(); + //options.MaxRolloverFiles = 5; + ////options.FileSizeLimit = 1 * 1024 * 1024 / 4; + //options.UseLocalTime = true; + //builder.Logging.AddEonaCatFileLogger(fileLoggerOptions: options, filenamePrefix: "web"); + //builder.Logging.AddEonaCatConsoleLogger(); + + //TcpLoggerOptions tcpLoggerOptions = new TcpLoggerOptions + //{ + // Host = "192.168.1.1", + // Port = 12345, + //}; + + //builder.Logging.AddEonaCatTcpLogger((tcpLoggerOptions) => + //{ + // tcpLoggerOptions.Port = 12345; + // tcpLoggerOptions.Host = "192.168.1.1"; + //}); + + builder.Services.AddRazorPages(); + + //var rateLimitOptions = new RateLimitOptions + //{ + // Capacity = 10, + // Duration = TimeSpan.FromMinutes(1) + //}; + + //builder.Services.AddRateLimiting(rateLimitOptions); + var rateOptions = new RateLimiterOptions(); + rateOptions.OutputLimitMessageAsHtml = false; + rateOptions.OutputLimitMessageAsXml = false; + rateOptions.OutputLimitMessageAsJson = true; + + rateOptions.AddDefaultConfiguration(config => + config.AddIpResolver().AddRule(3, TimeSpan.FromSeconds(10)) + ); + + RateLimiterMiddleware.OnLimitResponseCreated += RateLimiterMiddlewareOnLimitResponseCreatedAsync; + + async void RateLimiterMiddlewareOnLimitResponseCreatedAsync(object? sender, HttpContext httpContext) + { + await httpContext.Response.WriteAsync(" THIS IS MY CUSTOM RATE LIMIT MESSAGE").ConfigureAwait(false); + } + + //builder.Services.UseWebRateLimiter(rateOptions); + builder.Services.UseWebRateLimiter(rateOptions); + builder.Services.UseWebRateLimiter(rateOptions => + { + // Configures the default rateLimitConfiguration + rateOptions.AddDefaultConfiguration(rateLimitConfiguration => + { + // throttling is based on request ip + rateLimitConfiguration.AddIpResolver() + // add general rules for all ips + .AddRule(3, TimeSpan.FromSeconds(10)) // 3 requests could be called every 10 seconds + .AddRule(30, TimeSpan.FromMinutes(1)) // 30 requests could be called every 1 minute + .AddRule(500, TimeSpan.FromHours(1)); // 500 requests could be called every 1 hour + }); + }); + builder.Services.AddMemoryCache(); + + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (!app.Environment.IsDevelopment()) + { + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + //EonaCat.Web.RateLimiter.RateLimiterOptions options = new EonaCat.Web.RateLimiter.RateLimiterOptions(); + //options.LimitMessage = "FUCKING NOOB!"; + //options.WriteLimitMessageToResponse = true; + //options.WriteLimitMessageStatusCodeToResponse = true; + //app.UseRateLimiter(options); + //RateLimiterMiddleware.OnLimitResponseCreated += RateLimiterMiddleware_OnLimitResponseCreatedAsync; + //async void RateLimiterMiddleware_OnLimitResponseCreatedAsync(object? sender, HttpContext httpContext) + //{ + // await httpContext.Response.WriteAsync("THIS IS MY CUSTOM RATE LIMIT MESSAGE"); + //} + + //app.UseWebTracer(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + app.UseAuthorization(); + app.UseWebRateLimiter(); + + app.MapRazorPages().RateLimit(); + + //void RunLoggingExceptionTests() + //{ + // var loggerSettings = new LoggerSettings(); + // loggerSettings.FileLoggerOptions.UseLocalTime = true; + // loggerSettings.UseLocalTime = true; + // loggerSettings.Id = "TEST"; + // loggerSettings.FileLoggerOptions.Category = "ExceptionTests"; + // loggerSettings.FileLoggerOptions.EnableCategoryRouting = true; + // loggerSettings.TypesToLog.Add(ELogType.INFO); + + // var logger = new LogManager(loggerSettings); + // for (var i = 0; i < 10; i++) + // { + // try + // { + // throw new Exception($"Normal Exception {i}"); + // } + // catch (Exception exception) + // { + // logger.WriteAsync(exception).ConfigureAwait(false); + // Console.WriteLine($"Normal ExceptionLogged: {i}"); + // } + + // Task.Delay(1); + // } + //} + + ////MemoryLeakTester.Start(logger); + ////_ = Task.Run(RunMemoryReportTask).ConfigureAwait(false); + //_ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false); + //_ = Task.Run(RunWebLoggingTests).ConfigureAwait(false); + //_ = Task.Run(RunLoggingTestsAsync).ConfigureAwait(false); + //_ = Task.Run(RunLoggingExceptionTests).ConfigureAwait(false); + //_ = Task.Run(RunWebLoggingExceptionTests).ConfigureAwait(false); + + ////async Task RunMemoryReportTask() + ////{ + //// while (true) + //// { + //// await MemoryGuard.PrintReportAsync().ConfigureAwait(false); + //// await Task.Delay(60000).ConfigureAwait(false); + //// Console.ReadKey(); + //// } + ////} + + //void RunWebLoggingExceptionTests() + //{ + // for (var i = 0; i < 10; i++) + // { + // try + // { + // throw new Exception($"WebException {i}"); + // } + // catch (Exception exception) + // { + // app.Logger.LogCritical(exception, "CRITICAL"); + // app.Logger.LogDebug(exception, "DEBUG"); + // app.Logger.LogError(exception, "ERROR"); + // app.Logger.LogTrace(exception, "TRACE"); + // app.Logger.LogWarning(exception, "WARNING"); + // app.Logger.LogInformation(exception, "INFORMATION"); + // Console.WriteLine($"WebExceptionLogged: {i}"); + // } + + // Task.Delay(1); + // } + //} + + //async Task RunWebLoggingTests() + //{ + // if (!Directory.Exists(logger.LogFolder)) + // { + // Directory.CreateDirectory(logger.LogFolder); + // } + + // for (var i = 0; i < 9000000; i++) + // { + // app.Logger.LogInformation($"web-test {i}"); + // using (var file = new StreamWriter(Path.Combine(logger.LogFolder, "test.log"), true)) + // { + // await file.WriteAsync($"WebLogged: {i}{Environment.NewLine}").ConfigureAwait(false); + // } + // Console.WriteLine($"WebLogged: {i}"); + // await Task.Delay(1); + // } + //} + + //async Task RunLoggingTestsAsync() + //{ + // var loggerSettings = new LoggerSettings(); + // loggerSettings.Id = "TEST"; + // loggerSettings.UseLocalTime = true; + // loggerSettings.FileLoggerOptions.UseLocalTime = true; + // loggerSettings.TypesToLog.Add(ELogType.INFO); + // loggerSettings.TypesToLog.Add(ELogType.WARNING); + // loggerSettings.TypesToLog.Add(ELogType.ERROR); + // loggerSettings.TypesToLog.Add(ELogType.TRAFFIC); + // loggerSettings.TypesToLog.Add(ELogType.DEBUG); + // loggerSettings.LogDebug(); + // loggerSettings.LogInfo(); + // loggerSettings.LogWarning(); + // loggerSettings.LogError(); + // loggerSettings.LogCritical(); + // loggerSettings.LogTrace(); + // loggerSettings.LogTraffic(); + // loggerSettings.FileLoggerOptions.FileSizeLimit = 1024 * 1024 * 1; + // loggerSettings.FileLoggerOptions.FileNamePrefix = "AllTypes"; + // loggerSettings.FileLoggerOptions.MaxRolloverFiles = 5; + // var logger = new LogManager(loggerSettings); + + // for (var i = 0; i < 9000000; i++) + // { + // await logger.WriteAsync($"test to file {i}").ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.CRITICAL).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.DEBUG).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.ERROR).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.TRACE).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.TRAFFIC).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.WARNING).ConfigureAwait(false); + // await logger.WriteAsync($"test to file {i}", ELogType.NONE).ConfigureAwait(false); + // Console.WriteLine($"Logged: {i}"); + // await Task.Delay(1).ConfigureAwait(false); + // } + //} + + //async Task RunWebLoggerTestsAsync() + //{ + // var i = 0; + // while (true) + // { + // i++; + // await Task.Run(async () => + // { + // await logger.($"test via logger {i}").ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.CRITICAL).ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.DEBUG).ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.ERROR).ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.TRAFFIC).ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.WARNING).ConfigureAwait(false); + // await logger.LogAsync($"test via logger {i}", ELogType.NONE).ConfigureAwait(false); + // }).ConfigureAwait(false); + // await Task.Delay(1).ConfigureAwait(false); + // } + //} + app.Run(); + } + + //private static void Instance_LeakDetected(object? sender, EonaCat.MemoryGuard.EventArguments.MemoryLeakDetectedEventArgs e) + //{ + // // Leak detected + //} + } + + //static class MemoryLeakTester + //{ + // // Rooted forever → GC can never collect + // private static readonly List _managedLeak = new(); + // private static readonly List _handles = new(); + + // public static void Start(Logger logger) + // { + // // Leak triggered via logging (very realistic) + // logger.LoggerSettings.OnLog += OnLogLeak; + + // // Optional background unmanaged leak + // StartUnmanagedLeak(); + + // Console.ForegroundColor = ConsoleColor.Red; + // Console.WriteLine("⚠ MEMORY LEAK TEST ENABLED"); + // Console.ResetColor(); + // } + + // private static void OnLogLeak(EonaCatLogMessage message) + // { + // // 5 MB per log + // var data = new byte[5_000_000]; + // _managedLeak.Add(data); + + // // GCHandle leak (advanced / nasty) + // _handles.Add(GCHandle.Alloc(data, GCHandleType.Normal)); + // } + + // private static void StartUnmanagedLeak() + // { + // _ = Task.Run(async () => + // { + // while (true) + // { + // Marshal.AllocHGlobal(10_000_000); // 10 MB unmanaged + // await Task.Delay(500).ConfigureAwait(false); + // } + // }); + // } + //} + +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Properties/launchSettings.json b/Testers/EonaCat.LogStack.Test.Web/Properties/launchSettings.json new file mode 100644 index 0000000..a05667d --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16565", + "sslPort": 44361 + } + }, + "profiles": { + "EonaCat.LogStack.Web": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7204;http://localhost:5099", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Testers/EonaCat.LogStack.Test.Web/appsettings.Development.json b/Testers/EonaCat.LogStack.Test.Web/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Testers/EonaCat.LogStack.Test.Web/appsettings.json b/Testers/EonaCat.LogStack.Test.Web/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Testers/EonaCat.LogStack.Test.Web/libman.json b/Testers/EonaCat.LogStack.Test.Web/libman.json new file mode 100644 index 0000000..ceee271 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/css/site.css b/Testers/EonaCat.LogStack.Test.Web/wwwroot/css/site.css new file mode 100644 index 0000000..f27e5ad --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/css/site.css @@ -0,0 +1,18 @@ +html { + font-size: 14px; +} + +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +html { + position: relative; + min-height: 100%; +} + +body { + margin-bottom: 60px; +} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/favicon.ico b/Testers/EonaCat.LogStack.Test.Web/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63e859b476eff5055e0e557aaa151ca8223fbeef GIT binary patch literal 5430 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 * { + box-sizing: border-box; + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * .5); + padding-left: calc(var(--bs-gutter-x) * .5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0%; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-sm-0 { + margin-left: 0; + } + + .offset-sm-1 { + margin-left: 8.33333333%; + } + + .offset-sm-2 { + margin-left: 16.66666667%; + } + + .offset-sm-3 { + margin-left: 25%; + } + + .offset-sm-4 { + margin-left: 33.33333333%; + } + + .offset-sm-5 { + margin-left: 41.66666667%; + } + + .offset-sm-6 { + margin-left: 50%; + } + + .offset-sm-7 { + margin-left: 58.33333333%; + } + + .offset-sm-8 { + margin-left: 66.66666667%; + } + + .offset-sm-9 { + margin-left: 75%; + } + + .offset-sm-10 { + margin-left: 83.33333333%; + } + + .offset-sm-11 { + margin-left: 91.66666667%; + } + + .g-sm-0, +.gx-sm-0 { + --bs-gutter-x: 0; + } + + .g-sm-0, +.gy-sm-0 { + --bs-gutter-y: 0; + } + + .g-sm-1, +.gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + + .g-sm-1, +.gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + + .g-sm-2, +.gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + + .g-sm-2, +.gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + + .g-sm-3, +.gx-sm-3 { + --bs-gutter-x: 1rem; + } + + .g-sm-3, +.gy-sm-3 { + --bs-gutter-y: 1rem; + } + + .g-sm-4, +.gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + + .g-sm-4, +.gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + + .g-sm-5, +.gx-sm-5 { + --bs-gutter-x: 3rem; + } + + .g-sm-5, +.gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-md-0 { + margin-left: 0; + } + + .offset-md-1 { + margin-left: 8.33333333%; + } + + .offset-md-2 { + margin-left: 16.66666667%; + } + + .offset-md-3 { + margin-left: 25%; + } + + .offset-md-4 { + margin-left: 33.33333333%; + } + + .offset-md-5 { + margin-left: 41.66666667%; + } + + .offset-md-6 { + margin-left: 50%; + } + + .offset-md-7 { + margin-left: 58.33333333%; + } + + .offset-md-8 { + margin-left: 66.66666667%; + } + + .offset-md-9 { + margin-left: 75%; + } + + .offset-md-10 { + margin-left: 83.33333333%; + } + + .offset-md-11 { + margin-left: 91.66666667%; + } + + .g-md-0, +.gx-md-0 { + --bs-gutter-x: 0; + } + + .g-md-0, +.gy-md-0 { + --bs-gutter-y: 0; + } + + .g-md-1, +.gx-md-1 { + --bs-gutter-x: 0.25rem; + } + + .g-md-1, +.gy-md-1 { + --bs-gutter-y: 0.25rem; + } + + .g-md-2, +.gx-md-2 { + --bs-gutter-x: 0.5rem; + } + + .g-md-2, +.gy-md-2 { + --bs-gutter-y: 0.5rem; + } + + .g-md-3, +.gx-md-3 { + --bs-gutter-x: 1rem; + } + + .g-md-3, +.gy-md-3 { + --bs-gutter-y: 1rem; + } + + .g-md-4, +.gx-md-4 { + --bs-gutter-x: 1.5rem; + } + + .g-md-4, +.gy-md-4 { + --bs-gutter-y: 1.5rem; + } + + .g-md-5, +.gx-md-5 { + --bs-gutter-x: 3rem; + } + + .g-md-5, +.gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-lg-0 { + margin-left: 0; + } + + .offset-lg-1 { + margin-left: 8.33333333%; + } + + .offset-lg-2 { + margin-left: 16.66666667%; + } + + .offset-lg-3 { + margin-left: 25%; + } + + .offset-lg-4 { + margin-left: 33.33333333%; + } + + .offset-lg-5 { + margin-left: 41.66666667%; + } + + .offset-lg-6 { + margin-left: 50%; + } + + .offset-lg-7 { + margin-left: 58.33333333%; + } + + .offset-lg-8 { + margin-left: 66.66666667%; + } + + .offset-lg-9 { + margin-left: 75%; + } + + .offset-lg-10 { + margin-left: 83.33333333%; + } + + .offset-lg-11 { + margin-left: 91.66666667%; + } + + .g-lg-0, +.gx-lg-0 { + --bs-gutter-x: 0; + } + + .g-lg-0, +.gy-lg-0 { + --bs-gutter-y: 0; + } + + .g-lg-1, +.gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + + .g-lg-1, +.gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + + .g-lg-2, +.gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + + .g-lg-2, +.gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + + .g-lg-3, +.gx-lg-3 { + --bs-gutter-x: 1rem; + } + + .g-lg-3, +.gy-lg-3 { + --bs-gutter-y: 1rem; + } + + .g-lg-4, +.gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + + .g-lg-4, +.gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + + .g-lg-5, +.gx-lg-5 { + --bs-gutter-x: 3rem; + } + + .g-lg-5, +.gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xl-0 { + margin-left: 0; + } + + .offset-xl-1 { + margin-left: 8.33333333%; + } + + .offset-xl-2 { + margin-left: 16.66666667%; + } + + .offset-xl-3 { + margin-left: 25%; + } + + .offset-xl-4 { + margin-left: 33.33333333%; + } + + .offset-xl-5 { + margin-left: 41.66666667%; + } + + .offset-xl-6 { + margin-left: 50%; + } + + .offset-xl-7 { + margin-left: 58.33333333%; + } + + .offset-xl-8 { + margin-left: 66.66666667%; + } + + .offset-xl-9 { + margin-left: 75%; + } + + .offset-xl-10 { + margin-left: 83.33333333%; + } + + .offset-xl-11 { + margin-left: 91.66666667%; + } + + .g-xl-0, +.gx-xl-0 { + --bs-gutter-x: 0; + } + + .g-xl-0, +.gy-xl-0 { + --bs-gutter-y: 0; + } + + .g-xl-1, +.gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xl-1, +.gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xl-2, +.gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xl-2, +.gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xl-3, +.gx-xl-3 { + --bs-gutter-x: 1rem; + } + + .g-xl-3, +.gy-xl-3 { + --bs-gutter-y: 1rem; + } + + .g-xl-4, +.gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xl-4, +.gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xl-5, +.gx-xl-5 { + --bs-gutter-x: 3rem; + } + + .g-xl-5, +.gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xxl-0 { + margin-left: 0; + } + + .offset-xxl-1 { + margin-left: 8.33333333%; + } + + .offset-xxl-2 { + margin-left: 16.66666667%; + } + + .offset-xxl-3 { + margin-left: 25%; + } + + .offset-xxl-4 { + margin-left: 33.33333333%; + } + + .offset-xxl-5 { + margin-left: 41.66666667%; + } + + .offset-xxl-6 { + margin-left: 50%; + } + + .offset-xxl-7 { + margin-left: 58.33333333%; + } + + .offset-xxl-8 { + margin-left: 66.66666667%; + } + + .offset-xxl-9 { + margin-left: 75%; + } + + .offset-xxl-10 { + margin-left: 83.33333333%; + } + + .offset-xxl-11 { + margin-left: 91.66666667%; + } + + .g-xxl-0, +.gx-xxl-0 { + --bs-gutter-x: 0; + } + + .g-xxl-0, +.gy-xxl-0 { + --bs-gutter-y: 0; + } + + .g-xxl-1, +.gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xxl-1, +.gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xxl-2, +.gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xxl-2, +.gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xxl-3, +.gx-xxl-3 { + --bs-gutter-x: 1rem; + } + + .g-xxl-3, +.gy-xxl-3 { + --bs-gutter-y: 1rem; + } + + .g-xxl-4, +.gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xxl-4, +.gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xxl-5, +.gx-xxl-5 { + --bs-gutter-x: 3rem; + } + + .g-xxl-5, +.gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +@media (min-width: 576px) { + .d-sm-inline { + display: inline !important; + } + + .d-sm-inline-block { + display: inline-block !important; + } + + .d-sm-block { + display: block !important; + } + + .d-sm-grid { + display: grid !important; + } + + .d-sm-table { + display: table !important; + } + + .d-sm-table-row { + display: table-row !important; + } + + .d-sm-table-cell { + display: table-cell !important; + } + + .d-sm-flex { + display: flex !important; + } + + .d-sm-inline-flex { + display: inline-flex !important; + } + + .d-sm-none { + display: none !important; + } + + .flex-sm-fill { + flex: 1 1 auto !important; + } + + .flex-sm-row { + flex-direction: row !important; + } + + .flex-sm-column { + flex-direction: column !important; + } + + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-sm-wrap { + flex-wrap: wrap !important; + } + + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-sm-start { + justify-content: flex-start !important; + } + + .justify-content-sm-end { + justify-content: flex-end !important; + } + + .justify-content-sm-center { + justify-content: center !important; + } + + .justify-content-sm-between { + justify-content: space-between !important; + } + + .justify-content-sm-around { + justify-content: space-around !important; + } + + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + + .align-items-sm-start { + align-items: flex-start !important; + } + + .align-items-sm-end { + align-items: flex-end !important; + } + + .align-items-sm-center { + align-items: center !important; + } + + .align-items-sm-baseline { + align-items: baseline !important; + } + + .align-items-sm-stretch { + align-items: stretch !important; + } + + .align-content-sm-start { + align-content: flex-start !important; + } + + .align-content-sm-end { + align-content: flex-end !important; + } + + .align-content-sm-center { + align-content: center !important; + } + + .align-content-sm-between { + align-content: space-between !important; + } + + .align-content-sm-around { + align-content: space-around !important; + } + + .align-content-sm-stretch { + align-content: stretch !important; + } + + .align-self-sm-auto { + align-self: auto !important; + } + + .align-self-sm-start { + align-self: flex-start !important; + } + + .align-self-sm-end { + align-self: flex-end !important; + } + + .align-self-sm-center { + align-self: center !important; + } + + .align-self-sm-baseline { + align-self: baseline !important; + } + + .align-self-sm-stretch { + align-self: stretch !important; + } + + .order-sm-first { + order: -1 !important; + } + + .order-sm-0 { + order: 0 !important; + } + + .order-sm-1 { + order: 1 !important; + } + + .order-sm-2 { + order: 2 !important; + } + + .order-sm-3 { + order: 3 !important; + } + + .order-sm-4 { + order: 4 !important; + } + + .order-sm-5 { + order: 5 !important; + } + + .order-sm-last { + order: 6 !important; + } + + .m-sm-0 { + margin: 0 !important; + } + + .m-sm-1 { + margin: 0.25rem !important; + } + + .m-sm-2 { + margin: 0.5rem !important; + } + + .m-sm-3 { + margin: 1rem !important; + } + + .m-sm-4 { + margin: 1.5rem !important; + } + + .m-sm-5 { + margin: 3rem !important; + } + + .m-sm-auto { + margin: auto !important; + } + + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-sm-0 { + margin-top: 0 !important; + } + + .mt-sm-1 { + margin-top: 0.25rem !important; + } + + .mt-sm-2 { + margin-top: 0.5rem !important; + } + + .mt-sm-3 { + margin-top: 1rem !important; + } + + .mt-sm-4 { + margin-top: 1.5rem !important; + } + + .mt-sm-5 { + margin-top: 3rem !important; + } + + .mt-sm-auto { + margin-top: auto !important; + } + + .me-sm-0 { + margin-right: 0 !important; + } + + .me-sm-1 { + margin-right: 0.25rem !important; + } + + .me-sm-2 { + margin-right: 0.5rem !important; + } + + .me-sm-3 { + margin-right: 1rem !important; + } + + .me-sm-4 { + margin-right: 1.5rem !important; + } + + .me-sm-5 { + margin-right: 3rem !important; + } + + .me-sm-auto { + margin-right: auto !important; + } + + .mb-sm-0 { + margin-bottom: 0 !important; + } + + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + + .mb-sm-3 { + margin-bottom: 1rem !important; + } + + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + + .mb-sm-5 { + margin-bottom: 3rem !important; + } + + .mb-sm-auto { + margin-bottom: auto !important; + } + + .ms-sm-0 { + margin-left: 0 !important; + } + + .ms-sm-1 { + margin-left: 0.25rem !important; + } + + .ms-sm-2 { + margin-left: 0.5rem !important; + } + + .ms-sm-3 { + margin-left: 1rem !important; + } + + .ms-sm-4 { + margin-left: 1.5rem !important; + } + + .ms-sm-5 { + margin-left: 3rem !important; + } + + .ms-sm-auto { + margin-left: auto !important; + } + + .p-sm-0 { + padding: 0 !important; + } + + .p-sm-1 { + padding: 0.25rem !important; + } + + .p-sm-2 { + padding: 0.5rem !important; + } + + .p-sm-3 { + padding: 1rem !important; + } + + .p-sm-4 { + padding: 1.5rem !important; + } + + .p-sm-5 { + padding: 3rem !important; + } + + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-sm-0 { + padding-top: 0 !important; + } + + .pt-sm-1 { + padding-top: 0.25rem !important; + } + + .pt-sm-2 { + padding-top: 0.5rem !important; + } + + .pt-sm-3 { + padding-top: 1rem !important; + } + + .pt-sm-4 { + padding-top: 1.5rem !important; + } + + .pt-sm-5 { + padding-top: 3rem !important; + } + + .pe-sm-0 { + padding-right: 0 !important; + } + + .pe-sm-1 { + padding-right: 0.25rem !important; + } + + .pe-sm-2 { + padding-right: 0.5rem !important; + } + + .pe-sm-3 { + padding-right: 1rem !important; + } + + .pe-sm-4 { + padding-right: 1.5rem !important; + } + + .pe-sm-5 { + padding-right: 3rem !important; + } + + .pb-sm-0 { + padding-bottom: 0 !important; + } + + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + + .pb-sm-3 { + padding-bottom: 1rem !important; + } + + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + + .pb-sm-5 { + padding-bottom: 3rem !important; + } + + .ps-sm-0 { + padding-left: 0 !important; + } + + .ps-sm-1 { + padding-left: 0.25rem !important; + } + + .ps-sm-2 { + padding-left: 0.5rem !important; + } + + .ps-sm-3 { + padding-left: 1rem !important; + } + + .ps-sm-4 { + padding-left: 1.5rem !important; + } + + .ps-sm-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 768px) { + .d-md-inline { + display: inline !important; + } + + .d-md-inline-block { + display: inline-block !important; + } + + .d-md-block { + display: block !important; + } + + .d-md-grid { + display: grid !important; + } + + .d-md-table { + display: table !important; + } + + .d-md-table-row { + display: table-row !important; + } + + .d-md-table-cell { + display: table-cell !important; + } + + .d-md-flex { + display: flex !important; + } + + .d-md-inline-flex { + display: inline-flex !important; + } + + .d-md-none { + display: none !important; + } + + .flex-md-fill { + flex: 1 1 auto !important; + } + + .flex-md-row { + flex-direction: row !important; + } + + .flex-md-column { + flex-direction: column !important; + } + + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-md-grow-0 { + flex-grow: 0 !important; + } + + .flex-md-grow-1 { + flex-grow: 1 !important; + } + + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-md-wrap { + flex-wrap: wrap !important; + } + + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-md-start { + justify-content: flex-start !important; + } + + .justify-content-md-end { + justify-content: flex-end !important; + } + + .justify-content-md-center { + justify-content: center !important; + } + + .justify-content-md-between { + justify-content: space-between !important; + } + + .justify-content-md-around { + justify-content: space-around !important; + } + + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + + .align-items-md-start { + align-items: flex-start !important; + } + + .align-items-md-end { + align-items: flex-end !important; + } + + .align-items-md-center { + align-items: center !important; + } + + .align-items-md-baseline { + align-items: baseline !important; + } + + .align-items-md-stretch { + align-items: stretch !important; + } + + .align-content-md-start { + align-content: flex-start !important; + } + + .align-content-md-end { + align-content: flex-end !important; + } + + .align-content-md-center { + align-content: center !important; + } + + .align-content-md-between { + align-content: space-between !important; + } + + .align-content-md-around { + align-content: space-around !important; + } + + .align-content-md-stretch { + align-content: stretch !important; + } + + .align-self-md-auto { + align-self: auto !important; + } + + .align-self-md-start { + align-self: flex-start !important; + } + + .align-self-md-end { + align-self: flex-end !important; + } + + .align-self-md-center { + align-self: center !important; + } + + .align-self-md-baseline { + align-self: baseline !important; + } + + .align-self-md-stretch { + align-self: stretch !important; + } + + .order-md-first { + order: -1 !important; + } + + .order-md-0 { + order: 0 !important; + } + + .order-md-1 { + order: 1 !important; + } + + .order-md-2 { + order: 2 !important; + } + + .order-md-3 { + order: 3 !important; + } + + .order-md-4 { + order: 4 !important; + } + + .order-md-5 { + order: 5 !important; + } + + .order-md-last { + order: 6 !important; + } + + .m-md-0 { + margin: 0 !important; + } + + .m-md-1 { + margin: 0.25rem !important; + } + + .m-md-2 { + margin: 0.5rem !important; + } + + .m-md-3 { + margin: 1rem !important; + } + + .m-md-4 { + margin: 1.5rem !important; + } + + .m-md-5 { + margin: 3rem !important; + } + + .m-md-auto { + margin: auto !important; + } + + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-md-0 { + margin-top: 0 !important; + } + + .mt-md-1 { + margin-top: 0.25rem !important; + } + + .mt-md-2 { + margin-top: 0.5rem !important; + } + + .mt-md-3 { + margin-top: 1rem !important; + } + + .mt-md-4 { + margin-top: 1.5rem !important; + } + + .mt-md-5 { + margin-top: 3rem !important; + } + + .mt-md-auto { + margin-top: auto !important; + } + + .me-md-0 { + margin-right: 0 !important; + } + + .me-md-1 { + margin-right: 0.25rem !important; + } + + .me-md-2 { + margin-right: 0.5rem !important; + } + + .me-md-3 { + margin-right: 1rem !important; + } + + .me-md-4 { + margin-right: 1.5rem !important; + } + + .me-md-5 { + margin-right: 3rem !important; + } + + .me-md-auto { + margin-right: auto !important; + } + + .mb-md-0 { + margin-bottom: 0 !important; + } + + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + + .mb-md-3 { + margin-bottom: 1rem !important; + } + + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + + .mb-md-5 { + margin-bottom: 3rem !important; + } + + .mb-md-auto { + margin-bottom: auto !important; + } + + .ms-md-0 { + margin-left: 0 !important; + } + + .ms-md-1 { + margin-left: 0.25rem !important; + } + + .ms-md-2 { + margin-left: 0.5rem !important; + } + + .ms-md-3 { + margin-left: 1rem !important; + } + + .ms-md-4 { + margin-left: 1.5rem !important; + } + + .ms-md-5 { + margin-left: 3rem !important; + } + + .ms-md-auto { + margin-left: auto !important; + } + + .p-md-0 { + padding: 0 !important; + } + + .p-md-1 { + padding: 0.25rem !important; + } + + .p-md-2 { + padding: 0.5rem !important; + } + + .p-md-3 { + padding: 1rem !important; + } + + .p-md-4 { + padding: 1.5rem !important; + } + + .p-md-5 { + padding: 3rem !important; + } + + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-md-0 { + padding-top: 0 !important; + } + + .pt-md-1 { + padding-top: 0.25rem !important; + } + + .pt-md-2 { + padding-top: 0.5rem !important; + } + + .pt-md-3 { + padding-top: 1rem !important; + } + + .pt-md-4 { + padding-top: 1.5rem !important; + } + + .pt-md-5 { + padding-top: 3rem !important; + } + + .pe-md-0 { + padding-right: 0 !important; + } + + .pe-md-1 { + padding-right: 0.25rem !important; + } + + .pe-md-2 { + padding-right: 0.5rem !important; + } + + .pe-md-3 { + padding-right: 1rem !important; + } + + .pe-md-4 { + padding-right: 1.5rem !important; + } + + .pe-md-5 { + padding-right: 3rem !important; + } + + .pb-md-0 { + padding-bottom: 0 !important; + } + + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + + .pb-md-3 { + padding-bottom: 1rem !important; + } + + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + + .pb-md-5 { + padding-bottom: 3rem !important; + } + + .ps-md-0 { + padding-left: 0 !important; + } + + .ps-md-1 { + padding-left: 0.25rem !important; + } + + .ps-md-2 { + padding-left: 0.5rem !important; + } + + .ps-md-3 { + padding-left: 1rem !important; + } + + .ps-md-4 { + padding-left: 1.5rem !important; + } + + .ps-md-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 992px) { + .d-lg-inline { + display: inline !important; + } + + .d-lg-inline-block { + display: inline-block !important; + } + + .d-lg-block { + display: block !important; + } + + .d-lg-grid { + display: grid !important; + } + + .d-lg-table { + display: table !important; + } + + .d-lg-table-row { + display: table-row !important; + } + + .d-lg-table-cell { + display: table-cell !important; + } + + .d-lg-flex { + display: flex !important; + } + + .d-lg-inline-flex { + display: inline-flex !important; + } + + .d-lg-none { + display: none !important; + } + + .flex-lg-fill { + flex: 1 1 auto !important; + } + + .flex-lg-row { + flex-direction: row !important; + } + + .flex-lg-column { + flex-direction: column !important; + } + + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-lg-wrap { + flex-wrap: wrap !important; + } + + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-lg-start { + justify-content: flex-start !important; + } + + .justify-content-lg-end { + justify-content: flex-end !important; + } + + .justify-content-lg-center { + justify-content: center !important; + } + + .justify-content-lg-between { + justify-content: space-between !important; + } + + .justify-content-lg-around { + justify-content: space-around !important; + } + + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + + .align-items-lg-start { + align-items: flex-start !important; + } + + .align-items-lg-end { + align-items: flex-end !important; + } + + .align-items-lg-center { + align-items: center !important; + } + + .align-items-lg-baseline { + align-items: baseline !important; + } + + .align-items-lg-stretch { + align-items: stretch !important; + } + + .align-content-lg-start { + align-content: flex-start !important; + } + + .align-content-lg-end { + align-content: flex-end !important; + } + + .align-content-lg-center { + align-content: center !important; + } + + .align-content-lg-between { + align-content: space-between !important; + } + + .align-content-lg-around { + align-content: space-around !important; + } + + .align-content-lg-stretch { + align-content: stretch !important; + } + + .align-self-lg-auto { + align-self: auto !important; + } + + .align-self-lg-start { + align-self: flex-start !important; + } + + .align-self-lg-end { + align-self: flex-end !important; + } + + .align-self-lg-center { + align-self: center !important; + } + + .align-self-lg-baseline { + align-self: baseline !important; + } + + .align-self-lg-stretch { + align-self: stretch !important; + } + + .order-lg-first { + order: -1 !important; + } + + .order-lg-0 { + order: 0 !important; + } + + .order-lg-1 { + order: 1 !important; + } + + .order-lg-2 { + order: 2 !important; + } + + .order-lg-3 { + order: 3 !important; + } + + .order-lg-4 { + order: 4 !important; + } + + .order-lg-5 { + order: 5 !important; + } + + .order-lg-last { + order: 6 !important; + } + + .m-lg-0 { + margin: 0 !important; + } + + .m-lg-1 { + margin: 0.25rem !important; + } + + .m-lg-2 { + margin: 0.5rem !important; + } + + .m-lg-3 { + margin: 1rem !important; + } + + .m-lg-4 { + margin: 1.5rem !important; + } + + .m-lg-5 { + margin: 3rem !important; + } + + .m-lg-auto { + margin: auto !important; + } + + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-lg-0 { + margin-top: 0 !important; + } + + .mt-lg-1 { + margin-top: 0.25rem !important; + } + + .mt-lg-2 { + margin-top: 0.5rem !important; + } + + .mt-lg-3 { + margin-top: 1rem !important; + } + + .mt-lg-4 { + margin-top: 1.5rem !important; + } + + .mt-lg-5 { + margin-top: 3rem !important; + } + + .mt-lg-auto { + margin-top: auto !important; + } + + .me-lg-0 { + margin-right: 0 !important; + } + + .me-lg-1 { + margin-right: 0.25rem !important; + } + + .me-lg-2 { + margin-right: 0.5rem !important; + } + + .me-lg-3 { + margin-right: 1rem !important; + } + + .me-lg-4 { + margin-right: 1.5rem !important; + } + + .me-lg-5 { + margin-right: 3rem !important; + } + + .me-lg-auto { + margin-right: auto !important; + } + + .mb-lg-0 { + margin-bottom: 0 !important; + } + + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + + .mb-lg-3 { + margin-bottom: 1rem !important; + } + + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + + .mb-lg-5 { + margin-bottom: 3rem !important; + } + + .mb-lg-auto { + margin-bottom: auto !important; + } + + .ms-lg-0 { + margin-left: 0 !important; + } + + .ms-lg-1 { + margin-left: 0.25rem !important; + } + + .ms-lg-2 { + margin-left: 0.5rem !important; + } + + .ms-lg-3 { + margin-left: 1rem !important; + } + + .ms-lg-4 { + margin-left: 1.5rem !important; + } + + .ms-lg-5 { + margin-left: 3rem !important; + } + + .ms-lg-auto { + margin-left: auto !important; + } + + .p-lg-0 { + padding: 0 !important; + } + + .p-lg-1 { + padding: 0.25rem !important; + } + + .p-lg-2 { + padding: 0.5rem !important; + } + + .p-lg-3 { + padding: 1rem !important; + } + + .p-lg-4 { + padding: 1.5rem !important; + } + + .p-lg-5 { + padding: 3rem !important; + } + + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-lg-0 { + padding-top: 0 !important; + } + + .pt-lg-1 { + padding-top: 0.25rem !important; + } + + .pt-lg-2 { + padding-top: 0.5rem !important; + } + + .pt-lg-3 { + padding-top: 1rem !important; + } + + .pt-lg-4 { + padding-top: 1.5rem !important; + } + + .pt-lg-5 { + padding-top: 3rem !important; + } + + .pe-lg-0 { + padding-right: 0 !important; + } + + .pe-lg-1 { + padding-right: 0.25rem !important; + } + + .pe-lg-2 { + padding-right: 0.5rem !important; + } + + .pe-lg-3 { + padding-right: 1rem !important; + } + + .pe-lg-4 { + padding-right: 1.5rem !important; + } + + .pe-lg-5 { + padding-right: 3rem !important; + } + + .pb-lg-0 { + padding-bottom: 0 !important; + } + + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + + .pb-lg-3 { + padding-bottom: 1rem !important; + } + + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + + .pb-lg-5 { + padding-bottom: 3rem !important; + } + + .ps-lg-0 { + padding-left: 0 !important; + } + + .ps-lg-1 { + padding-left: 0.25rem !important; + } + + .ps-lg-2 { + padding-left: 0.5rem !important; + } + + .ps-lg-3 { + padding-left: 1rem !important; + } + + .ps-lg-4 { + padding-left: 1.5rem !important; + } + + .ps-lg-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 1200px) { + .d-xl-inline { + display: inline !important; + } + + .d-xl-inline-block { + display: inline-block !important; + } + + .d-xl-block { + display: block !important; + } + + .d-xl-grid { + display: grid !important; + } + + .d-xl-table { + display: table !important; + } + + .d-xl-table-row { + display: table-row !important; + } + + .d-xl-table-cell { + display: table-cell !important; + } + + .d-xl-flex { + display: flex !important; + } + + .d-xl-inline-flex { + display: inline-flex !important; + } + + .d-xl-none { + display: none !important; + } + + .flex-xl-fill { + flex: 1 1 auto !important; + } + + .flex-xl-row { + flex-direction: row !important; + } + + .flex-xl-column { + flex-direction: column !important; + } + + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xl-wrap { + flex-wrap: wrap !important; + } + + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-xl-start { + justify-content: flex-start !important; + } + + .justify-content-xl-end { + justify-content: flex-end !important; + } + + .justify-content-xl-center { + justify-content: center !important; + } + + .justify-content-xl-between { + justify-content: space-between !important; + } + + .justify-content-xl-around { + justify-content: space-around !important; + } + + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xl-start { + align-items: flex-start !important; + } + + .align-items-xl-end { + align-items: flex-end !important; + } + + .align-items-xl-center { + align-items: center !important; + } + + .align-items-xl-baseline { + align-items: baseline !important; + } + + .align-items-xl-stretch { + align-items: stretch !important; + } + + .align-content-xl-start { + align-content: flex-start !important; + } + + .align-content-xl-end { + align-content: flex-end !important; + } + + .align-content-xl-center { + align-content: center !important; + } + + .align-content-xl-between { + align-content: space-between !important; + } + + .align-content-xl-around { + align-content: space-around !important; + } + + .align-content-xl-stretch { + align-content: stretch !important; + } + + .align-self-xl-auto { + align-self: auto !important; + } + + .align-self-xl-start { + align-self: flex-start !important; + } + + .align-self-xl-end { + align-self: flex-end !important; + } + + .align-self-xl-center { + align-self: center !important; + } + + .align-self-xl-baseline { + align-self: baseline !important; + } + + .align-self-xl-stretch { + align-self: stretch !important; + } + + .order-xl-first { + order: -1 !important; + } + + .order-xl-0 { + order: 0 !important; + } + + .order-xl-1 { + order: 1 !important; + } + + .order-xl-2 { + order: 2 !important; + } + + .order-xl-3 { + order: 3 !important; + } + + .order-xl-4 { + order: 4 !important; + } + + .order-xl-5 { + order: 5 !important; + } + + .order-xl-last { + order: 6 !important; + } + + .m-xl-0 { + margin: 0 !important; + } + + .m-xl-1 { + margin: 0.25rem !important; + } + + .m-xl-2 { + margin: 0.5rem !important; + } + + .m-xl-3 { + margin: 1rem !important; + } + + .m-xl-4 { + margin: 1.5rem !important; + } + + .m-xl-5 { + margin: 3rem !important; + } + + .m-xl-auto { + margin: auto !important; + } + + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xl-0 { + margin-top: 0 !important; + } + + .mt-xl-1 { + margin-top: 0.25rem !important; + } + + .mt-xl-2 { + margin-top: 0.5rem !important; + } + + .mt-xl-3 { + margin-top: 1rem !important; + } + + .mt-xl-4 { + margin-top: 1.5rem !important; + } + + .mt-xl-5 { + margin-top: 3rem !important; + } + + .mt-xl-auto { + margin-top: auto !important; + } + + .me-xl-0 { + margin-right: 0 !important; + } + + .me-xl-1 { + margin-right: 0.25rem !important; + } + + .me-xl-2 { + margin-right: 0.5rem !important; + } + + .me-xl-3 { + margin-right: 1rem !important; + } + + .me-xl-4 { + margin-right: 1.5rem !important; + } + + .me-xl-5 { + margin-right: 3rem !important; + } + + .me-xl-auto { + margin-right: auto !important; + } + + .mb-xl-0 { + margin-bottom: 0 !important; + } + + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xl-3 { + margin-bottom: 1rem !important; + } + + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xl-5 { + margin-bottom: 3rem !important; + } + + .mb-xl-auto { + margin-bottom: auto !important; + } + + .ms-xl-0 { + margin-left: 0 !important; + } + + .ms-xl-1 { + margin-left: 0.25rem !important; + } + + .ms-xl-2 { + margin-left: 0.5rem !important; + } + + .ms-xl-3 { + margin-left: 1rem !important; + } + + .ms-xl-4 { + margin-left: 1.5rem !important; + } + + .ms-xl-5 { + margin-left: 3rem !important; + } + + .ms-xl-auto { + margin-left: auto !important; + } + + .p-xl-0 { + padding: 0 !important; + } + + .p-xl-1 { + padding: 0.25rem !important; + } + + .p-xl-2 { + padding: 0.5rem !important; + } + + .p-xl-3 { + padding: 1rem !important; + } + + .p-xl-4 { + padding: 1.5rem !important; + } + + .p-xl-5 { + padding: 3rem !important; + } + + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xl-0 { + padding-top: 0 !important; + } + + .pt-xl-1 { + padding-top: 0.25rem !important; + } + + .pt-xl-2 { + padding-top: 0.5rem !important; + } + + .pt-xl-3 { + padding-top: 1rem !important; + } + + .pt-xl-4 { + padding-top: 1.5rem !important; + } + + .pt-xl-5 { + padding-top: 3rem !important; + } + + .pe-xl-0 { + padding-right: 0 !important; + } + + .pe-xl-1 { + padding-right: 0.25rem !important; + } + + .pe-xl-2 { + padding-right: 0.5rem !important; + } + + .pe-xl-3 { + padding-right: 1rem !important; + } + + .pe-xl-4 { + padding-right: 1.5rem !important; + } + + .pe-xl-5 { + padding-right: 3rem !important; + } + + .pb-xl-0 { + padding-bottom: 0 !important; + } + + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xl-3 { + padding-bottom: 1rem !important; + } + + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xl-5 { + padding-bottom: 3rem !important; + } + + .ps-xl-0 { + padding-left: 0 !important; + } + + .ps-xl-1 { + padding-left: 0.25rem !important; + } + + .ps-xl-2 { + padding-left: 0.5rem !important; + } + + .ps-xl-3 { + padding-left: 1rem !important; + } + + .ps-xl-4 { + padding-left: 1.5rem !important; + } + + .ps-xl-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 1400px) { + .d-xxl-inline { + display: inline !important; + } + + .d-xxl-inline-block { + display: inline-block !important; + } + + .d-xxl-block { + display: block !important; + } + + .d-xxl-grid { + display: grid !important; + } + + .d-xxl-table { + display: table !important; + } + + .d-xxl-table-row { + display: table-row !important; + } + + .d-xxl-table-cell { + display: table-cell !important; + } + + .d-xxl-flex { + display: flex !important; + } + + .d-xxl-inline-flex { + display: inline-flex !important; + } + + .d-xxl-none { + display: none !important; + } + + .flex-xxl-fill { + flex: 1 1 auto !important; + } + + .flex-xxl-row { + flex-direction: row !important; + } + + .flex-xxl-column { + flex-direction: column !important; + } + + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-xxl-start { + justify-content: flex-start !important; + } + + .justify-content-xxl-end { + justify-content: flex-end !important; + } + + .justify-content-xxl-center { + justify-content: center !important; + } + + .justify-content-xxl-between { + justify-content: space-between !important; + } + + .justify-content-xxl-around { + justify-content: space-around !important; + } + + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xxl-start { + align-items: flex-start !important; + } + + .align-items-xxl-end { + align-items: flex-end !important; + } + + .align-items-xxl-center { + align-items: center !important; + } + + .align-items-xxl-baseline { + align-items: baseline !important; + } + + .align-items-xxl-stretch { + align-items: stretch !important; + } + + .align-content-xxl-start { + align-content: flex-start !important; + } + + .align-content-xxl-end { + align-content: flex-end !important; + } + + .align-content-xxl-center { + align-content: center !important; + } + + .align-content-xxl-between { + align-content: space-between !important; + } + + .align-content-xxl-around { + align-content: space-around !important; + } + + .align-content-xxl-stretch { + align-content: stretch !important; + } + + .align-self-xxl-auto { + align-self: auto !important; + } + + .align-self-xxl-start { + align-self: flex-start !important; + } + + .align-self-xxl-end { + align-self: flex-end !important; + } + + .align-self-xxl-center { + align-self: center !important; + } + + .align-self-xxl-baseline { + align-self: baseline !important; + } + + .align-self-xxl-stretch { + align-self: stretch !important; + } + + .order-xxl-first { + order: -1 !important; + } + + .order-xxl-0 { + order: 0 !important; + } + + .order-xxl-1 { + order: 1 !important; + } + + .order-xxl-2 { + order: 2 !important; + } + + .order-xxl-3 { + order: 3 !important; + } + + .order-xxl-4 { + order: 4 !important; + } + + .order-xxl-5 { + order: 5 !important; + } + + .order-xxl-last { + order: 6 !important; + } + + .m-xxl-0 { + margin: 0 !important; + } + + .m-xxl-1 { + margin: 0.25rem !important; + } + + .m-xxl-2 { + margin: 0.5rem !important; + } + + .m-xxl-3 { + margin: 1rem !important; + } + + .m-xxl-4 { + margin: 1.5rem !important; + } + + .m-xxl-5 { + margin: 3rem !important; + } + + .m-xxl-auto { + margin: auto !important; + } + + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xxl-0 { + margin-top: 0 !important; + } + + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + + .mt-xxl-3 { + margin-top: 1rem !important; + } + + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + + .mt-xxl-5 { + margin-top: 3rem !important; + } + + .mt-xxl-auto { + margin-top: auto !important; + } + + .me-xxl-0 { + margin-right: 0 !important; + } + + .me-xxl-1 { + margin-right: 0.25rem !important; + } + + .me-xxl-2 { + margin-right: 0.5rem !important; + } + + .me-xxl-3 { + margin-right: 1rem !important; + } + + .me-xxl-4 { + margin-right: 1.5rem !important; + } + + .me-xxl-5 { + margin-right: 3rem !important; + } + + .me-xxl-auto { + margin-right: auto !important; + } + + .mb-xxl-0 { + margin-bottom: 0 !important; + } + + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + + .mb-xxl-auto { + margin-bottom: auto !important; + } + + .ms-xxl-0 { + margin-left: 0 !important; + } + + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + + .ms-xxl-3 { + margin-left: 1rem !important; + } + + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + + .ms-xxl-5 { + margin-left: 3rem !important; + } + + .ms-xxl-auto { + margin-left: auto !important; + } + + .p-xxl-0 { + padding: 0 !important; + } + + .p-xxl-1 { + padding: 0.25rem !important; + } + + .p-xxl-2 { + padding: 0.5rem !important; + } + + .p-xxl-3 { + padding: 1rem !important; + } + + .p-xxl-4 { + padding: 1.5rem !important; + } + + .p-xxl-5 { + padding: 3rem !important; + } + + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xxl-0 { + padding-top: 0 !important; + } + + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + + .pt-xxl-3 { + padding-top: 1rem !important; + } + + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + + .pt-xxl-5 { + padding-top: 3rem !important; + } + + .pe-xxl-0 { + padding-right: 0 !important; + } + + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + + .pe-xxl-3 { + padding-right: 1rem !important; + } + + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + + .pe-xxl-5 { + padding-right: 3rem !important; + } + + .pb-xxl-0 { + padding-bottom: 0 !important; + } + + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + + .ps-xxl-0 { + padding-left: 0 !important; + } + + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + + .ps-xxl-3 { + padding-left: 1rem !important; + } + + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + + .ps-xxl-5 { + padding-left: 3rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + + .d-print-inline-block { + display: inline-block !important; + } + + .d-print-block { + display: block !important; + } + + .d-print-grid { + display: grid !important; + } + + .d-print-table { + display: table !important; + } + + .d-print-table-row { + display: table-row !important; + } + + .d-print-table-cell { + display: table-cell !important; + } + + .d-print-flex { + display: flex !important; + } + + .d-print-inline-flex { + display: inline-flex !important; + } + + .d-print-none { + display: none !important; + } +} + +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map new file mode 100644 index 0000000..c006d39 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","bootstrap-grid.css","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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AAA,+BAAA;AL2uDZ;;AKlvDQ;EAOI,yBAAA;AL+uDZ;;AKtvDQ;EAOI,+BAAA;ALmvDZ;;AK1vDQ;EAOI,8BAAA;ALuvDZ;;AK9vDQ;EAOI,4BAAA;AL2vDZ;;AKlwDQ;EAOI,8BAAA;AL+vDZ;;AKtwDQ;EAOI,4BAAA;ALmwDZ;;AK1wDQ;EAOI,2BAAA;ALuwDZ;;AK9wDQ;EAOI,iCAAA;AL2wDZ;;AKlxDQ;EAOI,gCAAA;AL+wDZ;;AKtxDQ;EAOI,8BAAA;ALmxDZ;;AK1xDQ;EAOI,gCAAA;ALuxDZ;;AK9xDQ;EAOI,8BAAA;AL2xDZ;;AKlyDQ;EAOI,4BAAA;AL+xDZ;;AKtyDQ;EAOI,kCAAA;ALmyDZ;;AK1yDQ;EAOI,iCAAA;ALuyDZ;;AK9yDQ;EAOI,+BAAA;AL2yDZ;;AKlzDQ;EAOI,iCAAA;AL+yDZ;;AKtzDQ;EAOI,+BAAA;ALmzDZ;;AK1zDQ;EAOI,0BAAA;ALuzDZ;;AK9zDQ;EAOI,gCAAA;AL2zDZ;;AKl0DQ;EAOI,+BAAA;AL+zDZ;;AKt0DQ;EAOI,6BAAA;ALm0DZ;;AK10DQ;EAOI,+BAAA;ALu0DZ;;AK90DQ;EAOI,6BAAA;AL20DZ;;ACl1DI;EIAI;IAOI,0BAAA;ELg1DV;;EKv1DM;IAOI,gCAAA;ELo1DV;;EK31DM;IAOI,yBAAA;ELw1DV;;EK/1DM;IAOI,wBAAA;EL41DV;;EKn2DM;IAOI,yBAAA;ELg2DV;;EKv2DM;IAOI,6BAAA;ELo2DV;;EK32DM;IAOI,8BAAA;ELw2DV;;EK/2DM;IAOI,wBAAA;EL42DV;;EKn3DM;IAOI,+BAAA;ELg3DV;;EKv3DM;IAOI,wBAAA;ELo3DV;;EK33DM;IAOI,yBAAA;ELw3DV;;EK/3DM;IAOI,8BAAA;EL43DV;;EKn4DM;IAOI,iCAAA;ELg4DV;;EKv4DM;IAOI,sCAAA;ELo4DV;;EK34DM;IAOI,yCAAA;ELw4DV;;EK/4DM;IAOI,uBAAA;EL44DV;;EKn5DM;IAOI,uBAAA;ELg5DV;;EKv5DM;IAOI,yBAAA;ELo5DV;;EK35DM;IAOI,yBAAA;ELw5DV;;EK/5DM;IAOI,0BAAA;EL45DV;;EKn6DM;IAOI,4BAAA;ELg6DV;;EKv6DM;IAOI,kCAAA;ELo6DV;;EK36DM;IAOI,sCAAA;ELw6DV;;EK/6DM;IAOI,oCAAA;EL46DV;;EKn7DM;IAOI,kCAAA;ELg7DV;;EKv7DM;IAOI,yCAAA;ELo7DV;;EK37DM;IAOI,wCAAA;ELw7DV;;EK/7DM;IAOI,wCAAA;EL47DV;;EKn8DM;IAOI,kCAAA;ELg8DV;;EKv8DM;IAOI,gCAAA;ELo8DV;;EK38DM;IAOI,8BAAA;ELw8DV;;EK/8DM;IAOI,gCAAA;EL48DV;;EKn9DM;IAOI,+BAAA;ELg9DV;;EKv9DM;IAOI,oCAAA;ELo9DV;;EK39DM;IAOI,kCAAA;ELw9DV;;EK/9DM;IAOI,gCAAA;EL49DV;;EKn+DM;IAOI,uCAAA;ELg+DV;;EKv+DM;IAOI,sCAAA;ELo+DV;;EK3+DM;IAOI,iCAAA;ELw+DV;;EK/+DM;IAOI,2BAAA;EL4+DV;;EKn/DM;IAOI,iCAAA;ELg/DV;;EKv/DM;IAOI,+BAAA;ELo/DV;;EK3/DM;IAOI,6BAAA;ELw/DV;;EK//DM;IAOI,+BAAA;EL4/DV;;EKngEM;IAOI,8BAAA;ELggEV;;EKvgEM;IAOI,oBAAA;ELogEV;;EK3gEM;IAOI,mBAAA;ELwgEV;;EK/gEM;IAOI,mBAAA;EL4gEV;;EKnhEM;IAOI,mBAAA;ELghEV;;EKvhEM;IAOI,mBAAA;ELohEV;;EK3hEM;IAOI,mBAAA;ELwhEV;;EK/hEM;IAOI,mBAAA;EL4hEV;;EKniEM;IAOI,mBAAA;ELgiEV;;EKviEM;IAOI,oBAAA;ELoiEV;;EK3iEM;IAOI,0BAAA;ELwiEV;;EK/iEM;IAOI,yBAAA;EL4iEV;;EKnjEM;IAOI,uBAAA;ELgjEV;;EKvjEM;IAOI,yBAAA;ELojEV;;EK3jEM;IAOI,uBAAA;ELwjEV;;EK/jEM;IAOI,uBAAA;EL4jEV;;EKnkEM;IAOI,0BAAA;IAAA,yBAAA;ELikEV;;EKxkEM;IAOI,gCAAA;IAAA,+BAAA;ELskEV;;EK7kEM;IAOI,+BAAA;IAAA,8BAAA;EL2kEV;;EKllEM;IAOI,6BAAA;IAAA,4BAAA;ELglEV;;EKvlEM;IAOI,+BAAA;IAAA,8BAAA;ELqlEV;;EK5lEM;IAOI,6BAAA;IAAA,4BAAA;EL0lEV;;EKjmEM;IAOI,6BAAA;IAAA,4BAAA;EL+lEV;;EKtmEM;IAOI,wBAAA;IAAA,2BAAA;ELomEV;;EK3mEM;IAOI,8BAAA;IAAA,iCAAA;ELymEV;;EKhnEM;IAOI,6BAAA;IAAA,gCAAA;EL8mEV;;EKrnEM;IAOI,2BAAA;IAAA,8BAAA;ELmnEV;;EK1nEM;IAOI,6BAAA;IAAA,gCAAA;ELwnEV;;EK/nEM;IAOI,2BAAA;IAAA,8BAAA;EL6nEV;;EKpoEM;IAOI,2BAAA;IAAA,8BAAA;ELkoEV;;EKzoEM;IAOI,wBAAA;ELsoEV;;EK7oEM;IAOI,8BAAA;EL0oEV;;EKjpEM;IAOI,6BAAA;EL8oEV;;EKrpEM;IAOI,2BAAA;ELkpEV;;EKzpEM;IAOI,6BAAA;ELspEV;;EK7pEM;IAOI,2BAAA;EL0pEV;;EKjqEM;IAOI,2BAAA;EL8pEV;;EKrqEM;IAOI,0BAAA;ELkqEV;;EKzqEM;IAOI,gCAAA;ELsqEV;;EK7qEM;IAOI,+BAAA;EL0qEV;;EKjrEM;IAOI,6BAAA;EL8qEV;;EKrrEM;IAOI,+BAAA;ELkrEV;;EKzrEM;IAOI,6BAAA;ELsrEV;;EK7rEM;IAOI,6BAAA;EL0rEV;;EKjsEM;IAOI,2BAAA;EL8rEV;;EKrsEM;IAOI,iCAAA;ELksEV;;EKzsEM;IAOI,gCAAA;ELssEV;;EK7sEM;IAOI,8BAAA;EL0sEV;;EKjtEM;IAOI,gCAAA;EL8sEV;;EKrtEM;IAOI,8BAAA;ELktEV;;EKztEM;IAOI,8BAAA;ELstEV;;EK7tEM;IAOI,yBAAA;EL0tEV;;EKjuEM;IAOI,+BAAA;EL8tEV;;EKruEM;IAOI,8BAAA;ELkuEV;;EKzuEM;IAOI,4BAAA;ELsuEV;;EK7uEM;IAOI,8BAAA;EL0uEV;;EKjvEM;IAOI,4BAAA;EL8uEV;;EKrvEM;IAOI,4BAAA;ELkvEV;;EKzvEM;IAOI,qBAAA;ELsvEV;;EK7vEM;IAOI,2BAAA;EL0vEV;;EKjwEM;IAOI,0BAAA;EL8vEV;;EKrwEM;IAOI,wBAAA;ELkwEV;;EKzwEM;IAOI,0BAAA;ELswEV;;EK7wEM;IAOI,wBAAA;EL0wEV;;EKjxEM;IAOI,2BAAA;IAAA,0BAAA;EL+wEV;;EKtxEM;IAOI,iCAAA;IAAA,gCAAA;ELoxEV;;EK3xEM;IAOI,gCAAA;IAAA,+BAAA;ELyxEV;;EKhyEM;IAOI,8BAAA;IAAA,6BAAA;EL8xEV;;EKryEM;IAOI,gCAAA;IAAA,+BAAA;ELmyEV;;EK1yEM;IAOI,8BAAA;IAAA,6BAAA;ELwyEV;;EK/yEM;IAOI,yBAAA;IAAA,4BAAA;EL6yEV;;EKpzEM;IAOI,+BAAA;IAAA,kCAAA;ELkzEV;;EKzzEM;IAOI,8BAAA;IAAA,iCAAA;ELuzEV;;EK9zEM;IAOI,4BAAA;IAAA,+BAAA;EL4zEV;;EKn0EM;IAOI,8BAAA;IAAA,iCAAA;ELi0EV;;EKx0EM;IAOI,4BAAA;IAAA,+BAAA;ELs0EV;;EK70EM;IAOI,yBAAA;EL00EV;;EKj1EM;IAOI,+BAAA;EL80EV;;EKr1EM;IAOI,8BAAA;ELk1EV;;EKz1EM;IAOI,4BAAA;ELs1EV;;EK71EM;IAOI,8BAAA;EL01EV;;EKj2EM;IAOI,4BAAA;EL81EV;;EKr2EM;IAOI,2BAAA;ELk2EV;;EKz2EM;IAOI,iCAAA;ELs2EV;;EK72EM;IAOI,gCAAA;EL02EV;;EKj3EM;IAOI,8BAAA;EL82EV;;EKr3EM;IAOI,gCAAA;ELk3EV;;EKz3EM;IAOI,8BAAA;ELs3EV;;EK73EM;IAOI,4BAAA;EL03EV;;EKj4EM;IAOI,kCAAA;EL83EV;;EKr4EM;IAOI,iCAAA;ELk4EV;;EKz4EM;IAOI,+BAAA;ELs4EV;;EK74EM;IAOI,iCAAA;EL04EV;;EKj5EM;IAOI,+BAAA;EL84EV;;EKr5EM;IAOI,0BAAA;ELk5EV;;EKz5EM;IAOI,gCAAA;ELs5EV;;EK75EM;IAOI,+BAAA;EL05EV;;EKj6EM;IAOI,6BAAA;EL85EV;;EKr6EM;IAOI,+BAAA;ELk6EV;;EKz6EM;IAOI,6BAAA;ELs6EV;AACF;AC96EI;EIAI;IAOI,0BAAA;EL26EV;;EKl7EM;IAOI,gCAAA;EL+6EV;;EKt7EM;IAOI,yBAAA;ELm7EV;;EK17EM;IAOI,wBAAA;ELu7EV;;EK97EM;IAOI,yBAAA;EL27EV;;EKl8EM;IAOI,6BAAA;EL+7EV;;EKt8EM;IAOI,8BAAA;ELm8EV;;EK18EM;IAOI,wBAAA;ELu8EV;;EK98EM;IAOI,+BAAA;EL28EV;;EKl9EM;IAOI,wBAAA;EL+8EV;;EKt9EM;IAOI,yBAAA;ELm9EV;;EK19EM;IAOI,8BAAA;ELu9EV;;EK99EM;IAOI,iCAAA;EL29EV;;EKl+EM;IAOI,sCAAA;EL+9EV;;EKt+EM;IAOI,yCAAA;ELm+EV;;EK1+EM;IAOI,uBAAA;ELu+EV;;EK9+EM;IAOI,uBAAA;EL2+EV;;EKl/EM;IAOI,yBAAA;EL++EV;;EKt/EM;IAOI,yBAAA;ELm/EV;;EK1/EM;IAOI,0BAAA;ELu/EV;;EK9/EM;IAOI,4BAAA;EL2/EV;;EKlgFM;IAOI,kCAAA;EL+/EV;;EKtgFM;IAOI,sCAAA;ELmgFV;;EK1gFM;IAOI,oCAAA;ELugFV;;EK9gFM;IAOI,kCAAA;EL2gFV;;EKlhFM;IAOI,yCAAA;EL+gFV;;EKthFM;IAOI,wCAAA;ELmhFV;;EK1hFM;IAOI,wCAAA;ELuhFV;;EK9hFM;IAOI,kCAAA;EL2hFV;;EKliFM;IAOI,gCAAA;EL+hFV;;EKtiFM;IAOI,8BAAA;ELmiFV;;EK1iFM;IAOI,gCAAA;ELuiFV;;EK9iFM;IAOI,+BAAA;EL2iFV;;EKljFM;IAOI,oCAAA;EL+iFV;;EKtjFM;IAOI,kCAAA;ELmjFV;;EK1jFM;IAOI,gCAAA;ELujFV;;EK9jFM;IAOI,uCAAA;EL2jFV;;EKlkFM;IAOI,sCAAA;EL+jFV;;EKtkFM;IAOI,iCAAA;ELmkFV;;EK1kFM;IAOI,2BAAA;ELukFV;;EK9kFM;IAOI,iCAAA;EL2kFV;;EKllFM;IAOI,+BAAA;EL+kFV;;EKtlFM;IAOI,6BAAA;ELmlFV;;EK1lFM;IAOI,+BAAA;ELulFV;;EK9lFM;IAOI,8BAAA;EL2lFV;;EKlmFM;IAOI,oBAAA;EL+lFV;;EKtmFM;IAOI,mBAAA;ELmmFV;;EK1mFM;IAOI,mBAAA;ELumFV;;EK9mFM;IAOI,mBAAA;EL2mFV;;EKlnFM;IAOI,mBAAA;EL+mFV;;EKtnFM;IAOI,mB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FV;;EKx9FM;IAOI,4BAAA;ELq9FV;;EK59FM;IAOI,kCAAA;ELy9FV;;EKh+FM;IAOI,iCAAA;EL69FV;;EKp+FM;IAOI,+BAAA;ELi+FV;;EKx+FM;IAOI,iCAAA;ELq+FV;;EK5+FM;IAOI,+BAAA;ELy+FV;;EKh/FM;IAOI,0BAAA;EL6+FV;;EKp/FM;IAOI,gCAAA;ELi/FV;;EKx/FM;IAOI,+BAAA;ELq/FV;;EK5/FM;IAOI,6BAAA;ELy/FV;;EKhgGM;IAOI,+BAAA;EL6/FV;;EKpgGM;IAOI,6BAAA;ELigGV;AACF;ACzgGI;EIAI;IAOI,0BAAA;ELsgGV;;EK7gGM;IAOI,gCAAA;EL0gGV;;EKjhGM;IAOI,yBAAA;EL8gGV;;EKrhGM;IAOI,wBAAA;ELkhGV;;EKzhGM;IAOI,yBAAA;ELshGV;;EK7hGM;IAOI,6BAAA;EL0hGV;;EKjiGM;IAOI,8BAAA;EL8hGV;;EKriGM;IAOI,wBAAA;ELkiGV;;EKziGM;IAOI,+BAAA;ELsiGV;;EK7iGM;IAOI,wBAAA;EL0iGV;;EKjjGM;IAOI,yBAAA;EL8iGV;;EKrjGM;IAOI,8BAAA;ELkjGV;;EKzjGM;IAOI,iCAAA;ELsjGV;;EK7jGM;IAOI,sCAAA;EL0jGV;;EKjkGM;IAOI,yCAAA;EL8jGV;;EKrkGM;IAOI,uBAAA;ELkkGV;;EKzkGM;IAOI,uBAAA;ELskGV;;EK7kGM;IAOI,yBAAA;EL0kGV;;EKjlGM;IAOI,yBAAA;EL8kGV;;EKrlGM;IAOI,0BAAA;ELklGV;;EKzlGM;IAOI,4BAAA;ELslGV;;EK7lGM;IAOI,kCAAA;EL0lGV;;EKjmGM;IAOI,sCAAA;EL8lGV;;EKrmGM;IAOI,oCAAA;ELkmGV;;EKzmGM;IAOI,kCAAA;ELsmGV;;EK7mGM;IAOI,yCAAA;EL0mGV;;EKjnGM;IAOI,wCAAA;EL8mGV;;EKrnGM;IAOI,wCAAA;ELknGV;;EKznGM;IAOI,kCAAA;ELsnGV;;EK7nGM;IAOI,gCAAA;EL0nGV;;EKjoGM;IAOI,8BAAA;EL8nGV;;EKroGM;IAOI,gCAAA;ELkoGV;;EKzoGM;IAOI,+BAAA;ELsoGV;;EK7oGM;IAOI,oCAAA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/rII;EIAI;IAOI,0BAAA;EL4rIV;;EKnsIM;IAOI,gCAAA;ELgsIV;;EKvsIM;IAOI,yBAAA;ELosIV;;EK3sIM;IAOI,wBAAA;ELwsIV;;EK/sIM;IAOI,yBAAA;EL4sIV;;EKntIM;IAOI,6BAAA;ELgtIV;;EKvtIM;IAOI,8BAAA;ELotIV;;EK3tIM;IAOI,wBAAA;ELwtIV;;EK/tIM;IAOI,+BAAA;EL4tIV;;EKnuIM;IAOI,wBAAA;ELguIV;;EKvuIM;IAOI,yBAAA;ELouIV;;EK3uIM;IAOI,8BAAA;ELwuIV;;EK/uIM;IAOI,iCAAA;EL4uIV;;EKnvIM;IAOI,sCAAA;ELgvIV;;EKvvIM;IAOI,yCAAA;ELovIV;;EK3vIM;IAOI,uBAAA;ELwvIV;;EK/vIM;IAOI,uBAAA;EL4vIV;;EKnwIM;IAOI,yBAAA;ELgwIV;;EKvwIM;IAOI,yBAAA;ELowIV;;EK3wIM;IAOI,0BAAA;ELwwIV;;EK/wIM;IAOI,4BAAA;EL4wIV;;EKnxIM;IAOI,kCAAA;ELgxIV;;EKvxIM;IAOI,sCAAA;ELoxIV;;EK3xIM;IAOI,oCAAA;ELwxIV;;EK/xIM;IAOI,kCAAA;EL4xIV;;EKnyIM;IAOI,yCAAA;ELgyIV;;EKvyIM;IAOI,wCAAA;ELoyIV;;EK3yIM;IAOI,wCAAA;ELwyIV;;EK/yIM;IAOI,kCAAA;EL4yIV;;EKnzIM;IAOI,gCAAA;ELgzIV;;EKvzIM;IAOI,8BAAA;ELozIV;;EK3zIM;IAOI,gCAAA;ELwzIV;;EK/zIM;IAOI,+BAAA;EL4zIV;;EKn0IM;IAOI,oCAAA;ELg0IV;;EKv0IM;IAOI,kCAAA;ELo0IV;;EK30IM;IAOI,gCAAA;ELw0IV;;EK/0IM;IAOI,uCAAA;EL40IV;;EKn1IM;IAOI,sCAAA;ELg1IV;;EKv1IM;IAOI,iCAAA;ELo1IV;;EK31IM;IAOI,2BAAA;ELw1IV;;EK/1IM;IAOI,iCAAA;EL41IV;;EKn2IM;IAOI,+BAAA;ELg2IV;;EKv2IM;IAOI,6BAAA;ELo2IV;;EK32IM;IAOI,+BAAA;ELw2IV;;EK/2IM;IAOI,8BAAA;EL42IV;;EKn3IM;IAOI,oBAAA;ELg3IV;;E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OI,iCAAA;ELktJV;;EKztJM;IAOI,gCAAA;ELstJV;;EK7tJM;IAOI,8BAAA;EL0tJV;;EKjuJM;IAOI,gCAAA;EL8tJV;;EKruJM;IAOI,8BAAA;ELkuJV;;EKzuJM;IAOI,4BAAA;ELsuJV;;EK7uJM;IAOI,kCAAA;EL0uJV;;EKjvJM;IAOI,iCAAA;EL8uJV;;EKrvJM;IAOI,+BAAA;ELkvJV;;EKzvJM;IAOI,iCAAA;ELsvJV;;EK7vJM;IAOI,+BAAA;EL0vJV;;EKjwJM;IAOI,0BAAA;EL8vJV;;EKrwJM;IAOI,gCAAA;ELkwJV;;EKzwJM;IAOI,+BAAA;ELswJV;;EK7wJM;IAOI,6BAAA;EL0wJV;;EKjxJM;IAOI,+BAAA;EL8wJV;;EKrxJM;IAOI,6BAAA;ELkxJV;AACF;AMnzJA;EDyBQ;IAOI,0BAAA;ELuxJV;;EK9xJM;IAOI,gCAAA;EL2xJV;;EKlyJM;IAOI,yBAAA;EL+xJV;;EKtyJM;IAOI,wBAAA;ELmyJV;;EK1yJM;IAOI,yBAAA;ELuyJV;;EK9yJM;IAOI,6BAAA;EL2yJV;;EKlzJM;IAOI,8BAAA;EL+yJV;;EKtzJM;IAOI,wBAAA;ELmzJV;;EK1zJM;IAOI,+BAAA;ELuzJV;;EK9zJM;IAOI,wBAAA;EL2zJV;AACF","file":"bootstrap-grid.css","sourcesContent":["/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n$include-column-box-sizing: true !default;\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/lists\";\n@import \"mixins/breakpoints\";\n@import \"mixins/container\";\n@import \"mixins/grid\";\n@import \"mixins/utilities\";\n\n@import \"vendor/rfs\";\n\n@import \"containers\";\n@import \"grid\";\n\n@import \"utilities\";\n// Only use the utilities we need\n// stylelint-disable-next-line scss/dollar-variable-default\n$utilities: map-get-multiple(\n $utilities,\n (\n \"display\",\n \"order\",\n \"flex\",\n \"flex-direction\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-wrap\",\n \"justify-content\",\n \"align-items\",\n \"align-content\",\n \"align-self\",\n \"margin\",\n \"margin-x\",\n \"margin-y\",\n \"margin-top\",\n \"margin-end\",\n \"margin-bottom\",\n \"margin-start\",\n \"negative-margin\",\n \"negative-margin-x\",\n \"negative-margin-y\",\n \"negative-margin-top\",\n \"negative-margin-end\",\n \"negative-margin-bottom\",\n \"negative-margin-start\",\n \"padding\",\n \"padding-x\",\n \"padding-y\",\n \"padding-top\",\n \"padding-end\",\n \"padding-bottom\",\n \"padding-start\",\n )\n);\n\n@import \"utilities/api\";\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n width: 100%;\n padding-right: var(--#{$variable-prefix}gutter-x, #{$gutter});\n padding-left: var(--#{$variable-prefix}gutter-x, #{$gutter});\n margin-right: auto;\n margin-left: auto;\n}\n","/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * .5);\n padding-left: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n// scss-docs-start gray-color-variables\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n// scss-docs-end gray-color-variables\n\n// fusv-disable\n// scss-docs-start gray-colors-map\n$grays: (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n) !default;\n// scss-docs-end gray-colors-map\n// fusv-enable\n\n// scss-docs-start color-variables\n$blue: #0d6efd !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #d63384 !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #198754 !default;\n$teal: #20c997 !default;\n$cyan: #0dcaf0 !default;\n// scss-docs-end color-variables\n\n// scss-docs-start colors-map\n$colors: (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n) !default;\n// scss-docs-end colors-map\n\n// scss-docs-start theme-color-variables\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-900 !default;\n// scss-docs-end theme-color-variables\n\n// scss-docs-start theme-colors-map\n$theme-colors: (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n) !default;\n// scss-docs-end theme-colors-map\n\n// scss-docs-start theme-colors-rgb\n$theme-colors-rgb: map-loop($theme-colors, to-rgb, \"$value\") !default;\n// scss-docs-end theme-colors-rgb\n\n// The contrast ratio to reach against white, to determine if color changes from \"light\" to \"dark\". Acceptable values for WCAG 2.0 are 3, 4.5 and 7.\n// See https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast\n$min-contrast-ratio: 4.5 !default;\n\n// Customize the light and dark text colors for use in our color contrast function.\n$color-contrast-dark: $black !default;\n$color-contrast-light: $white !default;\n\n// fusv-disable\n$blue-100: tint-color($blue, 80%) !default;\n$blue-200: tint-color($blue, 60%) !default;\n$blue-300: tint-color($blue, 40%) !default;\n$blue-400: tint-color($blue, 20%) !default;\n$blue-500: $blue !default;\n$blue-600: shade-color($blue, 20%) !default;\n$blue-700: shade-color($blue, 40%) !default;\n$blue-800: shade-color($blue, 60%) !default;\n$blue-900: shade-color($blue, 80%) !default;\n\n$indigo-100: tint-color($indigo, 80%) !default;\n$indigo-200: tint-color($indigo, 60%) !default;\n$indigo-300: tint-color($indigo, 40%) !default;\n$indigo-400: tint-color($indigo, 20%) !default;\n$indigo-500: $indigo !default;\n$indigo-600: shade-color($indigo, 20%) !default;\n$indigo-700: shade-color($indigo, 40%) !default;\n$indigo-800: shade-color($indigo, 60%) !default;\n$indigo-900: shade-color($indigo, 80%) !default;\n\n$purple-100: tint-color($purple, 80%) !default;\n$purple-200: tint-color($purple, 60%) !default;\n$purple-300: tint-color($purple, 40%) !default;\n$purple-400: tint-color($purple, 20%) !default;\n$purple-500: $purple !default;\n$purple-600: shade-color($purple, 20%) !default;\n$purple-700: shade-color($purple, 40%) !default;\n$purple-800: shade-color($purple, 60%) !default;\n$purple-900: shade-color($purple, 80%) !default;\n\n$pink-100: tint-color($pink, 80%) !default;\n$pink-200: tint-color($pink, 60%) !default;\n$pink-300: tint-color($pink, 40%) !default;\n$pink-400: tint-color($pink, 20%) !default;\n$pink-500: $pink !default;\n$pink-600: shade-color($pink, 20%) !default;\n$pink-700: shade-color($pink, 40%) !default;\n$pink-800: shade-color($pink, 60%) !default;\n$pink-900: shade-color($pink, 80%) !default;\n\n$red-100: tint-color($red, 80%) !default;\n$red-200: tint-color($red, 60%) !default;\n$red-300: tint-color($red, 40%) !default;\n$red-400: tint-color($red, 20%) !default;\n$red-500: $red !default;\n$red-600: shade-color($red, 20%) !default;\n$red-700: shade-color($red, 40%) !default;\n$red-800: shade-color($red, 60%) !default;\n$red-900: shade-color($red, 80%) !default;\n\n$orange-100: tint-color($orange, 80%) !default;\n$orange-200: tint-color($orange, 60%) !default;\n$orange-300: tint-color($orange, 40%) !default;\n$orange-400: tint-color($orange, 20%) !default;\n$orange-500: $orange !default;\n$orange-600: shade-color($orange, 20%) !default;\n$orange-700: shade-color($orange, 40%) !default;\n$orange-800: shade-color($orange, 60%) !default;\n$orange-900: shade-color($orange, 80%) !default;\n\n$yellow-100: tint-color($yellow, 80%) !default;\n$yellow-200: tint-color($yellow, 60%) !default;\n$yellow-300: tint-color($yellow, 40%) !default;\n$yellow-400: tint-color($yellow, 20%) !default;\n$yellow-500: $yellow !default;\n$yellow-600: shade-color($yellow, 20%) !default;\n$yellow-700: shade-color($yellow, 40%) !default;\n$yellow-800: shade-color($yellow, 60%) !default;\n$yellow-900: shade-color($yellow, 80%) !default;\n\n$green-100: tint-color($green, 80%) !default;\n$green-200: tint-color($green, 60%) !default;\n$green-300: tint-color($green, 40%) !default;\n$green-400: tint-color($green, 20%) !default;\n$green-500: $green !default;\n$green-600: shade-color($green, 20%) !default;\n$green-700: shade-color($green, 40%) !default;\n$green-800: shade-color($green, 60%) !default;\n$green-900: shade-color($green, 80%) !default;\n\n$teal-100: tint-color($teal, 80%) !default;\n$teal-200: tint-color($teal, 60%) !default;\n$teal-300: tint-color($teal, 40%) !default;\n$teal-400: tint-color($teal, 20%) !default;\n$teal-500: $teal !default;\n$teal-600: shade-color($teal, 20%) !default;\n$teal-700: shade-color($teal, 40%) !default;\n$teal-800: shade-color($teal, 60%) !default;\n$teal-900: shade-color($teal, 80%) !default;\n\n$cyan-100: tint-color($cyan, 80%) !default;\n$cyan-200: tint-color($cyan, 60%) !default;\n$cyan-300: tint-color($cyan, 40%) !default;\n$cyan-400: tint-color($cyan, 20%) !default;\n$cyan-500: $cyan !default;\n$cyan-600: shade-color($cyan, 20%) !default;\n$cyan-700: shade-color($cyan, 40%) !default;\n$cyan-800: shade-color($cyan, 60%) !default;\n$cyan-900: shade-color($cyan, 80%) !default;\n\n$blues: (\n \"blue-100\": $blue-100,\n \"blue-200\": $blue-200,\n \"blue-300\": $blue-300,\n \"blue-400\": $blue-400,\n \"blue-500\": $blue-500,\n \"blue-600\": $blue-600,\n \"blue-700\": $blue-700,\n \"blue-800\": $blue-800,\n \"blue-900\": $blue-900\n) !default;\n\n$indigos: (\n \"indigo-100\": $indigo-100,\n \"indigo-200\": $indigo-200,\n \"indigo-300\": $indigo-300,\n \"indigo-400\": $indigo-400,\n \"indigo-500\": $indigo-500,\n \"indigo-600\": $indigo-600,\n \"indigo-700\": $indigo-700,\n \"indigo-800\": $indigo-800,\n \"indigo-900\": $indigo-900\n) !default;\n\n$purples: (\n \"purple-100\": $purple-200,\n \"purple-200\": $purple-100,\n \"purple-300\": $purple-300,\n \"purple-400\": $purple-400,\n \"purple-500\": $purple-500,\n \"purple-600\": $purple-600,\n \"purple-700\": $purple-700,\n \"purple-800\": $purple-800,\n \"purple-900\": $purple-900\n) !default;\n\n$pinks: (\n \"pink-100\": $pink-100,\n \"pink-200\": $pink-200,\n \"pink-300\": $pink-300,\n \"pink-400\": $pink-400,\n \"pink-500\": $pink-500,\n \"pink-600\": $pink-600,\n \"pink-700\": $pink-700,\n \"pink-800\": $pink-800,\n \"pink-900\": $pink-900\n) !default;\n\n$reds: (\n \"red-100\": $red-100,\n \"red-200\": $red-200,\n \"red-300\": $red-300,\n \"red-400\": $red-400,\n \"red-500\": $red-500,\n \"red-600\": $red-600,\n \"red-700\": $red-700,\n \"red-800\": $red-800,\n \"red-900\": $red-900\n) !default;\n\n$oranges: (\n \"orange-100\": $orange-100,\n \"orange-200\": $orange-200,\n \"orange-300\": $orange-300,\n \"orange-400\": $orange-400,\n \"orange-500\": $orange-500,\n \"orange-600\": $orange-600,\n \"orange-700\": $orange-700,\n \"orange-800\": $orange-800,\n \"orange-900\": $orange-900\n) !default;\n\n$yellows: (\n \"yellow-100\": $yellow-100,\n \"yellow-200\": $yellow-200,\n \"yellow-300\": $yellow-300,\n \"yellow-400\": $yellow-400,\n \"yellow-500\": $yellow-500,\n \"yellow-600\": $yellow-600,\n \"yellow-700\": $yellow-700,\n \"yellow-800\": $yellow-800,\n \"yellow-900\": $yellow-900\n) !default;\n\n$greens: (\n \"green-100\": $green-100,\n \"green-200\": $green-200,\n \"green-300\": $green-300,\n \"green-400\": $green-400,\n \"green-500\": $green-500,\n \"green-600\": $green-600,\n \"green-700\": $green-700,\n \"green-800\": $green-800,\n \"green-900\": $green-900\n) !default;\n\n$teals: (\n \"teal-100\": $teal-100,\n \"teal-200\": $teal-200,\n \"teal-300\": $teal-300,\n \"teal-400\": $teal-400,\n \"teal-500\": $teal-500,\n \"teal-600\": $teal-600,\n \"teal-700\": $teal-700,\n \"teal-800\": $teal-800,\n \"teal-900\": $teal-900\n) !default;\n\n$cyans: (\n \"cyan-100\": $cyan-100,\n \"cyan-200\": $cyan-200,\n \"cyan-300\": $cyan-300,\n \"cyan-400\": $cyan-400,\n \"cyan-500\": $cyan-500,\n \"cyan-600\": $cyan-600,\n \"cyan-700\": $cyan-700,\n \"cyan-800\": $cyan-800,\n \"cyan-900\": $cyan-900\n) !default;\n// fusv-enable\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-reduced-motion: true !default;\n$enable-smooth-scroll: true !default;\n$enable-grid-classes: true !default;\n$enable-cssgrid: false !default;\n$enable-button-pointers: true !default;\n$enable-rfs: true !default;\n$enable-validation-icons: true !default;\n$enable-negative-margins: false !default;\n$enable-deprecation-messages: true !default;\n$enable-important-utilities: true !default;\n\n// Prefix for :root CSS variables\n\n$variable-prefix: bs- !default;\n\n// Gradient\n//\n// The gradient which is added to components if `$enable-gradients` is `true`\n// This gradient is also added to elements with `.bg-gradient`\n// scss-docs-start variable-gradient\n$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default;\n// scss-docs-end variable-gradient\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n// scss-docs-start spacer-variables-maps\n$spacer: 1rem !default;\n$spacers: (\n 0: 0,\n 1: $spacer * .25,\n 2: $spacer * .5,\n 3: $spacer,\n 4: $spacer * 1.5,\n 5: $spacer * 3,\n) !default;\n\n$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default;\n// scss-docs-end spacer-variables-maps\n\n// Position\n//\n// Define the edge positioning anchors of the position utilities.\n\n// scss-docs-start position-map\n$position-values: (\n 0: 0,\n 50: 50%,\n 100: 100%\n) !default;\n// scss-docs-end position-map\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n$body-text-align: null !default;\n\n// Utilities maps\n//\n// Extends the default `$theme-colors` maps to help create our utilities.\n\n// scss-docs-start utilities-colors\n$utilities-colors: map-merge(\n $theme-colors-rgb,\n (\n \"black\": to-rgb($black),\n \"white\": to-rgb($white),\n \"body\": to-rgb($body-color)\n )\n) !default;\n// scss-docs-end utilities-colors\n\n// scss-docs-start utilities-text-colors\n$utilities-text-colors: map-loop($utilities-colors, rgba-css-var, \"$key\", \"text\") !default;\n// scss-docs-end utilities-text-colors\n\n// scss-docs-start utilities-bg-colors\n$utilities-bg-colors: map-loop($utilities-colors, rgba-css-var, \"$key\", \"bg\") !default;\n// scss-docs-end utilities-bg-colors\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: $primary !default;\n$link-decoration: underline !default;\n$link-shade-percentage: 20% !default;\n$link-hover-color: shift-color($link-color, $link-shade-percentage) !default;\n$link-hover-decoration: null !default;\n\n$stretched-link-pseudo-element: after !default;\n$stretched-link-z-index: 1 !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n// scss-docs-start grid-breakpoints\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px,\n xxl: 1400px\n) !default;\n// scss-docs-end grid-breakpoints\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n// scss-docs-start container-max-widths\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px,\n xxl: 1320px\n) !default;\n// scss-docs-end container-max-widths\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 1.5rem !default;\n$grid-row-columns: 6 !default;\n\n$gutters: $spacers !default;\n\n// Container padding\n\n$container-padding-x: $grid-gutter-width * .5 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n// scss-docs-start border-variables\n$border-width: 1px !default;\n$border-widths: (\n 1: 1px,\n 2: 2px,\n 3: 3px,\n 4: 4px,\n 5: 5px\n) !default;\n\n$border-color: $gray-300 !default;\n// scss-docs-end border-variables\n\n// scss-docs-start border-radius-variables\n$border-radius: .25rem !default;\n$border-radius-sm: .2rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-pill: 50rem !default;\n// scss-docs-end border-radius-variables\n\n// scss-docs-start box-shadow-variables\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default;\n// scss-docs-end box-shadow-variables\n\n$component-active-color: $white !default;\n$component-active-bg: $primary !default;\n\n// scss-docs-start caret-variables\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n// scss-docs-end caret-variables\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n// scss-docs-start collapse-transition\n$transition-collapse: height .35s ease !default;\n$transition-collapse-width: width .35s ease !default;\n// scss-docs-end collapse-transition\n\n// stylelint-disable function-disallowed-list\n// scss-docs-start aspect-ratios\n$aspect-ratios: (\n \"1x1\": 100%,\n \"4x3\": calc(3 / 4 * 100%),\n \"16x9\": calc(9 / 16 * 100%),\n \"21x9\": calc(9 / 21 * 100%)\n) !default;\n// scss-docs-end aspect-ratios\n// stylelint-enable function-disallowed-list\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// scss-docs-start font-variables\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Liberation Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n// stylelint-enable value-keyword-case\n$font-family-base: var(--#{$variable-prefix}font-sans-serif) !default;\n$font-family-code: var(--#{$variable-prefix}font-monospace) !default;\n\n// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins\n// $font-size-base affects the font size of the body text\n$font-size-root: null !default;\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-sm: $font-size-base * .875 !default;\n$font-size-lg: $font-size-base * 1.25 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n\n$line-height-base: 1.5 !default;\n$line-height-sm: 1.25 !default;\n$line-height-lg: 2 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n// scss-docs-end font-variables\n\n// scss-docs-start font-sizes\n$font-sizes: (\n 1: $h1-font-size,\n 2: $h2-font-size,\n 3: $h3-font-size,\n 4: $h4-font-size,\n 5: $h5-font-size,\n 6: $h6-font-size\n) !default;\n// scss-docs-end font-sizes\n\n// scss-docs-start headings-variables\n$headings-margin-bottom: $spacer * .5 !default;\n$headings-font-family: null !default;\n$headings-font-style: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n// scss-docs-end headings-variables\n\n// scss-docs-start display-headings\n$display-font-sizes: (\n 1: 5rem,\n 2: 4.5rem,\n 3: 4rem,\n 4: 3.5rem,\n 5: 3rem,\n 6: 2.5rem\n) !default;\n\n$display-font-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n// scss-docs-end display-headings\n\n// scss-docs-start type-variables\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: .875em !default;\n\n$sub-sup-font-size: .75em !default;\n\n$text-muted: $gray-600 !default;\n\n$initialism-font-size: $small-font-size !default;\n\n$blockquote-margin-y: $spacer !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n$blockquote-footer-color: $gray-600 !default;\n$blockquote-footer-font-size: $small-font-size !default;\n\n$hr-margin-y: $spacer !default;\n$hr-color: inherit !default;\n$hr-height: $border-width !default;\n$hr-opacity: .25 !default;\n\n$legend-margin-bottom: .5rem !default;\n$legend-font-size: 1.5rem !default;\n$legend-font-weight: null !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n// scss-docs-end type-variables\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n// scss-docs-start table-variables\n$table-cell-padding-y: .5rem !default;\n$table-cell-padding-x: .5rem !default;\n$table-cell-padding-y-sm: .25rem !default;\n$table-cell-padding-x-sm: .25rem !default;\n\n$table-cell-vertical-align: top !default;\n\n$table-color: $body-color !default;\n$table-bg: transparent !default;\n$table-accent-bg: transparent !default;\n\n$table-th-font-weight: null !default;\n\n$table-striped-color: $table-color !default;\n$table-striped-bg-factor: .05 !default;\n$table-striped-bg: rgba($black, $table-striped-bg-factor) !default;\n\n$table-active-color: $table-color !default;\n$table-active-bg-factor: .1 !default;\n$table-active-bg: rgba($black, $table-active-bg-factor) !default;\n\n$table-hover-color: $table-color !default;\n$table-hover-bg-factor: .075 !default;\n$table-hover-bg: rgba($black, $table-hover-bg-factor) !default;\n\n$table-border-factor: .1 !default;\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-striped-order: odd !default;\n\n$table-group-separator-color: currentColor !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-scale: -80% !default;\n// scss-docs-end table-variables\n\n// scss-docs-start table-loop\n$table-variants: (\n \"primary\": shift-color($primary, $table-bg-scale),\n \"secondary\": shift-color($secondary, $table-bg-scale),\n \"success\": shift-color($success, $table-bg-scale),\n \"info\": shift-color($info, $table-bg-scale),\n \"warning\": shift-color($warning, $table-bg-scale),\n \"danger\": shift-color($danger, $table-bg-scale),\n \"light\": $light,\n \"dark\": $dark,\n) !default;\n// scss-docs-end table-loop\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n// scss-docs-start input-btn-variables\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .25rem !default;\n$input-btn-focus-color-opacity: .25 !default;\n$input-btn-focus-color: rgba($component-active-bg, $input-btn-focus-color-opacity) !default;\n$input-btn-focus-blur: 0 !default;\n$input-btn-focus-box-shadow: 0 0 $input-btn-focus-blur $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n\n$input-btn-border-width: $border-width !default;\n// scss-docs-end input-btn-variables\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n// scss-docs-start btn-variables\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-color: $link-color !default;\n$btn-link-hover-color: $link-hover-color !default;\n$btn-link-disabled-color: $gray-600 !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$btn-hover-bg-shade-amount: 15% !default;\n$btn-hover-bg-tint-amount: 15% !default;\n$btn-hover-border-shade-amount: 20% !default;\n$btn-hover-border-tint-amount: 10% !default;\n$btn-active-bg-shade-amount: 20% !default;\n$btn-active-bg-tint-amount: 20% !default;\n$btn-active-border-shade-amount: 25% !default;\n$btn-active-border-tint-amount: 10% !default;\n// scss-docs-end btn-variables\n\n\n// Forms\n\n// scss-docs-start form-text-variables\n$form-text-margin-top: .25rem !default;\n$form-text-font-size: $small-font-size !default;\n$form-text-font-style: null !default;\n$form-text-font-weight: null !default;\n$form-text-color: $text-muted !default;\n// scss-docs-end form-text-variables\n\n// scss-docs-start form-label-variables\n$form-label-margin-bottom: .5rem !default;\n$form-label-font-size: null !default;\n$form-label-font-style: null !default;\n$form-label-font-weight: null !default;\n$form-label-color: null !default;\n// scss-docs-end form-label-variables\n\n// scss-docs-start form-input-variables\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n\n$input-bg: $body-bg !default;\n$input-disabled-bg: $gray-200 !default;\n$input-disabled-border-color: null !default;\n\n$input-color: $body-color !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: $box-shadow-inset !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-sm: $border-radius-sm !default;\n$input-border-radius-lg: $border-radius-lg !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: tint-color($component-active-bg, 50%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-color-width: 3rem !default;\n// scss-docs-end form-input-variables\n\n// scss-docs-start form-check-variables\n$form-check-input-width: 1em !default;\n$form-check-min-height: $font-size-base * $line-height-base !default;\n$form-check-padding-start: $form-check-input-width + .5em !default;\n$form-check-margin-bottom: .125rem !default;\n$form-check-label-color: null !default;\n$form-check-label-cursor: null !default;\n$form-check-transition: null !default;\n\n$form-check-input-active-filter: brightness(90%) !default;\n\n$form-check-input-bg: $input-bg !default;\n$form-check-input-border: 1px solid rgba($black, .25) !default;\n$form-check-input-border-radius: .25em !default;\n$form-check-radio-border-radius: 50% !default;\n$form-check-input-focus-border: $input-focus-border-color !default;\n$form-check-input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$form-check-input-checked-color: $component-active-color !default;\n$form-check-input-checked-bg-color: $component-active-bg !default;\n$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default;\n$form-check-input-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-check-radio-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-indeterminate-color: $component-active-color !default;\n$form-check-input-indeterminate-bg-color: $component-active-bg !default;\n$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default;\n$form-check-input-indeterminate-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-disabled-opacity: .5 !default;\n$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default;\n$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default;\n\n$form-check-inline-margin-end: 1rem !default;\n// scss-docs-end form-check-variables\n\n// scss-docs-start form-switch-variables\n$form-switch-color: rgba(0, 0, 0, .25) !default;\n$form-switch-width: 2em !default;\n$form-switch-padding-start: $form-switch-width + .5em !default;\n$form-switch-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-border-radius: $form-switch-width !default;\n$form-switch-transition: background-position .15s ease-in-out !default;\n\n$form-switch-focus-color: $input-focus-border-color !default;\n$form-switch-focus-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-switch-checked-color: $component-active-color !default;\n$form-switch-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-checked-bg-position: right center !default;\n// scss-docs-end form-switch-variables\n\n// scss-docs-start input-group-variables\n$input-group-addon-padding-y: $input-padding-y !default;\n$input-group-addon-padding-x: $input-padding-x !default;\n$input-group-addon-font-weight: $input-font-weight !default;\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n// scss-docs-end input-group-variables\n\n// scss-docs-start form-select-variables\n$form-select-padding-y: $input-padding-y !default;\n$form-select-padding-x: $input-padding-x !default;\n$form-select-font-family: $input-font-family !default;\n$form-select-font-size: $input-font-size !default;\n$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image\n$form-select-font-weight: $input-font-weight !default;\n$form-select-line-height: $input-line-height !default;\n$form-select-color: $input-color !default;\n$form-select-bg: $input-bg !default;\n$form-select-disabled-color: null !default;\n$form-select-disabled-bg: $gray-200 !default;\n$form-select-disabled-border-color: $input-disabled-border-color !default;\n$form-select-bg-position: right $form-select-padding-x center !default;\n$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions\n$form-select-indicator-color: $gray-800 !default;\n$form-select-indicator: url(\"data:image/svg+xml,\") !default;\n\n$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default;\n$form-select-feedback-icon-position: center right $form-select-indicator-padding !default;\n$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$form-select-border-width: $input-border-width !default;\n$form-select-border-color: $input-border-color !default;\n$form-select-border-radius: $border-radius !default;\n$form-select-box-shadow: $box-shadow-inset !default;\n\n$form-select-focus-border-color: $input-focus-border-color !default;\n$form-select-focus-width: $input-focus-width !default;\n$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default;\n\n$form-select-padding-y-sm: $input-padding-y-sm !default;\n$form-select-padding-x-sm: $input-padding-x-sm !default;\n$form-select-font-size-sm: $input-font-size-sm !default;\n\n$form-select-padding-y-lg: $input-padding-y-lg !default;\n$form-select-padding-x-lg: $input-padding-x-lg !default;\n$form-select-font-size-lg: $input-font-size-lg !default;\n\n$form-select-transition: $input-transition !default;\n// scss-docs-end form-select-variables\n\n// scss-docs-start form-range-variables\n$form-range-track-width: 100% !default;\n$form-range-track-height: .5rem !default;\n$form-range-track-cursor: pointer !default;\n$form-range-track-bg: $gray-300 !default;\n$form-range-track-border-radius: 1rem !default;\n$form-range-track-box-shadow: $box-shadow-inset !default;\n\n$form-range-thumb-width: 1rem !default;\n$form-range-thumb-height: $form-range-thumb-width !default;\n$form-range-thumb-bg: $component-active-bg !default;\n$form-range-thumb-border: 0 !default;\n$form-range-thumb-border-radius: 1rem !default;\n$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge\n$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default;\n$form-range-thumb-disabled-bg: $gray-500 !default;\n$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n// scss-docs-end form-range-variables\n\n// scss-docs-start form-file-variables\n$form-file-button-color: $input-color !default;\n$form-file-button-bg: $input-group-addon-bg !default;\n$form-file-button-hover-bg: shade-color($form-file-button-bg, 5%) !default;\n// scss-docs-end form-file-variables\n\n// scss-docs-start form-floating-variables\n$form-floating-height: add(3.5rem, $input-height-border) !default;\n$form-floating-line-height: 1.25 !default;\n$form-floating-padding-x: $input-padding-x !default;\n$form-floating-padding-y: 1rem !default;\n$form-floating-input-padding-t: 1.625rem !default;\n$form-floating-input-padding-b: .625rem !default;\n$form-floating-label-opacity: .65 !default;\n$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default;\n$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default;\n// scss-docs-end form-floating-variables\n\n// Form validation\n\n// scss-docs-start form-feedback-variables\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $form-text-font-size !default;\n$form-feedback-font-style: $form-text-font-style !default;\n$form-feedback-valid-color: $success !default;\n$form-feedback-invalid-color: $danger !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end form-feedback-variables\n\n// scss-docs-start form-validation-states\n$form-validation-states: (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n )\n) !default;\n// scss-docs-end form-validation-states\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n// scss-docs-start zindex-stack\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-offcanvas-backdrop: 1040 !default;\n$zindex-offcanvas: 1045 !default;\n$zindex-modal-backdrop: 1050 !default;\n$zindex-modal: 1055 !default;\n$zindex-popover: 1070 !default;\n$zindex-tooltip: 1080 !default;\n// scss-docs-end zindex-stack\n\n\n// Navs\n\n// scss-docs-start nav-variables\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-font-size: null !default;\n$nav-link-font-weight: null !default;\n$nav-link-color: $link-color !default;\n$nav-link-hover-color: $link-hover-color !default;\n$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n// scss-docs-end nav-variables\n\n\n// Navbar\n\n// scss-docs-start navbar-variables\n$navbar-padding-y: $spacer * .5 !default;\n$navbar-padding-x: null !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default;\n$navbar-brand-margin-end: 1rem !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n$navbar-toggler-focus-width: $btn-focus-width !default;\n$navbar-toggler-transition: box-shadow .15s ease-in-out !default;\n// scss-docs-end navbar-variables\n\n// scss-docs-start navbar-theme-variables\n$navbar-dark-color: rgba($white, .55) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .55) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n// scss-docs-end navbar-theme-variables\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n// scss-docs-start dropdown-variables\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $dropdown-border-color !default;\n$dropdown-divider-margin-y: $spacer * .5 !default;\n$dropdown-box-shadow: $box-shadow !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: shade-color($gray-900, 10%) !default;\n$dropdown-link-hover-bg: $gray-200 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-500 !default;\n\n$dropdown-item-padding-y: $spacer * .25 !default;\n$dropdown-item-padding-x: $spacer !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding: $dropdown-padding-y $dropdown-item-padding-x !default;\n// scss-docs-end dropdown-variables\n\n// scss-docs-start dropdown-dark-variables\n$dropdown-dark-color: $gray-300 !default;\n$dropdown-dark-bg: $gray-800 !default;\n$dropdown-dark-border-color: $dropdown-border-color !default;\n$dropdown-dark-divider-bg: $dropdown-divider-bg !default;\n$dropdown-dark-box-shadow: null !default;\n$dropdown-dark-link-color: $dropdown-dark-color !default;\n$dropdown-dark-link-hover-color: $white !default;\n$dropdown-dark-link-hover-bg: rgba($white, .15) !default;\n$dropdown-dark-link-active-color: $dropdown-link-active-color !default;\n$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default;\n$dropdown-dark-link-disabled-color: $gray-500 !default;\n$dropdown-dark-header-color: $gray-500 !default;\n// scss-docs-end dropdown-dark-variables\n\n\n// Pagination\n\n// scss-docs-start pagination-variables\n$pagination-padding-y: .375rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-radius: $border-radius !default;\n$pagination-margin-start: -$pagination-border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-color: $link-hover-color !default;\n$pagination-focus-bg: $gray-200 !default;\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$pagination-border-radius-sm: $border-radius-sm !default;\n$pagination-border-radius-lg: $border-radius-lg !default;\n// scss-docs-end pagination-variables\n\n\n// Placeholders\n\n// scss-docs-start placeholders\n$placeholder-opacity-max: .5 !default;\n$placeholder-opacity-min: .2 !default;\n// scss-docs-end placeholders\n\n// Cards\n\n// scss-docs-start card-variables\n$card-spacer-y: $spacer !default;\n$card-spacer-x: $spacer !default;\n$card-title-spacer-y: $spacer * .5 !default;\n$card-border-width: $border-width !default;\n$card-border-color: rgba($black, .125) !default;\n$card-border-radius: $border-radius !default;\n$card-box-shadow: null !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-padding-y: $card-spacer-y * .5 !default;\n$card-cap-padding-x: $card-spacer-x !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n$card-img-overlay-padding: $spacer !default;\n$card-group-margin: $grid-gutter-width * .5 !default;\n// scss-docs-end card-variables\n\n// Accordion\n\n// scss-docs-start accordion-variables\n$accordion-padding-y: 1rem !default;\n$accordion-padding-x: 1.25rem !default;\n$accordion-color: $body-color !default;\n$accordion-bg: $body-bg !default;\n$accordion-border-width: $border-width !default;\n$accordion-border-color: rgba($black, .125) !default;\n$accordion-border-radius: $border-radius !default;\n$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default;\n\n$accordion-body-padding-y: $accordion-padding-y !default;\n$accordion-body-padding-x: $accordion-padding-x !default;\n\n$accordion-button-padding-y: $accordion-padding-y !default;\n$accordion-button-padding-x: $accordion-padding-x !default;\n$accordion-button-color: $accordion-color !default;\n$accordion-button-bg: $accordion-bg !default;\n$accordion-transition: $btn-transition, border-radius .15s ease !default;\n$accordion-button-active-bg: tint-color($component-active-bg, 90%) !default;\n$accordion-button-active-color: shade-color($primary, 10%) !default;\n\n$accordion-button-focus-border-color: $input-focus-border-color !default;\n$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default;\n\n$accordion-icon-width: 1.25rem !default;\n$accordion-icon-color: $accordion-button-color !default;\n$accordion-icon-active-color: $accordion-button-active-color !default;\n$accordion-icon-transition: transform .2s ease-in-out !default;\n$accordion-icon-transform: rotate(-180deg) !default;\n\n$accordion-button-icon: url(\"data:image/svg+xml,\") !default;\n$accordion-button-active-icon: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end accordion-variables\n\n// Tooltips\n\n// scss-docs-start tooltip-variables\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: $spacer * .25 !default;\n$tooltip-padding-x: $spacer * .5 !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n// scss-docs-end tooltip-variables\n\n// Form tooltips must come after regular tooltips\n// scss-docs-start tooltip-feedback-variables\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: null !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n// scss-docs-end tooltip-feedback-variables\n\n\n// Popovers\n\n// scss-docs-start popover-variables\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: $box-shadow !default;\n\n$popover-header-bg: shade-color($popover-bg, 6%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: $spacer !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $spacer !default;\n$popover-body-padding-x: $spacer !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n// scss-docs-end popover-variables\n\n\n// Toasts\n\n// scss-docs-start toast-variables\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .5rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: $border-radius !default;\n$toast-box-shadow: $box-shadow !default;\n$toast-spacing: $container-padding-x !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n// scss-docs-end toast-variables\n\n\n// Badges\n\n// scss-docs-start badge-variables\n$badge-font-size: .75em !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-color: $white !default;\n$badge-padding-y: .35em !default;\n$badge-padding-x: .65em !default;\n$badge-border-radius: $border-radius !default;\n// scss-docs-end badge-variables\n\n\n// Modals\n\n// scss-docs-start modal-variables\n$modal-inner-padding: $spacer !default;\n\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: $box-shadow-sm !default;\n$modal-content-box-shadow-sm-up: $box-shadow !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: $modal-inner-padding !default;\n$modal-header-padding-x: $modal-inner-padding !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-sm: 300px !default;\n$modal-md: 500px !default;\n$modal-lg: 800px !default;\n$modal-xl: 1140px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n// scss-docs-end modal-variables\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n// scss-docs-start alert-variables\n$alert-padding-y: $spacer !default;\n$alert-padding-x: $spacer !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n$alert-bg-scale: -80% !default;\n$alert-border-scale: -70% !default;\n$alert-color-scale: 40% !default;\n$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side\n// scss-docs-end alert-variables\n\n\n// Progress bars\n\n// scss-docs-start progress-variables\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: $box-shadow-inset !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: $primary !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n// scss-docs-end progress-variables\n\n\n// List group\n\n// scss-docs-start list-group-variables\n$list-group-color: $gray-900 !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: $spacer * .5 !default;\n$list-group-item-padding-x: $spacer !default;\n$list-group-item-bg-scale: -80% !default;\n$list-group-item-color-scale: 40% !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n// scss-docs-end list-group-variables\n\n\n// Image thumbnails\n\n// scss-docs-start thumbnail-variables\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: $box-shadow-sm !default;\n// scss-docs-end thumbnail-variables\n\n\n// Figures\n\n// scss-docs-start figure-variables\n$figure-caption-font-size: $small-font-size !default;\n$figure-caption-color: $gray-600 !default;\n// scss-docs-end figure-variables\n\n\n// Breadcrumbs\n\n// scss-docs-start breadcrumb-variables\n$breadcrumb-font-size: null !default;\n$breadcrumb-padding-y: 0 !default;\n$breadcrumb-padding-x: 0 !default;\n$breadcrumb-item-padding-x: .5rem !default;\n$breadcrumb-margin-bottom: 1rem !default;\n$breadcrumb-bg: null !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n$breadcrumb-divider-flipped: $breadcrumb-divider !default;\n$breadcrumb-border-radius: null !default;\n// scss-docs-end breadcrumb-variables\n\n// Carousel\n\n// scss-docs-start carousel-variables\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-opacity: .5 !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-active-opacity: 1 !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n$carousel-caption-padding-y: 1.25rem !default;\n$carousel-caption-spacer: 1.25rem !default;\n\n$carousel-control-icon-width: 2rem !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n$carousel-dark-indicator-active-bg: $black !default;\n$carousel-dark-caption-color: $black !default;\n$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default;\n// scss-docs-end carousel-variables\n\n\n// Spinners\n\n// scss-docs-start spinner-variables\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-vertical-align: -.125em !default;\n$spinner-border-width: .25em !default;\n$spinner-animation-speed: .75s !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n// scss-docs-end spinner-variables\n\n\n// Close\n\n// scss-docs-start close-variables\n$btn-close-width: 1em !default;\n$btn-close-height: $btn-close-width !default;\n$btn-close-padding-x: .25em !default;\n$btn-close-padding-y: $btn-close-padding-x !default;\n$btn-close-color: $black !default;\n$btn-close-bg: url(\"data:image/svg+xml,\") !default;\n$btn-close-focus-shadow: $input-btn-focus-box-shadow !default;\n$btn-close-opacity: .5 !default;\n$btn-close-hover-opacity: .75 !default;\n$btn-close-focus-opacity: 1 !default;\n$btn-close-disabled-opacity: .25 !default;\n$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default;\n// scss-docs-end close-variables\n\n\n// Offcanvas\n\n// scss-docs-start offcanvas-variables\n$offcanvas-padding-y: $modal-inner-padding !default;\n$offcanvas-padding-x: $modal-inner-padding !default;\n$offcanvas-horizontal-width: 400px !default;\n$offcanvas-vertical-height: 30vh !default;\n$offcanvas-transition-duration: .3s !default;\n$offcanvas-border-color: $modal-content-border-color !default;\n$offcanvas-border-width: $modal-content-border-width !default;\n$offcanvas-title-line-height: $modal-title-line-height !default;\n$offcanvas-bg-color: $modal-content-bg !default;\n$offcanvas-color: $modal-content-color !default;\n$offcanvas-box-shadow: $modal-content-box-shadow-xs !default;\n$offcanvas-backdrop-bg: $modal-backdrop-bg !default;\n$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default;\n// scss-docs-end offcanvas-variables\n\n// Code\n\n$code-font-size: $small-font-size !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: null !default;\n","// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$variable-prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$variable-prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$variable-prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$variable-prefix}gutter-x: #{$gutter};\n --#{$variable-prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list\n margin-right: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n margin-left: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$variable-prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: divide(100%, $count);\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0%; // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is and invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (eg. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $value in $is-local-vars {\n --#{$variable-prefix}#{$local-var}: #{$value};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 0000000..3160359 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}@media (min-width:576px){.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}}@media (min-width:768px){.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}}@media (min-width:992px){.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}}@media (min-width:1200px){.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}}@media (min-width:1400px){.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map new file mode 100644 index 0000000..4f745de --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","../../scss/_containers.scss","dist/css/bootstrap-grid.css","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n$include-column-box-sizing: true !default;\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/lists\";\n@import \"mixins/breakpoints\";\n@import \"mixins/container\";\n@import \"mixins/grid\";\n@import \"mixins/utilities\";\n\n@import \"vendor/rfs\";\n\n@import \"containers\";\n@import \"grid\";\n\n@import \"utilities\";\n// Only use the utilities we need\n// stylelint-disable-next-line scss/dollar-variable-default\n$utilities: map-get-multiple(\n $utilities,\n (\n \"display\",\n \"order\",\n \"flex\",\n \"flex-direction\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-wrap\",\n \"justify-content\",\n \"align-items\",\n \"align-content\",\n \"align-self\",\n \"margin\",\n \"margin-x\",\n \"margin-y\",\n \"margin-top\",\n \"margin-end\",\n \"margin-bottom\",\n \"margin-start\",\n \"negative-margin\",\n \"negative-margin-x\",\n \"negative-margin-y\",\n \"negative-margin-top\",\n \"negative-margin-end\",\n \"negative-margin-bottom\",\n \"negative-margin-start\",\n \"padding\",\n \"padding-x\",\n \"padding-y\",\n \"padding-top\",\n \"padding-end\",\n \"padding-bottom\",\n \"padding-start\",\n )\n);\n\n@import \"utilities/api\";\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * .5);\n padding-left: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n width: 100%;\n padding-right: var(--#{$variable-prefix}gutter-x, #{$gutter});\n padding-left: var(--#{$variable-prefix}gutter-x, #{$gutter});\n margin-right: auto;\n margin-left: auto;\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$variable-prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$variable-prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$variable-prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$variable-prefix}gutter-x: #{$gutter};\n --#{$variable-prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list\n margin-right: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n margin-left: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$variable-prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: divide(100%, $count);\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0%; // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is and invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (eg. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $value in $is-local-vars {\n --#{$variable-prefix}#{$local-var}: #{$value};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css new file mode 100644 index 0000000..3c339b5 --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css @@ -0,0 +1,4996 @@ +/*! + * Bootstrap Grid v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + width: 100%; + padding-left: var(--bs-gutter-x, 0.75rem); + padding-right: var(--bs-gutter-x, 0.75rem); + margin-left: auto; + margin-right: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(var(--bs-gutter-y) * -1); + margin-left: calc(var(--bs-gutter-x) * -.5); + margin-right: calc(var(--bs-gutter-x) * -.5); +} +.row > * { + box-sizing: border-box; + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-left: calc(var(--bs-gutter-x) * .5); + padding-right: calc(var(--bs-gutter-x) * .5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0%; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-right: 8.33333333%; +} + +.offset-2 { + margin-right: 16.66666667%; +} + +.offset-3 { + margin-right: 25%; +} + +.offset-4 { + margin-right: 33.33333333%; +} + +.offset-5 { + margin-right: 41.66666667%; +} + +.offset-6 { + margin-right: 50%; +} + +.offset-7 { + margin-right: 58.33333333%; +} + +.offset-8 { + margin-right: 66.66666667%; +} + +.offset-9 { + margin-right: 75%; +} + +.offset-10 { + margin-right: 83.33333333%; +} + +.offset-11 { + margin-right: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-sm-0 { + margin-right: 0; + } + + .offset-sm-1 { + margin-right: 8.33333333%; + } + + .offset-sm-2 { + margin-right: 16.66666667%; + } + + .offset-sm-3 { + margin-right: 25%; + } + + .offset-sm-4 { + margin-right: 33.33333333%; + } + + .offset-sm-5 { + margin-right: 41.66666667%; + } + + .offset-sm-6 { + margin-right: 50%; + } + + .offset-sm-7 { + margin-right: 58.33333333%; + } + + .offset-sm-8 { + margin-right: 66.66666667%; + } + + .offset-sm-9 { + margin-right: 75%; + } + + .offset-sm-10 { + margin-right: 83.33333333%; + } + + .offset-sm-11 { + margin-right: 91.66666667%; + } + + .g-sm-0, +.gx-sm-0 { + --bs-gutter-x: 0; + } + + .g-sm-0, +.gy-sm-0 { + --bs-gutter-y: 0; + } + + .g-sm-1, +.gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + + .g-sm-1, +.gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + + .g-sm-2, +.gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + + .g-sm-2, +.gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + + .g-sm-3, +.gx-sm-3 { + --bs-gutter-x: 1rem; + } + + .g-sm-3, +.gy-sm-3 { + --bs-gutter-y: 1rem; + } + + .g-sm-4, +.gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + + .g-sm-4, +.gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + + .g-sm-5, +.gx-sm-5 { + --bs-gutter-x: 3rem; + } + + .g-sm-5, +.gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-md-0 { + margin-right: 0; + } + + .offset-md-1 { + margin-right: 8.33333333%; + } + + .offset-md-2 { + margin-right: 16.66666667%; + } + + .offset-md-3 { + margin-right: 25%; + } + + .offset-md-4 { + margin-right: 33.33333333%; + } + + .offset-md-5 { + margin-right: 41.66666667%; + } + + .offset-md-6 { + margin-right: 50%; + } + + .offset-md-7 { + margin-right: 58.33333333%; + } + + .offset-md-8 { + margin-right: 66.66666667%; + } + + .offset-md-9 { + margin-right: 75%; + } + + .offset-md-10 { + margin-right: 83.33333333%; + } + + .offset-md-11 { + margin-right: 91.66666667%; + } + + .g-md-0, +.gx-md-0 { + --bs-gutter-x: 0; + } + + .g-md-0, +.gy-md-0 { + --bs-gutter-y: 0; + } + + .g-md-1, +.gx-md-1 { + --bs-gutter-x: 0.25rem; + } + + .g-md-1, +.gy-md-1 { + --bs-gutter-y: 0.25rem; + } + + .g-md-2, +.gx-md-2 { + --bs-gutter-x: 0.5rem; + } + + .g-md-2, +.gy-md-2 { + --bs-gutter-y: 0.5rem; + } + + .g-md-3, +.gx-md-3 { + --bs-gutter-x: 1rem; + } + + .g-md-3, +.gy-md-3 { + --bs-gutter-y: 1rem; + } + + .g-md-4, +.gx-md-4 { + --bs-gutter-x: 1.5rem; + } + + .g-md-4, +.gy-md-4 { + --bs-gutter-y: 1.5rem; + } + + .g-md-5, +.gx-md-5 { + --bs-gutter-x: 3rem; + } + + .g-md-5, +.gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-lg-0 { + margin-right: 0; + } + + .offset-lg-1 { + margin-right: 8.33333333%; + } + + .offset-lg-2 { + margin-right: 16.66666667%; + } + + .offset-lg-3 { + margin-right: 25%; + } + + .offset-lg-4 { + margin-right: 33.33333333%; + } + + .offset-lg-5 { + margin-right: 41.66666667%; + } + + .offset-lg-6 { + margin-right: 50%; + } + + .offset-lg-7 { + margin-right: 58.33333333%; + } + + .offset-lg-8 { + margin-right: 66.66666667%; + } + + .offset-lg-9 { + margin-right: 75%; + } + + .offset-lg-10 { + margin-right: 83.33333333%; + } + + .offset-lg-11 { + margin-right: 91.66666667%; + } + + .g-lg-0, +.gx-lg-0 { + --bs-gutter-x: 0; + } + + .g-lg-0, +.gy-lg-0 { + --bs-gutter-y: 0; + } + + .g-lg-1, +.gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + + .g-lg-1, +.gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + + .g-lg-2, +.gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + + .g-lg-2, +.gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + + .g-lg-3, +.gx-lg-3 { + --bs-gutter-x: 1rem; + } + + .g-lg-3, +.gy-lg-3 { + --bs-gutter-y: 1rem; + } + + .g-lg-4, +.gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + + .g-lg-4, +.gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + + .g-lg-5, +.gx-lg-5 { + --bs-gutter-x: 3rem; + } + + .g-lg-5, +.gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xl-0 { + margin-right: 0; + } + + .offset-xl-1 { + margin-right: 8.33333333%; + } + + .offset-xl-2 { + margin-right: 16.66666667%; + } + + .offset-xl-3 { + margin-right: 25%; + } + + .offset-xl-4 { + margin-right: 33.33333333%; + } + + .offset-xl-5 { + margin-right: 41.66666667%; + } + + .offset-xl-6 { + margin-right: 50%; + } + + .offset-xl-7 { + margin-right: 58.33333333%; + } + + .offset-xl-8 { + margin-right: 66.66666667%; + } + + .offset-xl-9 { + margin-right: 75%; + } + + .offset-xl-10 { + margin-right: 83.33333333%; + } + + .offset-xl-11 { + margin-right: 91.66666667%; + } + + .g-xl-0, +.gx-xl-0 { + --bs-gutter-x: 0; + } + + .g-xl-0, +.gy-xl-0 { + --bs-gutter-y: 0; + } + + .g-xl-1, +.gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xl-1, +.gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xl-2, +.gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xl-2, +.gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xl-3, +.gx-xl-3 { + --bs-gutter-x: 1rem; + } + + .g-xl-3, +.gy-xl-3 { + --bs-gutter-y: 1rem; + } + + .g-xl-4, +.gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xl-4, +.gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xl-5, +.gx-xl-5 { + --bs-gutter-x: 3rem; + } + + .g-xl-5, +.gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xxl-0 { + margin-right: 0; + } + + .offset-xxl-1 { + margin-right: 8.33333333%; + } + + .offset-xxl-2 { + margin-right: 16.66666667%; + } + + .offset-xxl-3 { + margin-right: 25%; + } + + .offset-xxl-4 { + margin-right: 33.33333333%; + } + + .offset-xxl-5 { + margin-right: 41.66666667%; + } + + .offset-xxl-6 { + margin-right: 50%; + } + + .offset-xxl-7 { + margin-right: 58.33333333%; + } + + .offset-xxl-8 { + margin-right: 66.66666667%; + } + + .offset-xxl-9 { + margin-right: 75%; + } + + .offset-xxl-10 { + margin-right: 83.33333333%; + } + + .offset-xxl-11 { + margin-right: 91.66666667%; + } + + .g-xxl-0, +.gx-xxl-0 { + --bs-gutter-x: 0; + } + + .g-xxl-0, +.gy-xxl-0 { + --bs-gutter-y: 0; + } + + .g-xxl-1, +.gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xxl-1, +.gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xxl-2, +.gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xxl-2, +.gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xxl-3, +.gx-xxl-3 { + --bs-gutter-x: 1rem; + } + + .g-xxl-3, +.gy-xxl-3 { + --bs-gutter-y: 1rem; + } + + .g-xxl-4, +.gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xxl-4, +.gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xxl-5, +.gx-xxl-5 { + --bs-gutter-x: 3rem; + } + + .g-xxl-5, +.gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-left: 0 !important; + margin-right: 0 !important; +} + +.mx-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; +} + +.mx-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; +} + +.mx-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; +} + +.mx-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; +} + +.mx-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; +} + +.mx-auto { + margin-left: auto !important; + margin-right: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-left: 0 !important; +} + +.me-1 { + margin-left: 0.25rem !important; +} + +.me-2 { + margin-left: 0.5rem !important; +} + +.me-3 { + margin-left: 1rem !important; +} + +.me-4 { + margin-left: 1.5rem !important; +} + +.me-5 { + margin-left: 3rem !important; +} + +.me-auto { + margin-left: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-right: 0 !important; +} + +.ms-1 { + margin-right: 0.25rem !important; +} + +.ms-2 { + margin-right: 0.5rem !important; +} + +.ms-3 { + margin-right: 1rem !important; +} + +.ms-4 { + margin-right: 1.5rem !important; +} + +.ms-5 { + margin-right: 3rem !important; +} + +.ms-auto { + margin-right: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.px-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; +} + +.px-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; +} + +.px-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; +} + +.px-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; +} + +.px-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-left: 0 !important; +} + +.pe-1 { + padding-left: 0.25rem !important; +} + +.pe-2 { + padding-left: 0.5rem !important; +} + +.pe-3 { + padding-left: 1rem !important; +} + +.pe-4 { + padding-left: 1.5rem !important; +} + +.pe-5 { + padding-left: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-right: 0 !important; +} + +.ps-1 { + padding-right: 0.25rem !important; +} + +.ps-2 { + padding-right: 0.5rem !important; +} + +.ps-3 { + padding-right: 1rem !important; +} + +.ps-4 { + padding-right: 1.5rem !important; +} + +.ps-5 { + padding-right: 3rem !important; +} + +@media (min-width: 576px) { + .d-sm-inline { + display: inline !important; + } + + .d-sm-inline-block { + display: inline-block !important; + } + + .d-sm-block { + display: block !important; + } + + .d-sm-grid { + display: grid !important; + } + + .d-sm-table { + display: table !important; + } + + .d-sm-table-row { + display: table-row !important; + } + + .d-sm-table-cell { + display: table-cell !important; + } + + .d-sm-flex { + display: flex !important; + } + + .d-sm-inline-flex { + display: inline-flex !important; + } + + .d-sm-none { + display: none !important; + } + + .flex-sm-fill { + flex: 1 1 auto !important; + } + + .flex-sm-row { + flex-direction: row !important; + } + + .flex-sm-column { + flex-direction: column !important; + } + + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-sm-wrap { + flex-wrap: wrap !important; + } + + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-sm-start { + justify-content: flex-start !important; + } + + .justify-content-sm-end { + justify-content: flex-end !important; + } + + .justify-content-sm-center { + justify-content: center !important; + } + + .justify-content-sm-between { + justify-content: space-between !important; + } + + .justify-content-sm-around { + justify-content: space-around !important; + } + + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + + .align-items-sm-start { + align-items: flex-start !important; + } + + .align-items-sm-end { + align-items: flex-end !important; + } + + .align-items-sm-center { + align-items: center !important; + } + + .align-items-sm-baseline { + align-items: baseline !important; + } + + .align-items-sm-stretch { + align-items: stretch !important; + } + + .align-content-sm-start { + align-content: flex-start !important; + } + + .align-content-sm-end { + align-content: flex-end !important; + } + + .align-content-sm-center { + align-content: center !important; + } + + .align-content-sm-between { + align-content: space-between !important; + } + + .align-content-sm-around { + align-content: space-around !important; + } + + .align-content-sm-stretch { + align-content: stretch !important; + } + + .align-self-sm-auto { + align-self: auto !important; + } + + .align-self-sm-start { + align-self: flex-start !important; + } + + .align-self-sm-end { + align-self: flex-end !important; + } + + .align-self-sm-center { + align-self: center !important; + } + + .align-self-sm-baseline { + align-self: baseline !important; + } + + .align-self-sm-stretch { + align-self: stretch !important; + } + + .order-sm-first { + order: -1 !important; + } + + .order-sm-0 { + order: 0 !important; + } + + .order-sm-1 { + order: 1 !important; + } + + .order-sm-2 { + order: 2 !important; + } + + .order-sm-3 { + order: 3 !important; + } + + .order-sm-4 { + order: 4 !important; + } + + .order-sm-5 { + order: 5 !important; + } + + .order-sm-last { + order: 6 !important; + } + + .m-sm-0 { + margin: 0 !important; + } + + .m-sm-1 { + margin: 0.25rem !important; + } + + .m-sm-2 { + margin: 0.5rem !important; + } + + .m-sm-3 { + margin: 1rem !important; + } + + .m-sm-4 { + margin: 1.5rem !important; + } + + .m-sm-5 { + margin: 3rem !important; + } + + .m-sm-auto { + margin: auto !important; + } + + .mx-sm-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .mx-sm-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + + .mx-sm-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + + .mx-sm-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + + .mx-sm-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + + .mx-sm-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + + .mx-sm-auto { + margin-left: auto !important; + margin-right: auto !important; + } + + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-sm-0 { + margin-top: 0 !important; + } + + .mt-sm-1 { + margin-top: 0.25rem !important; + } + + .mt-sm-2 { + margin-top: 0.5rem !important; + } + + .mt-sm-3 { + margin-top: 1rem !important; + } + + .mt-sm-4 { + margin-top: 1.5rem !important; + } + + .mt-sm-5 { + margin-top: 3rem !important; + } + + .mt-sm-auto { + margin-top: auto !important; + } + + .me-sm-0 { + margin-left: 0 !important; + } + + .me-sm-1 { + margin-left: 0.25rem !important; + } + + .me-sm-2 { + margin-left: 0.5rem !important; + } + + .me-sm-3 { + margin-left: 1rem !important; + } + + .me-sm-4 { + margin-left: 1.5rem !important; + } + + .me-sm-5 { + margin-left: 3rem !important; + } + + .me-sm-auto { + margin-left: auto !important; + } + + .mb-sm-0 { + margin-bottom: 0 !important; + } + + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + + .mb-sm-3 { + margin-bottom: 1rem !important; + } + + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + + .mb-sm-5 { + margin-bottom: 3rem !important; + } + + .mb-sm-auto { + margin-bottom: auto !important; + } + + .ms-sm-0 { + margin-right: 0 !important; + } + + .ms-sm-1 { + margin-right: 0.25rem !important; + } + + .ms-sm-2 { + margin-right: 0.5rem !important; + } + + .ms-sm-3 { + margin-right: 1rem !important; + } + + .ms-sm-4 { + margin-right: 1.5rem !important; + } + + .ms-sm-5 { + margin-right: 3rem !important; + } + + .ms-sm-auto { + margin-right: auto !important; + } + + .p-sm-0 { + padding: 0 !important; + } + + .p-sm-1 { + padding: 0.25rem !important; + } + + .p-sm-2 { + padding: 0.5rem !important; + } + + .p-sm-3 { + padding: 1rem !important; + } + + .p-sm-4 { + padding: 1.5rem !important; + } + + .p-sm-5 { + padding: 3rem !important; + } + + .px-sm-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .px-sm-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + .px-sm-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + + .px-sm-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .px-sm-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + + .px-sm-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-sm-0 { + padding-top: 0 !important; + } + + .pt-sm-1 { + padding-top: 0.25rem !important; + } + + .pt-sm-2 { + padding-top: 0.5rem !important; + } + + .pt-sm-3 { + padding-top: 1rem !important; + } + + .pt-sm-4 { + padding-top: 1.5rem !important; + } + + .pt-sm-5 { + padding-top: 3rem !important; + } + + .pe-sm-0 { + padding-left: 0 !important; + } + + .pe-sm-1 { + padding-left: 0.25rem !important; + } + + .pe-sm-2 { + padding-left: 0.5rem !important; + } + + .pe-sm-3 { + padding-left: 1rem !important; + } + + .pe-sm-4 { + padding-left: 1.5rem !important; + } + + .pe-sm-5 { + padding-left: 3rem !important; + } + + .pb-sm-0 { + padding-bottom: 0 !important; + } + + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + + .pb-sm-3 { + padding-bottom: 1rem !important; + } + + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + + .pb-sm-5 { + padding-bottom: 3rem !important; + } + + .ps-sm-0 { + padding-right: 0 !important; + } + + .ps-sm-1 { + padding-right: 0.25rem !important; + } + + .ps-sm-2 { + padding-right: 0.5rem !important; + } + + .ps-sm-3 { + padding-right: 1rem !important; + } + + .ps-sm-4 { + padding-right: 1.5rem !important; + } + + .ps-sm-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 768px) { + .d-md-inline { + display: inline !important; + } + + .d-md-inline-block { + display: inline-block !important; + } + + .d-md-block { + display: block !important; + } + + .d-md-grid { + display: grid !important; + } + + .d-md-table { + display: table !important; + } + + .d-md-table-row { + display: table-row !important; + } + + .d-md-table-cell { + display: table-cell !important; + } + + .d-md-flex { + display: flex !important; + } + + .d-md-inline-flex { + display: inline-flex !important; + } + + .d-md-none { + display: none !important; + } + + .flex-md-fill { + flex: 1 1 auto !important; + } + + .flex-md-row { + flex-direction: row !important; + } + + .flex-md-column { + flex-direction: column !important; + } + + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-md-grow-0 { + flex-grow: 0 !important; + } + + .flex-md-grow-1 { + flex-grow: 1 !important; + } + + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-md-wrap { + flex-wrap: wrap !important; + } + + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-md-start { + justify-content: flex-start !important; + } + + .justify-content-md-end { + justify-content: flex-end !important; + } + + .justify-content-md-center { + justify-content: center !important; + } + + .justify-content-md-between { + justify-content: space-between !important; + } + + .justify-content-md-around { + justify-content: space-around !important; + } + + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + + .align-items-md-start { + align-items: flex-start !important; + } + + .align-items-md-end { + align-items: flex-end !important; + } + + .align-items-md-center { + align-items: center !important; + } + + .align-items-md-baseline { + align-items: baseline !important; + } + + .align-items-md-stretch { + align-items: stretch !important; + } + + .align-content-md-start { + align-content: flex-start !important; + } + + .align-content-md-end { + align-content: flex-end !important; + } + + .align-content-md-center { + align-content: center !important; + } + + .align-content-md-between { + align-content: space-between !important; + } + + .align-content-md-around { + align-content: space-around !important; + } + + .align-content-md-stretch { + align-content: stretch !important; + } + + .align-self-md-auto { + align-self: auto !important; + } + + .align-self-md-start { + align-self: flex-start !important; + } + + .align-self-md-end { + align-self: flex-end !important; + } + + .align-self-md-center { + align-self: center !important; + } + + .align-self-md-baseline { + align-self: baseline !important; + } + + .align-self-md-stretch { + align-self: stretch !important; + } + + .order-md-first { + order: -1 !important; + } + + .order-md-0 { + order: 0 !important; + } + + .order-md-1 { + order: 1 !important; + } + + .order-md-2 { + order: 2 !important; + } + + .order-md-3 { + order: 3 !important; + } + + .order-md-4 { + order: 4 !important; + } + + .order-md-5 { + order: 5 !important; + } + + .order-md-last { + order: 6 !important; + } + + .m-md-0 { + margin: 0 !important; + } + + .m-md-1 { + margin: 0.25rem !important; + } + + .m-md-2 { + margin: 0.5rem !important; + } + + .m-md-3 { + margin: 1rem !important; + } + + .m-md-4 { + margin: 1.5rem !important; + } + + .m-md-5 { + margin: 3rem !important; + } + + .m-md-auto { + margin: auto !important; + } + + .mx-md-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .mx-md-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + + .mx-md-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + + .mx-md-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + + .mx-md-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + + .mx-md-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + + .mx-md-auto { + margin-left: auto !important; + margin-right: auto !important; + } + + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-md-0 { + margin-top: 0 !important; + } + + .mt-md-1 { + margin-top: 0.25rem !important; + } + + .mt-md-2 { + margin-top: 0.5rem !important; + } + + .mt-md-3 { + margin-top: 1rem !important; + } + + .mt-md-4 { + margin-top: 1.5rem !important; + } + + .mt-md-5 { + margin-top: 3rem !important; + } + + .mt-md-auto { + margin-top: auto !important; + } + + .me-md-0 { + margin-left: 0 !important; + } + + .me-md-1 { + margin-left: 0.25rem !important; + } + + .me-md-2 { + margin-left: 0.5rem !important; + } + + .me-md-3 { + margin-left: 1rem !important; + } + + .me-md-4 { + margin-left: 1.5rem !important; + } + + .me-md-5 { + margin-left: 3rem !important; + } + + .me-md-auto { + margin-left: auto !important; + } + + .mb-md-0 { + margin-bottom: 0 !important; + } + + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + + .mb-md-3 { + margin-bottom: 1rem !important; + } + + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + + .mb-md-5 { + margin-bottom: 3rem !important; + } + + .mb-md-auto { + margin-bottom: auto !important; + } + + .ms-md-0 { + margin-right: 0 !important; + } + + .ms-md-1 { + margin-right: 0.25rem !important; + } + + .ms-md-2 { + margin-right: 0.5rem !important; + } + + .ms-md-3 { + margin-right: 1rem !important; + } + + .ms-md-4 { + margin-right: 1.5rem !important; + } + + .ms-md-5 { + margin-right: 3rem !important; + } + + .ms-md-auto { + margin-right: auto !important; + } + + .p-md-0 { + padding: 0 !important; + } + + .p-md-1 { + padding: 0.25rem !important; + } + + .p-md-2 { + padding: 0.5rem !important; + } + + .p-md-3 { + padding: 1rem !important; + } + + .p-md-4 { + padding: 1.5rem !important; + } + + .p-md-5 { + padding: 3rem !important; + } + + .px-md-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .px-md-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + .px-md-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + + .px-md-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .px-md-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + + .px-md-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-md-0 { + padding-top: 0 !important; + } + + .pt-md-1 { + padding-top: 0.25rem !important; + } + + .pt-md-2 { + padding-top: 0.5rem !important; + } + + .pt-md-3 { + padding-top: 1rem !important; + } + + .pt-md-4 { + padding-top: 1.5rem !important; + } + + .pt-md-5 { + padding-top: 3rem !important; + } + + .pe-md-0 { + padding-left: 0 !important; + } + + .pe-md-1 { + padding-left: 0.25rem !important; + } + + .pe-md-2 { + padding-left: 0.5rem !important; + } + + .pe-md-3 { + padding-left: 1rem !important; + } + + .pe-md-4 { + padding-left: 1.5rem !important; + } + + .pe-md-5 { + padding-left: 3rem !important; + } + + .pb-md-0 { + padding-bottom: 0 !important; + } + + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + + .pb-md-3 { + padding-bottom: 1rem !important; + } + + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + + .pb-md-5 { + padding-bottom: 3rem !important; + } + + .ps-md-0 { + padding-right: 0 !important; + } + + .ps-md-1 { + padding-right: 0.25rem !important; + } + + .ps-md-2 { + padding-right: 0.5rem !important; + } + + .ps-md-3 { + padding-right: 1rem !important; + } + + .ps-md-4 { + padding-right: 1.5rem !important; + } + + .ps-md-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 992px) { + .d-lg-inline { + display: inline !important; + } + + .d-lg-inline-block { + display: inline-block !important; + } + + .d-lg-block { + display: block !important; + } + + .d-lg-grid { + display: grid !important; + } + + .d-lg-table { + display: table !important; + } + + .d-lg-table-row { + display: table-row !important; + } + + .d-lg-table-cell { + display: table-cell !important; + } + + .d-lg-flex { + display: flex !important; + } + + .d-lg-inline-flex { + display: inline-flex !important; + } + + .d-lg-none { + display: none !important; + } + + .flex-lg-fill { + flex: 1 1 auto !important; + } + + .flex-lg-row { + flex-direction: row !important; + } + + .flex-lg-column { + flex-direction: column !important; + } + + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-lg-wrap { + flex-wrap: wrap !important; + } + + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-lg-start { + justify-content: flex-start !important; + } + + .justify-content-lg-end { + justify-content: flex-end !important; + } + + .justify-content-lg-center { + justify-content: center !important; + } + + .justify-content-lg-between { + justify-content: space-between !important; + } + + .justify-content-lg-around { + justify-content: space-around !important; + } + + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + + .align-items-lg-start { + align-items: flex-start !important; + } + + .align-items-lg-end { + align-items: flex-end !important; + } + + .align-items-lg-center { + align-items: center !important; + } + + .align-items-lg-baseline { + align-items: baseline !important; + } + + .align-items-lg-stretch { + align-items: stretch !important; + } + + .align-content-lg-start { + align-content: flex-start !important; + } + + .align-content-lg-end { + align-content: flex-end !important; + } + + .align-content-lg-center { + align-content: center !important; + } + + .align-content-lg-between { + align-content: space-between !important; + } + + .align-content-lg-around { + align-content: space-around !important; + } + + .align-content-lg-stretch { + align-content: stretch !important; + } + + .align-self-lg-auto { + align-self: auto !important; + } + + .align-self-lg-start { + align-self: flex-start !important; + } + + .align-self-lg-end { + align-self: flex-end !important; + } + + .align-self-lg-center { + align-self: center !important; + } + + .align-self-lg-baseline { + align-self: baseline !important; + } + + .align-self-lg-stretch { + align-self: stretch !important; + } + + .order-lg-first { + order: -1 !important; + } + + .order-lg-0 { + order: 0 !important; + } + + .order-lg-1 { + order: 1 !important; + } + + .order-lg-2 { + order: 2 !important; + } + + .order-lg-3 { + order: 3 !important; + } + + .order-lg-4 { + order: 4 !important; + } + + .order-lg-5 { + order: 5 !important; + } + + .order-lg-last { + order: 6 !important; + } + + .m-lg-0 { + margin: 0 !important; + } + + .m-lg-1 { + margin: 0.25rem !important; + } + + .m-lg-2 { + margin: 0.5rem !important; + } + + .m-lg-3 { + margin: 1rem !important; + } + + .m-lg-4 { + margin: 1.5rem !important; + } + + .m-lg-5 { + margin: 3rem !important; + } + + .m-lg-auto { + margin: auto !important; + } + + .mx-lg-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .mx-lg-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + + .mx-lg-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + + .mx-lg-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + + .mx-lg-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + + .mx-lg-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + + .mx-lg-auto { + margin-left: auto !important; + margin-right: auto !important; + } + + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-lg-0 { + margin-top: 0 !important; + } + + .mt-lg-1 { + margin-top: 0.25rem !important; + } + + .mt-lg-2 { + margin-top: 0.5rem !important; + } + + .mt-lg-3 { + margin-top: 1rem !important; + } + + .mt-lg-4 { + margin-top: 1.5rem !important; + } + + .mt-lg-5 { + margin-top: 3rem !important; + } + + .mt-lg-auto { + margin-top: auto !important; + } + + .me-lg-0 { + margin-left: 0 !important; + } + + .me-lg-1 { + margin-left: 0.25rem !important; + } + + .me-lg-2 { + margin-left: 0.5rem !important; + } + + .me-lg-3 { + margin-left: 1rem !important; + } + + .me-lg-4 { + margin-left: 1.5rem !important; + } + + .me-lg-5 { + margin-left: 3rem !important; + } + + .me-lg-auto { + margin-left: auto !important; + } + + .mb-lg-0 { + margin-bottom: 0 !important; + } + + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + + .mb-lg-3 { + margin-bottom: 1rem !important; + } + + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + + .mb-lg-5 { + margin-bottom: 3rem !important; + } + + .mb-lg-auto { + margin-bottom: auto !important; + } + + .ms-lg-0 { + margin-right: 0 !important; + } + + .ms-lg-1 { + margin-right: 0.25rem !important; + } + + .ms-lg-2 { + margin-right: 0.5rem !important; + } + + .ms-lg-3 { + margin-right: 1rem !important; + } + + .ms-lg-4 { + margin-right: 1.5rem !important; + } + + .ms-lg-5 { + margin-right: 3rem !important; + } + + .ms-lg-auto { + margin-right: auto !important; + } + + .p-lg-0 { + padding: 0 !important; + } + + .p-lg-1 { + padding: 0.25rem !important; + } + + .p-lg-2 { + padding: 0.5rem !important; + } + + .p-lg-3 { + padding: 1rem !important; + } + + .p-lg-4 { + padding: 1.5rem !important; + } + + .p-lg-5 { + padding: 3rem !important; + } + + .px-lg-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .px-lg-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + .px-lg-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + + .px-lg-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .px-lg-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + + .px-lg-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-lg-0 { + padding-top: 0 !important; + } + + .pt-lg-1 { + padding-top: 0.25rem !important; + } + + .pt-lg-2 { + padding-top: 0.5rem !important; + } + + .pt-lg-3 { + padding-top: 1rem !important; + } + + .pt-lg-4 { + padding-top: 1.5rem !important; + } + + .pt-lg-5 { + padding-top: 3rem !important; + } + + .pe-lg-0 { + padding-left: 0 !important; + } + + .pe-lg-1 { + padding-left: 0.25rem !important; + } + + .pe-lg-2 { + padding-left: 0.5rem !important; + } + + .pe-lg-3 { + padding-left: 1rem !important; + } + + .pe-lg-4 { + padding-left: 1.5rem !important; + } + + .pe-lg-5 { + padding-left: 3rem !important; + } + + .pb-lg-0 { + padding-bottom: 0 !important; + } + + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + + .pb-lg-3 { + padding-bottom: 1rem !important; + } + + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + + .pb-lg-5 { + padding-bottom: 3rem !important; + } + + .ps-lg-0 { + padding-right: 0 !important; + } + + .ps-lg-1 { + padding-right: 0.25rem !important; + } + + .ps-lg-2 { + padding-right: 0.5rem !important; + } + + .ps-lg-3 { + padding-right: 1rem !important; + } + + .ps-lg-4 { + padding-right: 1.5rem !important; + } + + .ps-lg-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 1200px) { + .d-xl-inline { + display: inline !important; + } + + .d-xl-inline-block { + display: inline-block !important; + } + + .d-xl-block { + display: block !important; + } + + .d-xl-grid { + display: grid !important; + } + + .d-xl-table { + display: table !important; + } + + .d-xl-table-row { + display: table-row !important; + } + + .d-xl-table-cell { + display: table-cell !important; + } + + .d-xl-flex { + display: flex !important; + } + + .d-xl-inline-flex { + display: inline-flex !important; + } + + .d-xl-none { + display: none !important; + } + + .flex-xl-fill { + flex: 1 1 auto !important; + } + + .flex-xl-row { + flex-direction: row !important; + } + + .flex-xl-column { + flex-direction: column !important; + } + + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xl-wrap { + flex-wrap: wrap !important; + } + + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-xl-start { + justify-content: flex-start !important; + } + + .justify-content-xl-end { + justify-content: flex-end !important; + } + + .justify-content-xl-center { + justify-content: center !important; + } + + .justify-content-xl-between { + justify-content: space-between !important; + } + + .justify-content-xl-around { + justify-content: space-around !important; + } + + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xl-start { + align-items: flex-start !important; + } + + .align-items-xl-end { + align-items: flex-end !important; + } + + .align-items-xl-center { + align-items: center !important; + } + + .align-items-xl-baseline { + align-items: baseline !important; + } + + .align-items-xl-stretch { + align-items: stretch !important; + } + + .align-content-xl-start { + align-content: flex-start !important; + } + + .align-content-xl-end { + align-content: flex-end !important; + } + + .align-content-xl-center { + align-content: center !important; + } + + .align-content-xl-between { + align-content: space-between !important; + } + + .align-content-xl-around { + align-content: space-around !important; + } + + .align-content-xl-stretch { + align-content: stretch !important; + } + + .align-self-xl-auto { + align-self: auto !important; + } + + .align-self-xl-start { + align-self: flex-start !important; + } + + .align-self-xl-end { + align-self: flex-end !important; + } + + .align-self-xl-center { + align-self: center !important; + } + + .align-self-xl-baseline { + align-self: baseline !important; + } + + .align-self-xl-stretch { + align-self: stretch !important; + } + + .order-xl-first { + order: -1 !important; + } + + .order-xl-0 { + order: 0 !important; + } + + .order-xl-1 { + order: 1 !important; + } + + .order-xl-2 { + order: 2 !important; + } + + .order-xl-3 { + order: 3 !important; + } + + .order-xl-4 { + order: 4 !important; + } + + .order-xl-5 { + order: 5 !important; + } + + .order-xl-last { + order: 6 !important; + } + + .m-xl-0 { + margin: 0 !important; + } + + .m-xl-1 { + margin: 0.25rem !important; + } + + .m-xl-2 { + margin: 0.5rem !important; + } + + .m-xl-3 { + margin: 1rem !important; + } + + .m-xl-4 { + margin: 1.5rem !important; + } + + .m-xl-5 { + margin: 3rem !important; + } + + .m-xl-auto { + margin: auto !important; + } + + .mx-xl-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .mx-xl-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + + .mx-xl-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + + .mx-xl-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + + .mx-xl-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + + .mx-xl-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + + .mx-xl-auto { + margin-left: auto !important; + margin-right: auto !important; + } + + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xl-0 { + margin-top: 0 !important; + } + + .mt-xl-1 { + margin-top: 0.25rem !important; + } + + .mt-xl-2 { + margin-top: 0.5rem !important; + } + + .mt-xl-3 { + margin-top: 1rem !important; + } + + .mt-xl-4 { + margin-top: 1.5rem !important; + } + + .mt-xl-5 { + margin-top: 3rem !important; + } + + .mt-xl-auto { + margin-top: auto !important; + } + + .me-xl-0 { + margin-left: 0 !important; + } + + .me-xl-1 { + margin-left: 0.25rem !important; + } + + .me-xl-2 { + margin-left: 0.5rem !important; + } + + .me-xl-3 { + margin-left: 1rem !important; + } + + .me-xl-4 { + margin-left: 1.5rem !important; + } + + .me-xl-5 { + margin-left: 3rem !important; + } + + .me-xl-auto { + margin-left: auto !important; + } + + .mb-xl-0 { + margin-bottom: 0 !important; + } + + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xl-3 { + margin-bottom: 1rem !important; + } + + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xl-5 { + margin-bottom: 3rem !important; + } + + .mb-xl-auto { + margin-bottom: auto !important; + } + + .ms-xl-0 { + margin-right: 0 !important; + } + + .ms-xl-1 { + margin-right: 0.25rem !important; + } + + .ms-xl-2 { + margin-right: 0.5rem !important; + } + + .ms-xl-3 { + margin-right: 1rem !important; + } + + .ms-xl-4 { + margin-right: 1.5rem !important; + } + + .ms-xl-5 { + margin-right: 3rem !important; + } + + .ms-xl-auto { + margin-right: auto !important; + } + + .p-xl-0 { + padding: 0 !important; + } + + .p-xl-1 { + padding: 0.25rem !important; + } + + .p-xl-2 { + padding: 0.5rem !important; + } + + .p-xl-3 { + padding: 1rem !important; + } + + .p-xl-4 { + padding: 1.5rem !important; + } + + .p-xl-5 { + padding: 3rem !important; + } + + .px-xl-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .px-xl-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + .px-xl-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + + .px-xl-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .px-xl-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + + .px-xl-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xl-0 { + padding-top: 0 !important; + } + + .pt-xl-1 { + padding-top: 0.25rem !important; + } + + .pt-xl-2 { + padding-top: 0.5rem !important; + } + + .pt-xl-3 { + padding-top: 1rem !important; + } + + .pt-xl-4 { + padding-top: 1.5rem !important; + } + + .pt-xl-5 { + padding-top: 3rem !important; + } + + .pe-xl-0 { + padding-left: 0 !important; + } + + .pe-xl-1 { + padding-left: 0.25rem !important; + } + + .pe-xl-2 { + padding-left: 0.5rem !important; + } + + .pe-xl-3 { + padding-left: 1rem !important; + } + + .pe-xl-4 { + padding-left: 1.5rem !important; + } + + .pe-xl-5 { + padding-left: 3rem !important; + } + + .pb-xl-0 { + padding-bottom: 0 !important; + } + + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xl-3 { + padding-bottom: 1rem !important; + } + + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xl-5 { + padding-bottom: 3rem !important; + } + + .ps-xl-0 { + padding-right: 0 !important; + } + + .ps-xl-1 { + padding-right: 0.25rem !important; + } + + .ps-xl-2 { + padding-right: 0.5rem !important; + } + + .ps-xl-3 { + padding-right: 1rem !important; + } + + .ps-xl-4 { + padding-right: 1.5rem !important; + } + + .ps-xl-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 1400px) { + .d-xxl-inline { + display: inline !important; + } + + .d-xxl-inline-block { + display: inline-block !important; + } + + .d-xxl-block { + display: block !important; + } + + .d-xxl-grid { + display: grid !important; + } + + .d-xxl-table { + display: table !important; + } + + .d-xxl-table-row { + display: table-row !important; + } + + .d-xxl-table-cell { + display: table-cell !important; + } + + .d-xxl-flex { + display: flex !important; + } + + .d-xxl-inline-flex { + display: inline-flex !important; + } + + .d-xxl-none { + display: none !important; + } + + .flex-xxl-fill { + flex: 1 1 auto !important; + } + + .flex-xxl-row { + flex-direction: row !important; + } + + .flex-xxl-column { + flex-direction: column !important; + } + + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .justify-content-xxl-start { + justify-content: flex-start !important; + } + + .justify-content-xxl-end { + justify-content: flex-end !important; + } + + .justify-content-xxl-center { + justify-content: center !important; + } + + .justify-content-xxl-between { + justify-content: space-between !important; + } + + .justify-content-xxl-around { + justify-content: space-around !important; + } + + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xxl-start { + align-items: flex-start !important; + } + + .align-items-xxl-end { + align-items: flex-end !important; + } + + .align-items-xxl-center { + align-items: center !important; + } + + .align-items-xxl-baseline { + align-items: baseline !important; + } + + .align-items-xxl-stretch { + align-items: stretch !important; + } + + .align-content-xxl-start { + align-content: flex-start !important; + } + + .align-content-xxl-end { + align-content: flex-end !important; + } + + .align-content-xxl-center { + align-content: center !important; + } + + .align-content-xxl-between { + align-content: space-between !important; + } + + .align-content-xxl-around { + align-content: space-around !important; + } + + .align-content-xxl-stretch { + align-content: stretch !important; + } + + .align-self-xxl-auto { + align-self: auto !important; + } + + .align-self-xxl-start { + align-self: flex-start !important; + } + + .align-self-xxl-end { + align-self: flex-end !important; + } + + .align-self-xxl-center { + align-self: center !important; + } + + .align-self-xxl-baseline { + align-self: baseline !important; + } + + .align-self-xxl-stretch { + align-self: stretch !important; + } + + .order-xxl-first { + order: -1 !important; + } + + .order-xxl-0 { + order: 0 !important; + } + + .order-xxl-1 { + order: 1 !important; + } + + .order-xxl-2 { + order: 2 !important; + } + + .order-xxl-3 { + order: 3 !important; + } + + .order-xxl-4 { + order: 4 !important; + } + + .order-xxl-5 { + order: 5 !important; + } + + .order-xxl-last { + order: 6 !important; + } + + .m-xxl-0 { + margin: 0 !important; + } + + .m-xxl-1 { + margin: 0.25rem !important; + } + + .m-xxl-2 { + margin: 0.5rem !important; + } + + .m-xxl-3 { + margin: 1rem !important; + } + + .m-xxl-4 { + margin: 1.5rem !important; + } + + .m-xxl-5 { + margin: 3rem !important; + } + + .m-xxl-auto { + margin: auto !important; + } + + .mx-xxl-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .mx-xxl-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + + .mx-xxl-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + + .mx-xxl-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + + .mx-xxl-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + + .mx-xxl-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + + .mx-xxl-auto { + margin-left: auto !important; + margin-right: auto !important; + } + + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xxl-0 { + margin-top: 0 !important; + } + + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + + .mt-xxl-3 { + margin-top: 1rem !important; + } + + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + + .mt-xxl-5 { + margin-top: 3rem !important; + } + + .mt-xxl-auto { + margin-top: auto !important; + } + + .me-xxl-0 { + margin-left: 0 !important; + } + + .me-xxl-1 { + margin-left: 0.25rem !important; + } + + .me-xxl-2 { + margin-left: 0.5rem !important; + } + + .me-xxl-3 { + margin-left: 1rem !important; + } + + .me-xxl-4 { + margin-left: 1.5rem !important; + } + + .me-xxl-5 { + margin-left: 3rem !important; + } + + .me-xxl-auto { + margin-left: auto !important; + } + + .mb-xxl-0 { + margin-bottom: 0 !important; + } + + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + + .mb-xxl-auto { + margin-bottom: auto !important; + } + + .ms-xxl-0 { + margin-right: 0 !important; + } + + .ms-xxl-1 { + margin-right: 0.25rem !important; + } + + .ms-xxl-2 { + margin-right: 0.5rem !important; + } + + .ms-xxl-3 { + margin-right: 1rem !important; + } + + .ms-xxl-4 { + margin-right: 1.5rem !important; + } + + .ms-xxl-5 { + margin-right: 3rem !important; + } + + .ms-xxl-auto { + margin-right: auto !important; + } + + .p-xxl-0 { + padding: 0 !important; + } + + .p-xxl-1 { + padding: 0.25rem !important; + } + + .p-xxl-2 { + padding: 0.5rem !important; + } + + .p-xxl-3 { + padding: 1rem !important; + } + + .p-xxl-4 { + padding: 1.5rem !important; + } + + .p-xxl-5 { + padding: 3rem !important; + } + + .px-xxl-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .px-xxl-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + .px-xxl-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + + .px-xxl-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .px-xxl-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + + .px-xxl-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xxl-0 { + padding-top: 0 !important; + } + + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + + .pt-xxl-3 { + padding-top: 1rem !important; + } + + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + + .pt-xxl-5 { + padding-top: 3rem !important; + } + + .pe-xxl-0 { + padding-left: 0 !important; + } + + .pe-xxl-1 { + padding-left: 0.25rem !important; + } + + .pe-xxl-2 { + padding-left: 0.5rem !important; + } + + .pe-xxl-3 { + padding-left: 1rem !important; + } + + .pe-xxl-4 { + padding-left: 1.5rem !important; + } + + .pe-xxl-5 { + padding-left: 3rem !important; + } + + .pb-xxl-0 { + padding-bottom: 0 !important; + } + + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + + .ps-xxl-0 { + padding-right: 0 !important; + } + + .ps-xxl-1 { + padding-right: 0.25rem !important; + } + + .ps-xxl-2 { + padding-right: 0.5rem !important; + } + + .ps-xxl-3 { + padding-right: 1rem !important; + } + + .ps-xxl-4 { + padding-right: 1.5rem !important; + } + + .ps-xxl-5 { + padding-right: 3rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + + .d-print-inline-block { + display: inline-block !important; + } + + .d-print-block { + display: block !important; + } + + .d-print-grid { + display: grid !important; + } + + .d-print-table { + display: table !important; + } + + .d-print-table-row { + display: table-row !important; + } + + .d-print-table-cell { + display: table-cell !important; + } + + .d-print-flex { + display: flex !important; + } + + .d-print-inline-flex { + display: inline-flex !important; + } + + .d-print-none { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap-grid.rtl.css.map */ \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map new file mode 100644 index 0000000..33f5c3b --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","bootstrap-grid.css","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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AAA,+BAAA;AL2uDZ;;AKlvDQ;EAOI,yBAAA;AL+uDZ;;AKtvDQ;EAOI,+BAAA;ALmvDZ;;AK1vDQ;EAOI,8BAAA;ALuvDZ;;AK9vDQ;EAOI,4BAAA;AL2vDZ;;AKlwDQ;EAOI,8BAAA;AL+vDZ;;AKtwDQ;EAOI,4BAAA;ALmwDZ;;AK1wDQ;EAOI,0BAAA;ALuwDZ;;AK9wDQ;EAOI,gCAAA;AL2wDZ;;AKlxDQ;EAOI,+BAAA;AL+wDZ;;AKtxDQ;EAOI,6BAAA;ALmxDZ;;AK1xDQ;EAOI,+BAAA;ALuxDZ;;AK9xDQ;EAOI,6BAAA;AL2xDZ;;AKlyDQ;EAOI,4BAAA;AL+xDZ;;AKtyDQ;EAOI,kCAAA;ALmyDZ;;AK1yDQ;EAOI,iCAAA;ALuyDZ;;AK9yDQ;EAOI,+BAAA;AL2yDZ;;AKlzDQ;EAOI,iCAAA;AL+yDZ;;AKtzDQ;EAOI,+BAAA;ALmzDZ;;AK1zDQ;EAOI,2BAAA;ALuzDZ;;AK9zDQ;EAOI,iCAAA;AL2zDZ;;AKl0DQ;EAOI,gCAAA;AL+zDZ;;AKt0DQ;EAOI,8BAAA;ALm0DZ;;AK10DQ;EAOI,gCAAA;ALu0DZ;;AK90DQ;EAOI,8BAAA;AL20DZ;;ACl1DI;EIAI;IAOI,0BAAA;ELg1DV;;EKv1DM;IAOI,gCAAA;ELo1DV;;EK31DM;IAOI,yBAAA;ELw1DV;;EK/1DM;IAOI,wBAAA;EL41DV;;EKn2DM;IAOI,yBAAA;ELg2DV;;EKv2DM;IAOI,6BAAA;ELo2DV;;EK32DM;IAOI,8BAAA;ELw2DV;;EK/2DM;IAOI,wBAAA;EL42DV;;EKn3DM;IAOI,+BAAA;ELg3DV;;EKv3DM;IAOI,wBAAA;ELo3DV;;EK33DM;IAOI,yBAAA;ELw3DV;;EK/3DM;IAOI,8BAAA;EL43DV;;EKn4DM;IAOI,iCAAA;ELg4DV;;EKv4DM;IAOI,sCAAA;ELo4DV;;EK34DM;IAOI,yCAAA;ELw4DV;;EK/4DM;IAOI,uBAAA;EL44DV;;EKn5DM;IAOI,uBAAA;ELg5DV;;EKv5DM;IAOI,yBAAA;ELo5DV;;EK35DM;IAOI,yBAAA;ELw5DV;;EK/5DM;IAOI,0BAAA;EL45DV;;EKn6DM;IAOI,4BAAA;ELg6DV;;EKv6DM;IAOI,kCAAA;ELo6DV;;EK36DM;IAOI,sCAAA;ELw6DV;;EK/6DM;IAOI,oCAAA;EL46DV;;EKn7DM;IAOI,kCAAA;ELg7DV;;EKv7DM;IAOI,yCAAA;ELo7DV;;EK37DM;IAOI,wCAAA;ELw7DV;;EK/7DM;IAOI,wCAAA;EL47DV;;EKn8DM;IAOI,kCAAA;ELg8DV;;EKv8DM;IAOI,gCAAA;ELo8DV;;EK38DM;IAOI,8BAAA;ELw8DV;;EK/8DM;IAOI,gCAAA;EL48DV;;EKn9DM;IAOI,+BAAA;ELg9DV;;EKv9DM;IAOI,oCAAA;ELo9DV;;EK39DM;IAOI,kCAAA;ELw9DV;;EK/9DM;IAOI,gCAAA;EL49DV;;EKn+DM;IAOI,uCAAA;ELg+DV;;EKv+DM;IAOI,sCAAA;ELo+DV;;EK3+DM;IAOI,iCAAA;ELw+DV;;EK/+DM;IAOI,2BAAA;EL4+DV;;EKn/DM;IAOI,iCAAA;ELg/DV;;EKv/DM;IAOI,+BAAA;ELo/DV;;EK3/DM;IAOI,6BAAA;ELw/DV;;EK//DM;IAOI,+BAAA;EL4/DV;;EKngEM;IAOI,8BAAA;ELggEV;;EKvgEM;IAOI,oBAAA;ELogEV;;EK3gEM;IAOI,mBAAA;ELwgEV;;EK/gEM;IAOI,mBAAA;EL4gEV;;EKnhEM;IAOI,mBAAA;ELghEV;;EKvhEM;IAOI,mBAAA;ELohEV;;EK3hEM;IAOI,mBAAA;ELwhEV;;EK/hEM;IAOI,mBAAA;EL4hEV;;EKniEM;IAOI,mBAAA;ELgiEV;;EKviEM;IAOI,oBAAA;ELoiEV;;EK3iEM;IAOI,0BAAA;ELwiEV;;EK/iEM;IAOI,yBAAA;EL4iEV;;EKnjEM;IAOI,uBAAA;ELgjEV;;EKvjEM;IAOI,yBAAA;ELojEV;;EK3jEM;IAOI,uBAAA;ELwjEV;;EK/jEM;IAOI,uBAAA;EL4jEV;;EKnkEM;IAOI,yBAAA;IAAA,0BAAA;ELikEV;;EKxkEM;IAOI,+BAAA;IAAA,gCAAA;ELskEV;;EK7kEM;IAOI,8BAAA;IAAA,+BAAA;EL2kEV;;EKllEM;IAOI,4BAAA;IAAA,6BAAA;ELglEV;;EKvlEM;IAOI,8BAAA;IAAA,+BAAA;ELqlEV;;EK5lEM;IAOI,4BAAA;IAAA,6BAAA;EL0lEV;;EKjmEM;IAOI,4BAAA;IAAA,6BAAA;EL+lEV;;EKtmEM;IAOI,wBAAA;IAAA,2BAAA;ELomEV;;EK3mEM;IAOI,8BAAA;IAAA,iCAAA;ELymEV;;EKhnEM;IAOI,6BAAA;IAAA,gCAAA;EL8mEV;;EKrnEM;IAOI,2BAAA;IAAA,8BAAA;ELmnEV;;EK1nEM;IAOI,6BAAA;IAAA,gCAAA;ELwnEV;;EK/nEM;IAOI,2BAAA;IAAA,8BAAA;EL6nEV;;EKpoEM;IAOI,2BAAA;IAAA,8BAAA;ELkoEV;;EKzoEM;IAOI,wBAAA;ELsoEV;;EK7oEM;IAOI,8BAAA;EL0oEV;;EKjpEM;IAOI,6BAAA;EL8oEV;;EKrpEM;IAOI,2BAAA;ELkpEV;;EKzpEM;IAOI,6BAAA;ELspEV;;EK7pEM;IAOI,2BAAA;EL0pEV;;EKjqEM;IAOI,2BAAA;EL8pEV;;EKrqEM;IAOI,yBAAA;ELkqEV;;EKzqEM;IAOI,+BAAA;ELsqEV;;EK7qEM;IAOI,8BAAA;EL0qEV;;EKjrEM;IAOI,4BAAA;EL8qEV;;EKrrEM;IAOI,8BAAA;ELkrEV;;EKzrEM;IAOI,4BAAA;ELsrEV;;EK7rEM;IAOI,4BAAA;EL0rEV;;EKjsEM;IAOI,2BAAA;EL8rEV;;EKrsEM;IAOI,iCAAA;ELksEV;;EKzsEM;IAOI,gCAAA;ELssEV;;EK7sEM;IAOI,8BAAA;EL0sEV;;EKjtEM;IAOI,gCAAA;EL8sEV;;EKrtEM;IAOI,8BAAA;ELktEV;;EKztEM;IAOI,8BAAA;ELstEV;;EK7tEM;IAOI,0BAAA;EL0tEV;;EKjuEM;IAOI,gCAAA;EL8tEV;;EKruEM;IAOI,+BAAA;ELkuEV;;EKzuEM;IAOI,6BAAA;ELsuEV;;EK7uEM;IAOI,+BAAA;EL0uEV;;EKjvEM;IAOI,6BAAA;EL8uEV;;EKrvEM;IAOI,6BAAA;ELkvEV;;EKzvEM;IAOI,qBAAA;ELsvEV;;EK7vEM;IAOI,2BAAA;EL0vEV;;EKjwEM;IAOI,0BAAA;EL8vEV;;EKrwEM;IAOI,wBAAA;ELkwEV;;EKzwEM;IAOI,0BAAA;ELswEV;;EK7wEM;IAOI,wBAAA;EL0wEV;;EKjxEM;IAOI,0BAAA;IAAA,2BAAA;EL+wEV;;EKtxEM;IAOI,gCAAA;IAAA,iCAAA;ELoxEV;;EK3xEM;IAOI,+BAAA;IAAA,gCAAA;ELyxEV;;EKhyEM;IAOI,6BAAA;IAAA,8BAAA;EL8xEV;;EKryEM;IAOI,+BAAA;IAAA,gCAAA;ELmyEV;;EK1yEM;IAOI,6BAAA;IAAA,8BAAA;ELwyEV;;EK/yEM;IAOI,yBAAA;IAAA,4BAAA;EL6yEV;;EKpzEM;IAOI,+BAAA;IAAA,kCAAA;ELkzEV;;EKzzEM;IAOI,8BAAA;IAAA,iCAAA;ELuzEV;;EK9zEM;IAOI,4BAAA;IAAA,+BAAA;EL4zEV;;EKn0EM;IAOI,8BAAA;IAAA,iCAAA;ELi0EV;;EKx0EM;IAOI,4BAAA;IAAA,+BAAA;ELs0EV;;EK70EM;IAOI,yBAAA;EL00EV;;EKj1EM;IAOI,+BAAA;EL80EV;;EKr1EM;IAOI,8BAAA;ELk1EV;;EKz1EM;IAOI,4BAAA;ELs1EV;;EK71EM;IAOI,8BAAA;EL01EV;;EKj2EM;IAOI,4BAAA;EL81EV;;EKr2EM;IAOI,0BAAA;ELk2EV;;EKz2EM;IAOI,gCAAA;ELs2EV;;EK72EM;IAOI,+BAAA;EL02EV;;EKj3EM;IAOI,6BAAA;EL82EV;;EKr3EM;IAOI,+BAAA;ELk3EV;;EKz3EM;IAOI,6BAAA;ELs3EV;;EK73EM;IAOI,4BAAA;EL03EV;;EKj4EM;IAOI,kCAAA;EL83EV;;EKr4EM;IAOI,iCAAA;ELk4EV;;EKz4EM;IAOI,+BAAA;ELs4EV;;EK74EM;IAOI,iCAAA;EL04EV;;EKj5EM;IAOI,+BAAA;EL84EV;;EKr5EM;IAOI,2BAAA;ELk5EV;;EKz5EM;IAOI,iCAAA;ELs5EV;;EK75EM;IAOI,gCAAA;EL05EV;;EKj6EM;IAOI,8BAAA;EL85EV;;EKr6EM;IAOI,gCAAA;ELk6EV;;EKz6EM;IAOI,8BAAA;ELs6EV;AACF;AC96EI;EIAI;IAOI,0BAAA;EL26EV;;EKl7EM;IAOI,gCAAA;EL+6EV;;EKt7EM;IAOI,yBAAA;ELm7EV;;EK17EM;IAOI,wBAAA;ELu7EV;;EK97EM;IAOI,yBAAA;EL27EV;;EKl8EM;IAOI,6BAAA;EL+7EV;;EKt8EM;IAOI,8BAAA;ELm8EV;;EK18EM;IAOI,wBAAA;ELu8EV;;EK98EM;IAOI,+BAAA;EL28EV;;EKl9EM;IAOI,wBAAA;EL+8EV;;EKt9EM;IAOI,yBAAA;ELm9EV;;EK19EM;IAOI,8BAAA;ELu9EV;;EK99EM;IAOI,iCAAA;EL29EV;;EKl+EM;IAOI,sCAAA;EL+9EV;;EKt+EM;IAOI,yCAAA;ELm+EV;;EK1+EM;IAOI,uBAAA;ELu+EV;;EK9+EM;IAOI,uBAAA;EL2+EV;;EKl/EM;IAOI,yBAAA;EL++EV;;EKt/EM;IAOI,yBAAA;ELm/EV;;EK1/EM;IAOI,0BAAA;ELu/EV;;EK9/EM;IAOI,4BAAA;EL2/EV;;EKlgFM;IAOI,kCAAA;EL+/EV;;EKtgFM;IAOI,sCAAA;ELmgFV;;EK1gFM;IAOI,oCAAA;ELugFV;;EK9gFM;IAOI,kCAAA;EL2gFV;;EKlhFM;IAOI,yCAAA;EL+gFV;;EKthFM;IAOI,wCAAA;ELmhFV;;EK1hFM;IAOI,wCAAA;ELuhFV;;EK9hFM;IAOI,kCAAA;EL2hFV;;EKliFM;IAOI,gCAAA;EL+hFV;;EKtiFM;IAOI,8BAAA;ELmiFV;;EK1iFM;IAOI,gCAAA;ELuiFV;;EK9iFM;IAOI,+BAAA;EL2iFV;;EKljFM;IAOI,oCAAA;EL+iFV;;EKtjFM;IAOI,kCAAA;ELmjFV;;EK1jFM;IAOI,gCAAA;ELujFV;;EK9jFM;IAOI,uCAAA;EL2jFV;;EKlkFM;IAOI,sCAAA;EL+jFV;;EKtkFM;IAOI,iCAAA;ELmkFV;;EK1kFM;IAOI,2BAAA;ELukFV;;EK9kFM;IAOI,iCAAA;EL2kFV;;EKllFM;IAOI,+BAAA;EL+kFV;;EKtlFM;IAOI,6BAAA;ELmlFV;;EK1lFM;IAOI,+BAAA;ELulFV;;EK9lFM;IAOI,8BAAA;EL2lFV;;EKlmFM;IAOI,oBAAA;EL+lFV;;EKtmFM;IAOI,mBAAA;ELmmFV;;EK1mFM;IAOI,mBAAA;ELumFV;;EK9mFM;IAOI,mBAAA;EL2mFV;;EKlnFM;IAOI,mBAAA;EL+mFV;;EKtnFM;IAOI,mB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FV;;EKx9FM;IAOI,4BAAA;ELq9FV;;EK59FM;IAOI,kCAAA;ELy9FV;;EKh+FM;IAOI,iCAAA;EL69FV;;EKp+FM;IAOI,+BAAA;ELi+FV;;EKx+FM;IAOI,iCAAA;ELq+FV;;EK5+FM;IAOI,+BAAA;ELy+FV;;EKh/FM;IAOI,2BAAA;EL6+FV;;EKp/FM;IAOI,iCAAA;ELi/FV;;EKx/FM;IAOI,gCAAA;ELq/FV;;EK5/FM;IAOI,8BAAA;ELy/FV;;EKhgGM;IAOI,gCAAA;EL6/FV;;EKpgGM;IAOI,8BAAA;ELigGV;AACF;ACzgGI;EIAI;IAOI,0BAAA;ELsgGV;;EK7gGM;IAOI,gCAAA;EL0gGV;;EKjhGM;IAOI,yBAAA;EL8gGV;;EKrhGM;IAOI,wBAAA;ELkhGV;;EKzhGM;IAOI,yBAAA;ELshGV;;EK7hGM;IAOI,6BAAA;EL0hGV;;EKjiGM;IAOI,8BAAA;EL8hGV;;EKriGM;IAOI,wBAAA;ELkiGV;;EKziGM;IAOI,+BAAA;ELsiGV;;EK7iGM;IAOI,wBAAA;EL0iGV;;EKjjGM;IAOI,yBAAA;EL8iGV;;EKrjGM;IAOI,8BAAA;ELkjGV;;EKzjGM;IAOI,iCAAA;ELsjGV;;EK7jGM;IAOI,sCAAA;EL0jGV;;EKjkGM;IAOI,yCAAA;EL8jGV;;EKrkGM;IAOI,uBAAA;ELkkGV;;EKzkGM;IAOI,uBAAA;ELskGV;;EK7kGM;IAOI,yBAAA;EL0kGV;;EKjlGM;IAOI,yBAAA;EL8kGV;;EKrlGM;IAOI,0BAAA;ELklGV;;EKzlGM;IAOI,4BAAA;ELslGV;;EK7lGM;IAOI,kCAAA;EL0lGV;;EKjmGM;IAOI,sCAAA;EL8lGV;;EKrmGM;IAOI,oCAAA;ELkmGV;;EKzmGM;IAOI,kCAAA;ELsmGV;;EK7mGM;IAOI,yCAAA;EL0mGV;;EKjnGM;IAOI,wCAAA;EL8mGV;;EKrnGM;IAOI,wCAAA;ELknGV;;EKznGM;IAOI,kCAAA;ELsnGV;;EK7nGM;IAOI,gCAAA;EL0nGV;;EKjoGM;IAOI,8BAAA;EL8nGV;;EKroGM;IAOI,gCAAA;ELkoGV;;EKzoGM;IAOI,+BAAA;ELsoGV;;EK7oGM;IAOI,oCAAA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/rII;EIAI;IAOI,0BAAA;EL4rIV;;EKnsIM;IAOI,gCAAA;ELgsIV;;EKvsIM;IAOI,yBAAA;ELosIV;;EK3sIM;IAOI,wBAAA;ELwsIV;;EK/sIM;IAOI,yBAAA;EL4sIV;;EKntIM;IAOI,6BAAA;ELgtIV;;EKvtIM;IAOI,8BAAA;ELotIV;;EK3tIM;IAOI,wBAAA;ELwtIV;;EK/tIM;IAOI,+BAAA;EL4tIV;;EKnuIM;IAOI,wBAAA;ELguIV;;EKvuIM;IAOI,yBAAA;ELouIV;;EK3uIM;IAOI,8BAAA;ELwuIV;;EK/uIM;IAOI,iCAAA;EL4uIV;;EKnvIM;IAOI,sCAAA;ELgvIV;;EKvvIM;IAOI,yCAAA;ELovIV;;EK3vIM;IAOI,uBAAA;ELwvIV;;EK/vIM;IAOI,uBAAA;EL4vIV;;EKnwIM;IAOI,yBAAA;ELgwIV;;EKvwIM;IAOI,yBAAA;ELowIV;;EK3wIM;IAOI,0BAAA;ELwwIV;;EK/wIM;IAOI,4BAAA;EL4wIV;;EKnxIM;IAOI,kCAAA;ELgxIV;;EKvxIM;IAOI,sCAAA;ELoxIV;;EK3xIM;IAOI,oCAAA;ELwxIV;;EK/xIM;IAOI,kCAAA;EL4xIV;;EKnyIM;IAOI,yCAAA;ELgyIV;;EKvyIM;IAOI,wCAAA;ELoyIV;;EK3yIM;IAOI,wCAAA;ELwyIV;;EK/yIM;IAOI,kCAAA;EL4yIV;;EKnzIM;IAOI,gCAAA;ELgzIV;;EKvzIM;IAOI,8BAAA;ELozIV;;EK3zIM;IAOI,gCAAA;ELwzIV;;EK/zIM;IAOI,+BAAA;EL4zIV;;EKn0IM;IAOI,oCAAA;ELg0IV;;EKv0IM;IAOI,kCAAA;ELo0IV;;EK30IM;IAOI,gCAAA;ELw0IV;;EK/0IM;IAOI,uCAAA;EL40IV;;EKn1IM;IAOI,sCAAA;ELg1IV;;EKv1IM;IAOI,iCAAA;ELo1IV;;EK31IM;IAOI,2BAAA;ELw1IV;;EK/1IM;IAOI,iCAAA;EL41IV;;EKn2IM;IAOI,+BAAA;ELg2IV;;EKv2IM;IAOI,6BAAA;ELo2IV;;EK32IM;IAOI,+BAAA;ELw2IV;;EK/2IM;IAOI,8BAAA;EL42IV;;EKn3IM;IAOI,oBAAA;ELg3IV;;E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OI,gCAAA;ELktJV;;EKztJM;IAOI,+BAAA;ELstJV;;EK7tJM;IAOI,6BAAA;EL0tJV;;EKjuJM;IAOI,+BAAA;EL8tJV;;EKruJM;IAOI,6BAAA;ELkuJV;;EKzuJM;IAOI,4BAAA;ELsuJV;;EK7uJM;IAOI,kCAAA;EL0uJV;;EKjvJM;IAOI,iCAAA;EL8uJV;;EKrvJM;IAOI,+BAAA;ELkvJV;;EKzvJM;IAOI,iCAAA;ELsvJV;;EK7vJM;IAOI,+BAAA;EL0vJV;;EKjwJM;IAOI,2BAAA;EL8vJV;;EKrwJM;IAOI,iCAAA;ELkwJV;;EKzwJM;IAOI,gCAAA;ELswJV;;EK7wJM;IAOI,8BAAA;EL0wJV;;EKjxJM;IAOI,gCAAA;EL8wJV;;EKrxJM;IAOI,8BAAA;ELkxJV;AACF;AMnzJA;EDyBQ;IAOI,0BAAA;ELuxJV;;EK9xJM;IAOI,gCAAA;EL2xJV;;EKlyJM;IAOI,yBAAA;EL+xJV;;EKtyJM;IAOI,wBAAA;ELmyJV;;EK1yJM;IAOI,yBAAA;ELuyJV;;EK9yJM;IAOI,6BAAA;EL2yJV;;EKlzJM;IAOI,8BAAA;EL+yJV;;EKtzJM;IAOI,wBAAA;ELmzJV;;EK1zJM;IAOI,+BAAA;ELuzJV;;EK9zJM;IAOI,wBAAA;EL2zJV;AACF","file":"bootstrap-grid.rtl.css","sourcesContent":["/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n$include-column-box-sizing: true !default;\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/lists\";\n@import \"mixins/breakpoints\";\n@import \"mixins/container\";\n@import \"mixins/grid\";\n@import \"mixins/utilities\";\n\n@import \"vendor/rfs\";\n\n@import \"containers\";\n@import \"grid\";\n\n@import \"utilities\";\n// Only use the utilities we need\n// stylelint-disable-next-line scss/dollar-variable-default\n$utilities: map-get-multiple(\n $utilities,\n (\n \"display\",\n \"order\",\n \"flex\",\n \"flex-direction\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-wrap\",\n \"justify-content\",\n \"align-items\",\n \"align-content\",\n \"align-self\",\n \"margin\",\n \"margin-x\",\n \"margin-y\",\n \"margin-top\",\n \"margin-end\",\n \"margin-bottom\",\n \"margin-start\",\n \"negative-margin\",\n \"negative-margin-x\",\n \"negative-margin-y\",\n \"negative-margin-top\",\n \"negative-margin-end\",\n \"negative-margin-bottom\",\n \"negative-margin-start\",\n \"padding\",\n \"padding-x\",\n \"padding-y\",\n \"padding-top\",\n \"padding-end\",\n \"padding-bottom\",\n \"padding-start\",\n )\n);\n\n@import \"utilities/api\";\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n width: 100%;\n padding-right: var(--#{$variable-prefix}gutter-x, #{$gutter});\n padding-left: var(--#{$variable-prefix}gutter-x, #{$gutter});\n margin-right: auto;\n margin-left: auto;\n}\n","/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * .5);\n padding-left: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n// scss-docs-start gray-color-variables\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n// scss-docs-end gray-color-variables\n\n// fusv-disable\n// scss-docs-start gray-colors-map\n$grays: (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n) !default;\n// scss-docs-end gray-colors-map\n// fusv-enable\n\n// scss-docs-start color-variables\n$blue: #0d6efd !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #d63384 !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #198754 !default;\n$teal: #20c997 !default;\n$cyan: #0dcaf0 !default;\n// scss-docs-end color-variables\n\n// scss-docs-start colors-map\n$colors: (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n) !default;\n// scss-docs-end colors-map\n\n// scss-docs-start theme-color-variables\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-900 !default;\n// scss-docs-end theme-color-variables\n\n// scss-docs-start theme-colors-map\n$theme-colors: (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n) !default;\n// scss-docs-end theme-colors-map\n\n// scss-docs-start theme-colors-rgb\n$theme-colors-rgb: map-loop($theme-colors, to-rgb, \"$value\") !default;\n// scss-docs-end theme-colors-rgb\n\n// The contrast ratio to reach against white, to determine if color changes from \"light\" to \"dark\". Acceptable values for WCAG 2.0 are 3, 4.5 and 7.\n// See https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast\n$min-contrast-ratio: 4.5 !default;\n\n// Customize the light and dark text colors for use in our color contrast function.\n$color-contrast-dark: $black !default;\n$color-contrast-light: $white !default;\n\n// fusv-disable\n$blue-100: tint-color($blue, 80%) !default;\n$blue-200: tint-color($blue, 60%) !default;\n$blue-300: tint-color($blue, 40%) !default;\n$blue-400: tint-color($blue, 20%) !default;\n$blue-500: $blue !default;\n$blue-600: shade-color($blue, 20%) !default;\n$blue-700: shade-color($blue, 40%) !default;\n$blue-800: shade-color($blue, 60%) !default;\n$blue-900: shade-color($blue, 80%) !default;\n\n$indigo-100: tint-color($indigo, 80%) !default;\n$indigo-200: tint-color($indigo, 60%) !default;\n$indigo-300: tint-color($indigo, 40%) !default;\n$indigo-400: tint-color($indigo, 20%) !default;\n$indigo-500: $indigo !default;\n$indigo-600: shade-color($indigo, 20%) !default;\n$indigo-700: shade-color($indigo, 40%) !default;\n$indigo-800: shade-color($indigo, 60%) !default;\n$indigo-900: shade-color($indigo, 80%) !default;\n\n$purple-100: tint-color($purple, 80%) !default;\n$purple-200: tint-color($purple, 60%) !default;\n$purple-300: tint-color($purple, 40%) !default;\n$purple-400: tint-color($purple, 20%) !default;\n$purple-500: $purple !default;\n$purple-600: shade-color($purple, 20%) !default;\n$purple-700: shade-color($purple, 40%) !default;\n$purple-800: shade-color($purple, 60%) !default;\n$purple-900: shade-color($purple, 80%) !default;\n\n$pink-100: tint-color($pink, 80%) !default;\n$pink-200: tint-color($pink, 60%) !default;\n$pink-300: tint-color($pink, 40%) !default;\n$pink-400: tint-color($pink, 20%) !default;\n$pink-500: $pink !default;\n$pink-600: shade-color($pink, 20%) !default;\n$pink-700: shade-color($pink, 40%) !default;\n$pink-800: shade-color($pink, 60%) !default;\n$pink-900: shade-color($pink, 80%) !default;\n\n$red-100: tint-color($red, 80%) !default;\n$red-200: tint-color($red, 60%) !default;\n$red-300: tint-color($red, 40%) !default;\n$red-400: tint-color($red, 20%) !default;\n$red-500: $red !default;\n$red-600: shade-color($red, 20%) !default;\n$red-700: shade-color($red, 40%) !default;\n$red-800: shade-color($red, 60%) !default;\n$red-900: shade-color($red, 80%) !default;\n\n$orange-100: tint-color($orange, 80%) !default;\n$orange-200: tint-color($orange, 60%) !default;\n$orange-300: tint-color($orange, 40%) !default;\n$orange-400: tint-color($orange, 20%) !default;\n$orange-500: $orange !default;\n$orange-600: shade-color($orange, 20%) !default;\n$orange-700: shade-color($orange, 40%) !default;\n$orange-800: shade-color($orange, 60%) !default;\n$orange-900: shade-color($orange, 80%) !default;\n\n$yellow-100: tint-color($yellow, 80%) !default;\n$yellow-200: tint-color($yellow, 60%) !default;\n$yellow-300: tint-color($yellow, 40%) !default;\n$yellow-400: tint-color($yellow, 20%) !default;\n$yellow-500: $yellow !default;\n$yellow-600: shade-color($yellow, 20%) !default;\n$yellow-700: shade-color($yellow, 40%) !default;\n$yellow-800: shade-color($yellow, 60%) !default;\n$yellow-900: shade-color($yellow, 80%) !default;\n\n$green-100: tint-color($green, 80%) !default;\n$green-200: tint-color($green, 60%) !default;\n$green-300: tint-color($green, 40%) !default;\n$green-400: tint-color($green, 20%) !default;\n$green-500: $green !default;\n$green-600: shade-color($green, 20%) !default;\n$green-700: shade-color($green, 40%) !default;\n$green-800: shade-color($green, 60%) !default;\n$green-900: shade-color($green, 80%) !default;\n\n$teal-100: tint-color($teal, 80%) !default;\n$teal-200: tint-color($teal, 60%) !default;\n$teal-300: tint-color($teal, 40%) !default;\n$teal-400: tint-color($teal, 20%) !default;\n$teal-500: $teal !default;\n$teal-600: shade-color($teal, 20%) !default;\n$teal-700: shade-color($teal, 40%) !default;\n$teal-800: shade-color($teal, 60%) !default;\n$teal-900: shade-color($teal, 80%) !default;\n\n$cyan-100: tint-color($cyan, 80%) !default;\n$cyan-200: tint-color($cyan, 60%) !default;\n$cyan-300: tint-color($cyan, 40%) !default;\n$cyan-400: tint-color($cyan, 20%) !default;\n$cyan-500: $cyan !default;\n$cyan-600: shade-color($cyan, 20%) !default;\n$cyan-700: shade-color($cyan, 40%) !default;\n$cyan-800: shade-color($cyan, 60%) !default;\n$cyan-900: shade-color($cyan, 80%) !default;\n\n$blues: (\n \"blue-100\": $blue-100,\n \"blue-200\": $blue-200,\n \"blue-300\": $blue-300,\n \"blue-400\": $blue-400,\n \"blue-500\": $blue-500,\n \"blue-600\": $blue-600,\n \"blue-700\": $blue-700,\n \"blue-800\": $blue-800,\n \"blue-900\": $blue-900\n) !default;\n\n$indigos: (\n \"indigo-100\": $indigo-100,\n \"indigo-200\": $indigo-200,\n \"indigo-300\": $indigo-300,\n \"indigo-400\": $indigo-400,\n \"indigo-500\": $indigo-500,\n \"indigo-600\": $indigo-600,\n \"indigo-700\": $indigo-700,\n \"indigo-800\": $indigo-800,\n \"indigo-900\": $indigo-900\n) !default;\n\n$purples: (\n \"purple-100\": $purple-200,\n \"purple-200\": $purple-100,\n \"purple-300\": $purple-300,\n \"purple-400\": $purple-400,\n \"purple-500\": $purple-500,\n \"purple-600\": $purple-600,\n \"purple-700\": $purple-700,\n \"purple-800\": $purple-800,\n \"purple-900\": $purple-900\n) !default;\n\n$pinks: (\n \"pink-100\": $pink-100,\n \"pink-200\": $pink-200,\n \"pink-300\": $pink-300,\n \"pink-400\": $pink-400,\n \"pink-500\": $pink-500,\n \"pink-600\": $pink-600,\n \"pink-700\": $pink-700,\n \"pink-800\": $pink-800,\n \"pink-900\": $pink-900\n) !default;\n\n$reds: (\n \"red-100\": $red-100,\n \"red-200\": $red-200,\n \"red-300\": $red-300,\n \"red-400\": $red-400,\n \"red-500\": $red-500,\n \"red-600\": $red-600,\n \"red-700\": $red-700,\n \"red-800\": $red-800,\n \"red-900\": $red-900\n) !default;\n\n$oranges: (\n \"orange-100\": $orange-100,\n \"orange-200\": $orange-200,\n \"orange-300\": $orange-300,\n \"orange-400\": $orange-400,\n \"orange-500\": $orange-500,\n \"orange-600\": $orange-600,\n \"orange-700\": $orange-700,\n \"orange-800\": $orange-800,\n \"orange-900\": $orange-900\n) !default;\n\n$yellows: (\n \"yellow-100\": $yellow-100,\n \"yellow-200\": $yellow-200,\n \"yellow-300\": $yellow-300,\n \"yellow-400\": $yellow-400,\n \"yellow-500\": $yellow-500,\n \"yellow-600\": $yellow-600,\n \"yellow-700\": $yellow-700,\n \"yellow-800\": $yellow-800,\n \"yellow-900\": $yellow-900\n) !default;\n\n$greens: (\n \"green-100\": $green-100,\n \"green-200\": $green-200,\n \"green-300\": $green-300,\n \"green-400\": $green-400,\n \"green-500\": $green-500,\n \"green-600\": $green-600,\n \"green-700\": $green-700,\n \"green-800\": $green-800,\n \"green-900\": $green-900\n) !default;\n\n$teals: (\n \"teal-100\": $teal-100,\n \"teal-200\": $teal-200,\n \"teal-300\": $teal-300,\n \"teal-400\": $teal-400,\n \"teal-500\": $teal-500,\n \"teal-600\": $teal-600,\n \"teal-700\": $teal-700,\n \"teal-800\": $teal-800,\n \"teal-900\": $teal-900\n) !default;\n\n$cyans: (\n \"cyan-100\": $cyan-100,\n \"cyan-200\": $cyan-200,\n \"cyan-300\": $cyan-300,\n \"cyan-400\": $cyan-400,\n \"cyan-500\": $cyan-500,\n \"cyan-600\": $cyan-600,\n \"cyan-700\": $cyan-700,\n \"cyan-800\": $cyan-800,\n \"cyan-900\": $cyan-900\n) !default;\n// fusv-enable\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-reduced-motion: true !default;\n$enable-smooth-scroll: true !default;\n$enable-grid-classes: true !default;\n$enable-cssgrid: false !default;\n$enable-button-pointers: true !default;\n$enable-rfs: true !default;\n$enable-validation-icons: true !default;\n$enable-negative-margins: false !default;\n$enable-deprecation-messages: true !default;\n$enable-important-utilities: true !default;\n\n// Prefix for :root CSS variables\n\n$variable-prefix: bs- !default;\n\n// Gradient\n//\n// The gradient which is added to components if `$enable-gradients` is `true`\n// This gradient is also added to elements with `.bg-gradient`\n// scss-docs-start variable-gradient\n$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default;\n// scss-docs-end variable-gradient\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n// scss-docs-start spacer-variables-maps\n$spacer: 1rem !default;\n$spacers: (\n 0: 0,\n 1: $spacer * .25,\n 2: $spacer * .5,\n 3: $spacer,\n 4: $spacer * 1.5,\n 5: $spacer * 3,\n) !default;\n\n$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default;\n// scss-docs-end spacer-variables-maps\n\n// Position\n//\n// Define the edge positioning anchors of the position utilities.\n\n// scss-docs-start position-map\n$position-values: (\n 0: 0,\n 50: 50%,\n 100: 100%\n) !default;\n// scss-docs-end position-map\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n$body-text-align: null !default;\n\n// Utilities maps\n//\n// Extends the default `$theme-colors` maps to help create our utilities.\n\n// scss-docs-start utilities-colors\n$utilities-colors: map-merge(\n $theme-colors-rgb,\n (\n \"black\": to-rgb($black),\n \"white\": to-rgb($white),\n \"body\": to-rgb($body-color)\n )\n) !default;\n// scss-docs-end utilities-colors\n\n// scss-docs-start utilities-text-colors\n$utilities-text-colors: map-loop($utilities-colors, rgba-css-var, \"$key\", \"text\") !default;\n// scss-docs-end utilities-text-colors\n\n// scss-docs-start utilities-bg-colors\n$utilities-bg-colors: map-loop($utilities-colors, rgba-css-var, \"$key\", \"bg\") !default;\n// scss-docs-end utilities-bg-colors\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: $primary !default;\n$link-decoration: underline !default;\n$link-shade-percentage: 20% !default;\n$link-hover-color: shift-color($link-color, $link-shade-percentage) !default;\n$link-hover-decoration: null !default;\n\n$stretched-link-pseudo-element: after !default;\n$stretched-link-z-index: 1 !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n// scss-docs-start grid-breakpoints\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px,\n xxl: 1400px\n) !default;\n// scss-docs-end grid-breakpoints\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n// scss-docs-start container-max-widths\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px,\n xxl: 1320px\n) !default;\n// scss-docs-end container-max-widths\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 1.5rem !default;\n$grid-row-columns: 6 !default;\n\n$gutters: $spacers !default;\n\n// Container padding\n\n$container-padding-x: $grid-gutter-width * .5 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n// scss-docs-start border-variables\n$border-width: 1px !default;\n$border-widths: (\n 1: 1px,\n 2: 2px,\n 3: 3px,\n 4: 4px,\n 5: 5px\n) !default;\n\n$border-color: $gray-300 !default;\n// scss-docs-end border-variables\n\n// scss-docs-start border-radius-variables\n$border-radius: .25rem !default;\n$border-radius-sm: .2rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-pill: 50rem !default;\n// scss-docs-end border-radius-variables\n\n// scss-docs-start box-shadow-variables\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default;\n// scss-docs-end box-shadow-variables\n\n$component-active-color: $white !default;\n$component-active-bg: $primary !default;\n\n// scss-docs-start caret-variables\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n// scss-docs-end caret-variables\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n// scss-docs-start collapse-transition\n$transition-collapse: height .35s ease !default;\n$transition-collapse-width: width .35s ease !default;\n// scss-docs-end collapse-transition\n\n// stylelint-disable function-disallowed-list\n// scss-docs-start aspect-ratios\n$aspect-ratios: (\n \"1x1\": 100%,\n \"4x3\": calc(3 / 4 * 100%),\n \"16x9\": calc(9 / 16 * 100%),\n \"21x9\": calc(9 / 21 * 100%)\n) !default;\n// scss-docs-end aspect-ratios\n// stylelint-enable function-disallowed-list\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// scss-docs-start font-variables\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Liberation Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n// stylelint-enable value-keyword-case\n$font-family-base: var(--#{$variable-prefix}font-sans-serif) !default;\n$font-family-code: var(--#{$variable-prefix}font-monospace) !default;\n\n// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins\n// $font-size-base affects the font size of the body text\n$font-size-root: null !default;\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-sm: $font-size-base * .875 !default;\n$font-size-lg: $font-size-base * 1.25 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n\n$line-height-base: 1.5 !default;\n$line-height-sm: 1.25 !default;\n$line-height-lg: 2 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n// scss-docs-end font-variables\n\n// scss-docs-start font-sizes\n$font-sizes: (\n 1: $h1-font-size,\n 2: $h2-font-size,\n 3: $h3-font-size,\n 4: $h4-font-size,\n 5: $h5-font-size,\n 6: $h6-font-size\n) !default;\n// scss-docs-end font-sizes\n\n// scss-docs-start headings-variables\n$headings-margin-bottom: $spacer * .5 !default;\n$headings-font-family: null !default;\n$headings-font-style: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n// scss-docs-end headings-variables\n\n// scss-docs-start display-headings\n$display-font-sizes: (\n 1: 5rem,\n 2: 4.5rem,\n 3: 4rem,\n 4: 3.5rem,\n 5: 3rem,\n 6: 2.5rem\n) !default;\n\n$display-font-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n// scss-docs-end display-headings\n\n// scss-docs-start type-variables\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: .875em !default;\n\n$sub-sup-font-size: .75em !default;\n\n$text-muted: $gray-600 !default;\n\n$initialism-font-size: $small-font-size !default;\n\n$blockquote-margin-y: $spacer !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n$blockquote-footer-color: $gray-600 !default;\n$blockquote-footer-font-size: $small-font-size !default;\n\n$hr-margin-y: $spacer !default;\n$hr-color: inherit !default;\n$hr-height: $border-width !default;\n$hr-opacity: .25 !default;\n\n$legend-margin-bottom: .5rem !default;\n$legend-font-size: 1.5rem !default;\n$legend-font-weight: null !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n// scss-docs-end type-variables\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n// scss-docs-start table-variables\n$table-cell-padding-y: .5rem !default;\n$table-cell-padding-x: .5rem !default;\n$table-cell-padding-y-sm: .25rem !default;\n$table-cell-padding-x-sm: .25rem !default;\n\n$table-cell-vertical-align: top !default;\n\n$table-color: $body-color !default;\n$table-bg: transparent !default;\n$table-accent-bg: transparent !default;\n\n$table-th-font-weight: null !default;\n\n$table-striped-color: $table-color !default;\n$table-striped-bg-factor: .05 !default;\n$table-striped-bg: rgba($black, $table-striped-bg-factor) !default;\n\n$table-active-color: $table-color !default;\n$table-active-bg-factor: .1 !default;\n$table-active-bg: rgba($black, $table-active-bg-factor) !default;\n\n$table-hover-color: $table-color !default;\n$table-hover-bg-factor: .075 !default;\n$table-hover-bg: rgba($black, $table-hover-bg-factor) !default;\n\n$table-border-factor: .1 !default;\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-striped-order: odd !default;\n\n$table-group-separator-color: currentColor !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-scale: -80% !default;\n// scss-docs-end table-variables\n\n// scss-docs-start table-loop\n$table-variants: (\n \"primary\": shift-color($primary, $table-bg-scale),\n \"secondary\": shift-color($secondary, $table-bg-scale),\n \"success\": shift-color($success, $table-bg-scale),\n \"info\": shift-color($info, $table-bg-scale),\n \"warning\": shift-color($warning, $table-bg-scale),\n \"danger\": shift-color($danger, $table-bg-scale),\n \"light\": $light,\n \"dark\": $dark,\n) !default;\n// scss-docs-end table-loop\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n// scss-docs-start input-btn-variables\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .25rem !default;\n$input-btn-focus-color-opacity: .25 !default;\n$input-btn-focus-color: rgba($component-active-bg, $input-btn-focus-color-opacity) !default;\n$input-btn-focus-blur: 0 !default;\n$input-btn-focus-box-shadow: 0 0 $input-btn-focus-blur $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n\n$input-btn-border-width: $border-width !default;\n// scss-docs-end input-btn-variables\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n// scss-docs-start btn-variables\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-color: $link-color !default;\n$btn-link-hover-color: $link-hover-color !default;\n$btn-link-disabled-color: $gray-600 !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$btn-hover-bg-shade-amount: 15% !default;\n$btn-hover-bg-tint-amount: 15% !default;\n$btn-hover-border-shade-amount: 20% !default;\n$btn-hover-border-tint-amount: 10% !default;\n$btn-active-bg-shade-amount: 20% !default;\n$btn-active-bg-tint-amount: 20% !default;\n$btn-active-border-shade-amount: 25% !default;\n$btn-active-border-tint-amount: 10% !default;\n// scss-docs-end btn-variables\n\n\n// Forms\n\n// scss-docs-start form-text-variables\n$form-text-margin-top: .25rem !default;\n$form-text-font-size: $small-font-size !default;\n$form-text-font-style: null !default;\n$form-text-font-weight: null !default;\n$form-text-color: $text-muted !default;\n// scss-docs-end form-text-variables\n\n// scss-docs-start form-label-variables\n$form-label-margin-bottom: .5rem !default;\n$form-label-font-size: null !default;\n$form-label-font-style: null !default;\n$form-label-font-weight: null !default;\n$form-label-color: null !default;\n// scss-docs-end form-label-variables\n\n// scss-docs-start form-input-variables\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n\n$input-bg: $body-bg !default;\n$input-disabled-bg: $gray-200 !default;\n$input-disabled-border-color: null !default;\n\n$input-color: $body-color !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: $box-shadow-inset !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-sm: $border-radius-sm !default;\n$input-border-radius-lg: $border-radius-lg !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: tint-color($component-active-bg, 50%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-color-width: 3rem !default;\n// scss-docs-end form-input-variables\n\n// scss-docs-start form-check-variables\n$form-check-input-width: 1em !default;\n$form-check-min-height: $font-size-base * $line-height-base !default;\n$form-check-padding-start: $form-check-input-width + .5em !default;\n$form-check-margin-bottom: .125rem !default;\n$form-check-label-color: null !default;\n$form-check-label-cursor: null !default;\n$form-check-transition: null !default;\n\n$form-check-input-active-filter: brightness(90%) !default;\n\n$form-check-input-bg: $input-bg !default;\n$form-check-input-border: 1px solid rgba($black, .25) !default;\n$form-check-input-border-radius: .25em !default;\n$form-check-radio-border-radius: 50% !default;\n$form-check-input-focus-border: $input-focus-border-color !default;\n$form-check-input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$form-check-input-checked-color: $component-active-color !default;\n$form-check-input-checked-bg-color: $component-active-bg !default;\n$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default;\n$form-check-input-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-check-radio-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-indeterminate-color: $component-active-color !default;\n$form-check-input-indeterminate-bg-color: $component-active-bg !default;\n$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default;\n$form-check-input-indeterminate-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-disabled-opacity: .5 !default;\n$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default;\n$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default;\n\n$form-check-inline-margin-end: 1rem !default;\n// scss-docs-end form-check-variables\n\n// scss-docs-start form-switch-variables\n$form-switch-color: rgba(0, 0, 0, .25) !default;\n$form-switch-width: 2em !default;\n$form-switch-padding-start: $form-switch-width + .5em !default;\n$form-switch-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-border-radius: $form-switch-width !default;\n$form-switch-transition: background-position .15s ease-in-out !default;\n\n$form-switch-focus-color: $input-focus-border-color !default;\n$form-switch-focus-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-switch-checked-color: $component-active-color !default;\n$form-switch-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-checked-bg-position: right center !default;\n// scss-docs-end form-switch-variables\n\n// scss-docs-start input-group-variables\n$input-group-addon-padding-y: $input-padding-y !default;\n$input-group-addon-padding-x: $input-padding-x !default;\n$input-group-addon-font-weight: $input-font-weight !default;\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n// scss-docs-end input-group-variables\n\n// scss-docs-start form-select-variables\n$form-select-padding-y: $input-padding-y !default;\n$form-select-padding-x: $input-padding-x !default;\n$form-select-font-family: $input-font-family !default;\n$form-select-font-size: $input-font-size !default;\n$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image\n$form-select-font-weight: $input-font-weight !default;\n$form-select-line-height: $input-line-height !default;\n$form-select-color: $input-color !default;\n$form-select-bg: $input-bg !default;\n$form-select-disabled-color: null !default;\n$form-select-disabled-bg: $gray-200 !default;\n$form-select-disabled-border-color: $input-disabled-border-color !default;\n$form-select-bg-position: right $form-select-padding-x center !default;\n$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions\n$form-select-indicator-color: $gray-800 !default;\n$form-select-indicator: url(\"data:image/svg+xml,\") !default;\n\n$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default;\n$form-select-feedback-icon-position: center right $form-select-indicator-padding !default;\n$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$form-select-border-width: $input-border-width !default;\n$form-select-border-color: $input-border-color !default;\n$form-select-border-radius: $border-radius !default;\n$form-select-box-shadow: $box-shadow-inset !default;\n\n$form-select-focus-border-color: $input-focus-border-color !default;\n$form-select-focus-width: $input-focus-width !default;\n$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default;\n\n$form-select-padding-y-sm: $input-padding-y-sm !default;\n$form-select-padding-x-sm: $input-padding-x-sm !default;\n$form-select-font-size-sm: $input-font-size-sm !default;\n\n$form-select-padding-y-lg: $input-padding-y-lg !default;\n$form-select-padding-x-lg: $input-padding-x-lg !default;\n$form-select-font-size-lg: $input-font-size-lg !default;\n\n$form-select-transition: $input-transition !default;\n// scss-docs-end form-select-variables\n\n// scss-docs-start form-range-variables\n$form-range-track-width: 100% !default;\n$form-range-track-height: .5rem !default;\n$form-range-track-cursor: pointer !default;\n$form-range-track-bg: $gray-300 !default;\n$form-range-track-border-radius: 1rem !default;\n$form-range-track-box-shadow: $box-shadow-inset !default;\n\n$form-range-thumb-width: 1rem !default;\n$form-range-thumb-height: $form-range-thumb-width !default;\n$form-range-thumb-bg: $component-active-bg !default;\n$form-range-thumb-border: 0 !default;\n$form-range-thumb-border-radius: 1rem !default;\n$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge\n$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default;\n$form-range-thumb-disabled-bg: $gray-500 !default;\n$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n// scss-docs-end form-range-variables\n\n// scss-docs-start form-file-variables\n$form-file-button-color: $input-color !default;\n$form-file-button-bg: $input-group-addon-bg !default;\n$form-file-button-hover-bg: shade-color($form-file-button-bg, 5%) !default;\n// scss-docs-end form-file-variables\n\n// scss-docs-start form-floating-variables\n$form-floating-height: add(3.5rem, $input-height-border) !default;\n$form-floating-line-height: 1.25 !default;\n$form-floating-padding-x: $input-padding-x !default;\n$form-floating-padding-y: 1rem !default;\n$form-floating-input-padding-t: 1.625rem !default;\n$form-floating-input-padding-b: .625rem !default;\n$form-floating-label-opacity: .65 !default;\n$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default;\n$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default;\n// scss-docs-end form-floating-variables\n\n// Form validation\n\n// scss-docs-start form-feedback-variables\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $form-text-font-size !default;\n$form-feedback-font-style: $form-text-font-style !default;\n$form-feedback-valid-color: $success !default;\n$form-feedback-invalid-color: $danger !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end form-feedback-variables\n\n// scss-docs-start form-validation-states\n$form-validation-states: (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n )\n) !default;\n// scss-docs-end form-validation-states\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n// scss-docs-start zindex-stack\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-offcanvas-backdrop: 1040 !default;\n$zindex-offcanvas: 1045 !default;\n$zindex-modal-backdrop: 1050 !default;\n$zindex-modal: 1055 !default;\n$zindex-popover: 1070 !default;\n$zindex-tooltip: 1080 !default;\n// scss-docs-end zindex-stack\n\n\n// Navs\n\n// scss-docs-start nav-variables\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-font-size: null !default;\n$nav-link-font-weight: null !default;\n$nav-link-color: $link-color !default;\n$nav-link-hover-color: $link-hover-color !default;\n$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n// scss-docs-end nav-variables\n\n\n// Navbar\n\n// scss-docs-start navbar-variables\n$navbar-padding-y: $spacer * .5 !default;\n$navbar-padding-x: null !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default;\n$navbar-brand-margin-end: 1rem !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n$navbar-toggler-focus-width: $btn-focus-width !default;\n$navbar-toggler-transition: box-shadow .15s ease-in-out !default;\n// scss-docs-end navbar-variables\n\n// scss-docs-start navbar-theme-variables\n$navbar-dark-color: rgba($white, .55) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .55) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n// scss-docs-end navbar-theme-variables\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n// scss-docs-start dropdown-variables\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $dropdown-border-color !default;\n$dropdown-divider-margin-y: $spacer * .5 !default;\n$dropdown-box-shadow: $box-shadow !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: shade-color($gray-900, 10%) !default;\n$dropdown-link-hover-bg: $gray-200 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-500 !default;\n\n$dropdown-item-padding-y: $spacer * .25 !default;\n$dropdown-item-padding-x: $spacer !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding: $dropdown-padding-y $dropdown-item-padding-x !default;\n// scss-docs-end dropdown-variables\n\n// scss-docs-start dropdown-dark-variables\n$dropdown-dark-color: $gray-300 !default;\n$dropdown-dark-bg: $gray-800 !default;\n$dropdown-dark-border-color: $dropdown-border-color !default;\n$dropdown-dark-divider-bg: $dropdown-divider-bg !default;\n$dropdown-dark-box-shadow: null !default;\n$dropdown-dark-link-color: $dropdown-dark-color !default;\n$dropdown-dark-link-hover-color: $white !default;\n$dropdown-dark-link-hover-bg: rgba($white, .15) !default;\n$dropdown-dark-link-active-color: $dropdown-link-active-color !default;\n$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default;\n$dropdown-dark-link-disabled-color: $gray-500 !default;\n$dropdown-dark-header-color: $gray-500 !default;\n// scss-docs-end dropdown-dark-variables\n\n\n// Pagination\n\n// scss-docs-start pagination-variables\n$pagination-padding-y: .375rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-radius: $border-radius !default;\n$pagination-margin-start: -$pagination-border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-color: $link-hover-color !default;\n$pagination-focus-bg: $gray-200 !default;\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$pagination-border-radius-sm: $border-radius-sm !default;\n$pagination-border-radius-lg: $border-radius-lg !default;\n// scss-docs-end pagination-variables\n\n\n// Placeholders\n\n// scss-docs-start placeholders\n$placeholder-opacity-max: .5 !default;\n$placeholder-opacity-min: .2 !default;\n// scss-docs-end placeholders\n\n// Cards\n\n// scss-docs-start card-variables\n$card-spacer-y: $spacer !default;\n$card-spacer-x: $spacer !default;\n$card-title-spacer-y: $spacer * .5 !default;\n$card-border-width: $border-width !default;\n$card-border-color: rgba($black, .125) !default;\n$card-border-radius: $border-radius !default;\n$card-box-shadow: null !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-padding-y: $card-spacer-y * .5 !default;\n$card-cap-padding-x: $card-spacer-x !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n$card-img-overlay-padding: $spacer !default;\n$card-group-margin: $grid-gutter-width * .5 !default;\n// scss-docs-end card-variables\n\n// Accordion\n\n// scss-docs-start accordion-variables\n$accordion-padding-y: 1rem !default;\n$accordion-padding-x: 1.25rem !default;\n$accordion-color: $body-color !default;\n$accordion-bg: $body-bg !default;\n$accordion-border-width: $border-width !default;\n$accordion-border-color: rgba($black, .125) !default;\n$accordion-border-radius: $border-radius !default;\n$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default;\n\n$accordion-body-padding-y: $accordion-padding-y !default;\n$accordion-body-padding-x: $accordion-padding-x !default;\n\n$accordion-button-padding-y: $accordion-padding-y !default;\n$accordion-button-padding-x: $accordion-padding-x !default;\n$accordion-button-color: $accordion-color !default;\n$accordion-button-bg: $accordion-bg !default;\n$accordion-transition: $btn-transition, border-radius .15s ease !default;\n$accordion-button-active-bg: tint-color($component-active-bg, 90%) !default;\n$accordion-button-active-color: shade-color($primary, 10%) !default;\n\n$accordion-button-focus-border-color: $input-focus-border-color !default;\n$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default;\n\n$accordion-icon-width: 1.25rem !default;\n$accordion-icon-color: $accordion-button-color !default;\n$accordion-icon-active-color: $accordion-button-active-color !default;\n$accordion-icon-transition: transform .2s ease-in-out !default;\n$accordion-icon-transform: rotate(-180deg) !default;\n\n$accordion-button-icon: url(\"data:image/svg+xml,\") !default;\n$accordion-button-active-icon: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end accordion-variables\n\n// Tooltips\n\n// scss-docs-start tooltip-variables\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: $spacer * .25 !default;\n$tooltip-padding-x: $spacer * .5 !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n// scss-docs-end tooltip-variables\n\n// Form tooltips must come after regular tooltips\n// scss-docs-start tooltip-feedback-variables\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: null !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n// scss-docs-end tooltip-feedback-variables\n\n\n// Popovers\n\n// scss-docs-start popover-variables\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: $box-shadow !default;\n\n$popover-header-bg: shade-color($popover-bg, 6%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: $spacer !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $spacer !default;\n$popover-body-padding-x: $spacer !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n// scss-docs-end popover-variables\n\n\n// Toasts\n\n// scss-docs-start toast-variables\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .5rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: $border-radius !default;\n$toast-box-shadow: $box-shadow !default;\n$toast-spacing: $container-padding-x !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n// scss-docs-end toast-variables\n\n\n// Badges\n\n// scss-docs-start badge-variables\n$badge-font-size: .75em !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-color: $white !default;\n$badge-padding-y: .35em !default;\n$badge-padding-x: .65em !default;\n$badge-border-radius: $border-radius !default;\n// scss-docs-end badge-variables\n\n\n// Modals\n\n// scss-docs-start modal-variables\n$modal-inner-padding: $spacer !default;\n\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: $box-shadow-sm !default;\n$modal-content-box-shadow-sm-up: $box-shadow !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: $modal-inner-padding !default;\n$modal-header-padding-x: $modal-inner-padding !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-sm: 300px !default;\n$modal-md: 500px !default;\n$modal-lg: 800px !default;\n$modal-xl: 1140px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n// scss-docs-end modal-variables\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n// scss-docs-start alert-variables\n$alert-padding-y: $spacer !default;\n$alert-padding-x: $spacer !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n$alert-bg-scale: -80% !default;\n$alert-border-scale: -70% !default;\n$alert-color-scale: 40% !default;\n$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side\n// scss-docs-end alert-variables\n\n\n// Progress bars\n\n// scss-docs-start progress-variables\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: $box-shadow-inset !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: $primary !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n// scss-docs-end progress-variables\n\n\n// List group\n\n// scss-docs-start list-group-variables\n$list-group-color: $gray-900 !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: $spacer * .5 !default;\n$list-group-item-padding-x: $spacer !default;\n$list-group-item-bg-scale: -80% !default;\n$list-group-item-color-scale: 40% !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n// scss-docs-end list-group-variables\n\n\n// Image thumbnails\n\n// scss-docs-start thumbnail-variables\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: $box-shadow-sm !default;\n// scss-docs-end thumbnail-variables\n\n\n// Figures\n\n// scss-docs-start figure-variables\n$figure-caption-font-size: $small-font-size !default;\n$figure-caption-color: $gray-600 !default;\n// scss-docs-end figure-variables\n\n\n// Breadcrumbs\n\n// scss-docs-start breadcrumb-variables\n$breadcrumb-font-size: null !default;\n$breadcrumb-padding-y: 0 !default;\n$breadcrumb-padding-x: 0 !default;\n$breadcrumb-item-padding-x: .5rem !default;\n$breadcrumb-margin-bottom: 1rem !default;\n$breadcrumb-bg: null !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n$breadcrumb-divider-flipped: $breadcrumb-divider !default;\n$breadcrumb-border-radius: null !default;\n// scss-docs-end breadcrumb-variables\n\n// Carousel\n\n// scss-docs-start carousel-variables\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-opacity: .5 !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-active-opacity: 1 !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n$carousel-caption-padding-y: 1.25rem !default;\n$carousel-caption-spacer: 1.25rem !default;\n\n$carousel-control-icon-width: 2rem !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n$carousel-dark-indicator-active-bg: $black !default;\n$carousel-dark-caption-color: $black !default;\n$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default;\n// scss-docs-end carousel-variables\n\n\n// Spinners\n\n// scss-docs-start spinner-variables\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-vertical-align: -.125em !default;\n$spinner-border-width: .25em !default;\n$spinner-animation-speed: .75s !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n// scss-docs-end spinner-variables\n\n\n// Close\n\n// scss-docs-start close-variables\n$btn-close-width: 1em !default;\n$btn-close-height: $btn-close-width !default;\n$btn-close-padding-x: .25em !default;\n$btn-close-padding-y: $btn-close-padding-x !default;\n$btn-close-color: $black !default;\n$btn-close-bg: url(\"data:image/svg+xml,\") !default;\n$btn-close-focus-shadow: $input-btn-focus-box-shadow !default;\n$btn-close-opacity: .5 !default;\n$btn-close-hover-opacity: .75 !default;\n$btn-close-focus-opacity: 1 !default;\n$btn-close-disabled-opacity: .25 !default;\n$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default;\n// scss-docs-end close-variables\n\n\n// Offcanvas\n\n// scss-docs-start offcanvas-variables\n$offcanvas-padding-y: $modal-inner-padding !default;\n$offcanvas-padding-x: $modal-inner-padding !default;\n$offcanvas-horizontal-width: 400px !default;\n$offcanvas-vertical-height: 30vh !default;\n$offcanvas-transition-duration: .3s !default;\n$offcanvas-border-color: $modal-content-border-color !default;\n$offcanvas-border-width: $modal-content-border-width !default;\n$offcanvas-title-line-height: $modal-title-line-height !default;\n$offcanvas-bg-color: $modal-content-bg !default;\n$offcanvas-color: $modal-content-color !default;\n$offcanvas-box-shadow: $modal-content-box-shadow-xs !default;\n$offcanvas-backdrop-bg: $modal-backdrop-bg !default;\n$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default;\n// scss-docs-end offcanvas-variables\n\n// Code\n\n$code-font-size: $small-font-size !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: null !default;\n","// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$variable-prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$variable-prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$variable-prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$variable-prefix}gutter-x: #{$gutter};\n --#{$variable-prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list\n margin-right: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n margin-left: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$variable-prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: divide(100%, $count);\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0%; // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is and invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (eg. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $value in $is-local-vars {\n --#{$variable-prefix}#{$local-var}: #{$value};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css new file mode 100644 index 0000000..d3dfc1b --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-left:var(--bs-gutter-x,.75rem);padding-right:var(--bs-gutter-x,.75rem);margin-left:auto;margin-right:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-left:calc(var(--bs-gutter-x) * -.5);margin-right:calc(var(--bs-gutter-x) * -.5)}.row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-left:calc(var(--bs-gutter-x) * .5);padding-right:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-right:8.33333333%}.offset-2{margin-right:16.66666667%}.offset-3{margin-right:25%}.offset-4{margin-right:33.33333333%}.offset-5{margin-right:41.66666667%}.offset-6{margin-right:50%}.offset-7{margin-right:58.33333333%}.offset-8{margin-right:66.66666667%}.offset-9{margin-right:75%}.offset-10{margin-right:83.33333333%}.offset-11{margin-right:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-right:0}.offset-sm-1{margin-right:8.33333333%}.offset-sm-2{margin-right:16.66666667%}.offset-sm-3{margin-right:25%}.offset-sm-4{margin-right:33.33333333%}.offset-sm-5{margin-right:41.66666667%}.offset-sm-6{margin-right:50%}.offset-sm-7{margin-right:58.33333333%}.offset-sm-8{margin-right:66.66666667%}.offset-sm-9{margin-right:75%}.offset-sm-10{margin-right:83.33333333%}.offset-sm-11{margin-right:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-right:0}.offset-md-1{margin-right:8.33333333%}.offset-md-2{margin-right:16.66666667%}.offset-md-3{margin-right:25%}.offset-md-4{margin-right:33.33333333%}.offset-md-5{margin-right:41.66666667%}.offset-md-6{margin-right:50%}.offset-md-7{margin-right:58.33333333%}.offset-md-8{margin-right:66.66666667%}.offset-md-9{margin-right:75%}.offset-md-10{margin-right:83.33333333%}.offset-md-11{margin-right:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-right:0}.offset-lg-1{margin-right:8.33333333%}.offset-lg-2{margin-right:16.66666667%}.offset-lg-3{margin-right:25%}.offset-lg-4{margin-right:33.33333333%}.offset-lg-5{margin-right:41.66666667%}.offset-lg-6{margin-right:50%}.offset-lg-7{margin-right:58.33333333%}.offset-lg-8{margin-right:66.66666667%}.offset-lg-9{margin-right:75%}.offset-lg-10{margin-right:83.33333333%}.offset-lg-11{margin-right:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-right:0}.offset-xl-1{margin-right:8.33333333%}.offset-xl-2{margin-right:16.66666667%}.offset-xl-3{margin-right:25%}.offset-xl-4{margin-right:33.33333333%}.offset-xl-5{margin-right:41.66666667%}.offset-xl-6{margin-right:50%}.offset-xl-7{margin-right:58.33333333%}.offset-xl-8{margin-right:66.66666667%}.offset-xl-9{margin-right:75%}.offset-xl-10{margin-right:83.33333333%}.offset-xl-11{margin-right:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-right:0}.offset-xxl-1{margin-right:8.33333333%}.offset-xxl-2{margin-right:16.66666667%}.offset-xxl-3{margin-right:25%}.offset-xxl-4{margin-right:33.33333333%}.offset-xxl-5{margin-right:41.66666667%}.offset-xxl-6{margin-right:50%}.offset-xxl-7{margin-right:58.33333333%}.offset-xxl-8{margin-right:66.66666667%}.offset-xxl-9{margin-right:75%}.offset-xxl-10{margin-right:83.33333333%}.offset-xxl-11{margin-right:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-left:0!important;margin-right:0!important}.mx-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-3{margin-left:1rem!important;margin-right:1rem!important}.mx-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-5{margin-left:3rem!important;margin-right:3rem!important}.mx-auto{margin-left:auto!important;margin-right:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-left:0!important}.me-1{margin-left:.25rem!important}.me-2{margin-left:.5rem!important}.me-3{margin-left:1rem!important}.me-4{margin-left:1.5rem!important}.me-5{margin-left:3rem!important}.me-auto{margin-left:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-right:0!important}.ms-1{margin-right:.25rem!important}.ms-2{margin-right:.5rem!important}.ms-3{margin-right:1rem!important}.ms-4{margin-right:1.5rem!important}.ms-5{margin-right:3rem!important}.ms-auto{margin-right:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-left:0!important;padding-right:0!important}.px-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-3{padding-left:1rem!important;padding-right:1rem!important}.px-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-5{padding-left:3rem!important;padding-right:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-left:0!important}.pe-1{padding-left:.25rem!important}.pe-2{padding-left:.5rem!important}.pe-3{padding-left:1rem!important}.pe-4{padding-left:1.5rem!important}.pe-5{padding-left:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-right:0!important}.ps-1{padding-right:.25rem!important}.ps-2{padding-right:.5rem!important}.ps-3{padding-right:1rem!important}.ps-4{padding-right:1.5rem!important}.ps-5{padding-right:3rem!important}@media (min-width:576px){.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-left:0!important;margin-right:0!important}.mx-sm-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-sm-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-sm-3{margin-left:1rem!important;margin-right:1rem!important}.mx-sm-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-sm-5{margin-left:3rem!important;margin-right:3rem!important}.mx-sm-auto{margin-left:auto!important;margin-right:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-left:0!important}.me-sm-1{margin-left:.25rem!important}.me-sm-2{margin-left:.5rem!important}.me-sm-3{margin-left:1rem!important}.me-sm-4{margin-left:1.5rem!important}.me-sm-5{margin-left:3rem!important}.me-sm-auto{margin-left:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-right:0!important}.ms-sm-1{margin-right:.25rem!important}.ms-sm-2{margin-right:.5rem!important}.ms-sm-3{margin-right:1rem!important}.ms-sm-4{margin-right:1.5rem!important}.ms-sm-5{margin-right:3rem!important}.ms-sm-auto{margin-right:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-left:0!important;padding-right:0!important}.px-sm-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-sm-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-sm-3{padding-left:1rem!important;padding-right:1rem!important}.px-sm-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-sm-5{padding-left:3rem!important;padding-right:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-left:0!important}.pe-sm-1{padding-left:.25rem!important}.pe-sm-2{padding-left:.5rem!important}.pe-sm-3{padding-left:1rem!important}.pe-sm-4{padding-left:1.5rem!important}.pe-sm-5{padding-left:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-right:0!important}.ps-sm-1{padding-right:.25rem!important}.ps-sm-2{padding-right:.5rem!important}.ps-sm-3{padding-right:1rem!important}.ps-sm-4{padding-right:1.5rem!important}.ps-sm-5{padding-right:3rem!important}}@media (min-width:768px){.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-left:0!important;margin-right:0!important}.mx-md-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-md-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-md-3{margin-left:1rem!important;margin-right:1rem!important}.mx-md-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-md-5{margin-left:3rem!important;margin-right:3rem!important}.mx-md-auto{margin-left:auto!important;margin-right:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-left:0!important}.me-md-1{margin-left:.25rem!important}.me-md-2{margin-left:.5rem!important}.me-md-3{margin-left:1rem!important}.me-md-4{margin-left:1.5rem!important}.me-md-5{margin-left:3rem!important}.me-md-auto{margin-left:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-right:0!important}.ms-md-1{margin-right:.25rem!important}.ms-md-2{margin-right:.5rem!important}.ms-md-3{margin-right:1rem!important}.ms-md-4{margin-right:1.5rem!important}.ms-md-5{margin-right:3rem!important}.ms-md-auto{margin-right:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-left:0!important;padding-right:0!important}.px-md-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-md-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-md-3{padding-left:1rem!important;padding-right:1rem!important}.px-md-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-md-5{padding-left:3rem!important;padding-right:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-left:0!important}.pe-md-1{padding-left:.25rem!important}.pe-md-2{padding-left:.5rem!important}.pe-md-3{padding-left:1rem!important}.pe-md-4{padding-left:1.5rem!important}.pe-md-5{padding-left:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-right:0!important}.ps-md-1{padding-right:.25rem!important}.ps-md-2{padding-right:.5rem!important}.ps-md-3{padding-right:1rem!important}.ps-md-4{padding-right:1.5rem!important}.ps-md-5{padding-right:3rem!important}}@media (min-width:992px){.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-left:0!important;margin-right:0!important}.mx-lg-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-lg-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-lg-3{margin-left:1rem!important;margin-right:1rem!important}.mx-lg-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-lg-5{margin-left:3rem!important;margin-right:3rem!important}.mx-lg-auto{margin-left:auto!important;margin-right:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-left:0!important}.me-lg-1{margin-left:.25rem!important}.me-lg-2{margin-left:.5rem!important}.me-lg-3{margin-left:1rem!important}.me-lg-4{margin-left:1.5rem!important}.me-lg-5{margin-left:3rem!important}.me-lg-auto{margin-left:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-right:0!important}.ms-lg-1{margin-right:.25rem!important}.ms-lg-2{margin-right:.5rem!important}.ms-lg-3{margin-right:1rem!important}.ms-lg-4{margin-right:1.5rem!important}.ms-lg-5{margin-right:3rem!important}.ms-lg-auto{margin-right:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-left:0!important;padding-right:0!important}.px-lg-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-lg-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-lg-3{padding-left:1rem!important;padding-right:1rem!important}.px-lg-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-lg-5{padding-left:3rem!important;padding-right:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-left:0!important}.pe-lg-1{padding-left:.25rem!important}.pe-lg-2{padding-left:.5rem!important}.pe-lg-3{padding-left:1rem!important}.pe-lg-4{padding-left:1.5rem!important}.pe-lg-5{padding-left:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-right:0!important}.ps-lg-1{padding-right:.25rem!important}.ps-lg-2{padding-right:.5rem!important}.ps-lg-3{padding-right:1rem!important}.ps-lg-4{padding-right:1.5rem!important}.ps-lg-5{padding-right:3rem!important}}@media (min-width:1200px){.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-left:0!important;margin-right:0!important}.mx-xl-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-xl-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-xl-3{margin-left:1rem!important;margin-right:1rem!important}.mx-xl-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-xl-5{margin-left:3rem!important;margin-right:3rem!important}.mx-xl-auto{margin-left:auto!important;margin-right:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-left:0!important}.me-xl-1{margin-left:.25rem!important}.me-xl-2{margin-left:.5rem!important}.me-xl-3{margin-left:1rem!important}.me-xl-4{margin-left:1.5rem!important}.me-xl-5{margin-left:3rem!important}.me-xl-auto{margin-left:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-right:0!important}.ms-xl-1{margin-right:.25rem!important}.ms-xl-2{margin-right:.5rem!important}.ms-xl-3{margin-right:1rem!important}.ms-xl-4{margin-right:1.5rem!important}.ms-xl-5{margin-right:3rem!important}.ms-xl-auto{margin-right:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-left:0!important;padding-right:0!important}.px-xl-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-xl-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-xl-3{padding-left:1rem!important;padding-right:1rem!important}.px-xl-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-xl-5{padding-left:3rem!important;padding-right:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-left:0!important}.pe-xl-1{padding-left:.25rem!important}.pe-xl-2{padding-left:.5rem!important}.pe-xl-3{padding-left:1rem!important}.pe-xl-4{padding-left:1.5rem!important}.pe-xl-5{padding-left:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-right:0!important}.ps-xl-1{padding-right:.25rem!important}.ps-xl-2{padding-right:.5rem!important}.ps-xl-3{padding-right:1rem!important}.ps-xl-4{padding-right:1.5rem!important}.ps-xl-5{padding-right:3rem!important}}@media (min-width:1400px){.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-left:0!important;margin-right:0!important}.mx-xxl-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-xxl-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-xxl-3{margin-left:1rem!important;margin-right:1rem!important}.mx-xxl-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-xxl-5{margin-left:3rem!important;margin-right:3rem!important}.mx-xxl-auto{margin-left:auto!important;margin-right:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-left:0!important}.me-xxl-1{margin-left:.25rem!important}.me-xxl-2{margin-left:.5rem!important}.me-xxl-3{margin-left:1rem!important}.me-xxl-4{margin-left:1.5rem!important}.me-xxl-5{margin-left:3rem!important}.me-xxl-auto{margin-left:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-right:0!important}.ms-xxl-1{margin-right:.25rem!important}.ms-xxl-2{margin-right:.5rem!important}.ms-xxl-3{margin-right:1rem!important}.ms-xxl-4{margin-right:1.5rem!important}.ms-xxl-5{margin-right:3rem!important}.ms-xxl-auto{margin-right:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-left:0!important;padding-right:0!important}.px-xxl-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-xxl-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-xxl-3{padding-left:1rem!important;padding-right:1rem!important}.px-xxl-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-xxl-5{padding-left:3rem!important;padding-right:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-left:0!important}.pe-xxl-1{padding-left:.25rem!important}.pe-xxl-2{padding-left:.5rem!important}.pe-xxl-3{padding-left:1rem!important}.pe-xxl-4{padding-left:1.5rem!important}.pe-xxl-5{padding-left:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-right:0!important}.ps-xxl-1{padding-right:.25rem!important}.ps-xxl-2{padding-right:.5rem!important}.ps-xxl-3{padding-right:1rem!important}.ps-xxl-4{padding-right:1.5rem!important}.ps-xxl-5{padding-right:3rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap-grid.rtl.min.css.map */ \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map new file mode 100644 index 0000000..86d302a --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","../../scss/_containers.scss","dist/css/bootstrap-grid.rtl.css","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AAAA;;;;;ACME,WCCF,iBAGA,cACA,cACA,cAHA,cADA,eCLE,MAAA,KACA,aAAA,0BACA,cAAA,0BACA,YAAA,KACA,aAAA,KCwDE,yBH5CE,WAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cAAA,cACE,UAAA,OG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cAAA,eACE,UAAA,QIfN,KCAA,cAAA,OACA,cAAA,EACA,QAAA,KACA,UAAA,KACA,WAAA,8BACA,YAAA,+BACA,aAAA,+BDHE,OCQF,WAAA,WAIA,YAAA,EACA,MAAA,KACA,UAAA,KACA,aAAA,8BACA,cAAA,8BACA,WAAA,mBA+CI,KACE,KAAA,EAAA,EAAA,GAGF,iBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,cACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,UAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,UAxDV,aAAA,YAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,WAxDV,aAAA,aAwDU,WAxDV,aAAA,aAmEM,KJoGR,MIlGU,cAAA,EAGF,KJoGR,MIlGU,cAAA,EAPF,KJ8GR,MI5GU,cAAA,QAGF,KJ8GR,MI5GU,cAAA,QAPF,KJwHR,MItHU,cAAA,OAGF,KJwHR,MItHU,cAAA,OAPF,KJkIR,MIhIU,cAAA,KAGF,KJkIR,MIhIU,cAAA,KAPF,KJ4IR,MI1IU,cAAA,OAGF,KJ4IR,MI1IU,cAAA,OAPF,KJsJR,MIpJU,cAAA,KAGF,KJsJR,MIpJU,cAAA,KFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJyTR,SIvTU,cAAA,EAGF,QJyTR,SIvTU,cAAA,EAPF,QJmUR,SIjUU,cAAA,QAGF,QJmUR,SIjUU,cAAA,QAPF,QJ6UR,SI3UU,cAAA,OAGF,QJ6UR,SI3UU,cAAA,OAPF,QJuVR,SIrVU,cAAA,KAGF,QJuVR,SIrVU,cAAA,KAPF,QJiWR,SI/VU,cAAA,OAGF,QJiWR,SI/VU,cAAA,OAPF,QJ2WR,SIzWU,cAAA,KAGF,QJ2WR,SIzWU,cAAA,MFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJ8gBR,SI5gBU,cAAA,EAGF,QJ8gBR,SI5gBU,cAAA,EAPF,QJwhBR,SIthBU,cAAA,QAGF,QJwhBR,SIthBU,cAAA,QAPF,QJkiBR,SIhiBU,cAAA,OAGF,QJkiBR,SIhiBU,cAAA,OAPF,QJ4iBR,SI1iBU,cAAA,KAGF,QJ4iBR,SI1iBU,cAAA,KAPF,QJsjBR,SIpjBU,cAAA,OAGF,QJsjBR,SIpjBU,cAAA,OAPF,QJgkBR,SI9jBU,cAAA,KAGF,QJgkBR,SI9jBU,cAAA,MFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJmuBR,SIjuBU,cAAA,EAGF,QJmuBR,SIjuBU,cAAA,EAPF,QJ6uBR,SI3uBU,cAAA,QAGF,QJ6uBR,SI3uBU,cAAA,QAPF,QJuvBR,SIrvBU,cAAA,OAGF,QJuvBR,SIrvBU,cAAA,OAPF,QJiwBR,SI/vBU,cAAA,KAGF,QJiwBR,SI/vBU,cAAA,KAPF,QJ2wBR,SIzwBU,cAAA,OAGF,QJ2wBR,SIzwBU,cAAA,OAPF,QJqxBR,SInxBU,cAAA,KAGF,QJqxBR,SInxBU,cAAA,MFzDN,0BESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJw7BR,SIt7BU,cAAA,EAGF,QJw7BR,SIt7BU,cAAA,EAPF,QJk8BR,SIh8BU,cAAA,QAGF,QJk8BR,SIh8BU,cAAA,QAPF,QJ48BR,SI18BU,cAAA,OAGF,QJ48BR,SI18BU,cAAA,OAPF,QJs9BR,SIp9BU,cAAA,KAGF,QJs9BR,SIp9BU,cAAA,KAPF,QJg+BR,SI99BU,cAAA,OAGF,QJg+BR,SI99BU,cAAA,OAPF,QJ0+BR,SIx+BU,cAAA,KAGF,QJ0+BR,SIx+BU,cAAA,MFzDN,0BESE,SACE,KAAA,EAAA,EAAA,GAGF,qBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,cAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,cAxDV,aAAA,EAwDU,cAxDV,aAAA,YAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,eAxDV,aAAA,aAwDU,eAxDV,aAAA,aAmEM,SJ6oCR,UI3oCU,cAAA,EAGF,SJ6oCR,UI3oCU,cAAA,EAPF,SJupCR,UIrpCU,cAAA,QAGF,SJupCR,UIrpCU,cAAA,QAPF,SJiqCR,UI/pCU,cAAA,OAGF,SJiqCR,UI/pCU,cAAA,OAPF,SJ2qCR,UIzqCU,cAAA,KAGF,SJ2qCR,UIzqCU,cAAA,KAPF,SJqrCR,UInrCU,cAAA,OAGF,SJqrCR,UInrCU,cAAA,OAPF,SJ+rCR,UI7rCU,cAAA,KAGF,SJ+rCR,UI7rCU,cAAA,MCzDF,UAOI,QAAA,iBAPJ,gBAOI,QAAA,uBAPJ,SAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,SAOI,QAAA,gBAPJ,aAOI,QAAA,oBAPJ,cAOI,QAAA,qBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,QAOI,QAAA,eAPJ,WAOI,KAAA,EAAA,EAAA,eAPJ,UAOI,eAAA,cAPJ,aAOI,eAAA,iBAPJ,kBAOI,eAAA,sBAPJ,qBAOI,eAAA,yBAPJ,aAOI,UAAA,YAPJ,aAOI,UAAA,YAPJ,eAOI,YAAA,YAPJ,eAOI,YAAA,YAPJ,WAOI,UAAA,eAPJ,aAOI,UAAA,iBAPJ,mBAOI,UAAA,uBAPJ,uBAOI,gBAAA,qBAPJ,qBAOI,gBAAA,mBAPJ,wBAOI,gBAAA,iBAPJ,yBAOI,gBAAA,wBAPJ,wBAOI,gBAAA,uBAPJ,wBAOI,gBAAA,uBAPJ,mBAOI,YAAA,qBAPJ,iBAOI,YAAA,mBAPJ,oBAOI,YAAA,iBAPJ,sBAOI,YAAA,mBAPJ,qBAOI,YAAA,kBAPJ,qBAOI,cAAA,qBAPJ,mBAOI,cAAA,mBAPJ,sBAOI,cAAA,iBAPJ,uBAOI,cAAA,wBAPJ,sBAOI,cAAA,uBAPJ,uBAOI,cAAA,kBAPJ,iBAOI,WAAA,eAPJ,kBAOI,WAAA,qBAPJ,gBAOI,WAAA,mBAPJ,mBAOI,WAAA,iBAPJ,qBAOI,WAAA,mBAPJ,oBAOI,WAAA,kBAPJ,aAOI,MAAA,aAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,KAOI,OAAA,YAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,gBAPJ,KAOI,OAAA,eAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,eAPJ,QAOI,OAAA,eAPJ,MAOI,YAAA,YAAA,aAAA,YAPJ,MAOI,YAAA,iBAAA,aAAA,iBAPJ,MAOI,YAAA,gBAAA,aAAA,gBAPJ,MAOI,YAAA,eAAA,aAAA,eAPJ,MAOI,YAAA,iBAAA,aAAA,iBAPJ,MAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,MAOI,WAAA,YAAA,cAAA,YAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,gBAAA,cAAA,gBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,YAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,gBAPJ,MAOI,WAAA,eAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,eAPJ,SAOI,WAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,SAOI,YAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,SAOI,cAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,SAOI,aAAA,eAPJ,KAOI,QAAA,YAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,gBAPJ,KAOI,QAAA,eAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,eAPJ,MAOI,aAAA,YAAA,cAAA,YAPJ,MAOI,aAAA,iBAAA,cAAA,iBAPJ,MAOI,aAAA,gBAAA,cAAA,gBAPJ,MAOI,aAAA,eAAA,cAAA,eAPJ,MAOI,aAAA,iBAAA,cAAA,iBAPJ,MAOI,aAAA,eAAA,cAAA,eAPJ,MAOI,YAAA,YAAA,eAAA,YAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,gBAAA,eAAA,gBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,MAOI,eAAA,YAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,gBAPJ,MAOI,eAAA,eAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eHPR,yBGAI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHPR,yBGAI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHPR,yBGAI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHPR,0BGAI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHPR,0BGAI,cAOI,QAAA,iBAPJ,oBAOI,QAAA,uBAPJ,aAOI,QAAA,gBAPJ,YAOI,QAAA,eAPJ,aAOI,QAAA,gBAPJ,iBAOI,QAAA,oBAPJ,kBAOI,QAAA,qBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,YAOI,QAAA,eAPJ,eAOI,KAAA,EAAA,EAAA,eAPJ,cAOI,eAAA,cAPJ,iBAOI,eAAA,iBAPJ,sBAOI,eAAA,sBAPJ,yBAOI,eAAA,yBAPJ,iBAOI,UAAA,YAPJ,iBAOI,UAAA,YAPJ,mBAOI,YAAA,YAPJ,mBAOI,YAAA,YAPJ,eAOI,UAAA,eAPJ,iBAOI,UAAA,iBAPJ,uBAOI,UAAA,uBAPJ,2BAOI,gBAAA,qBAPJ,yBAOI,gBAAA,mBAPJ,4BAOI,gBAAA,iBAPJ,6BAOI,gBAAA,wBAPJ,4BAOI,gBAAA,uBAPJ,4BAOI,gBAAA,uBAPJ,uBAOI,YAAA,qBAPJ,qBAOI,YAAA,mBAPJ,wBAOI,YAAA,iBAPJ,0BAOI,YAAA,mBAPJ,yBAOI,YAAA,kBAPJ,yBAOI,cAAA,qBAPJ,uBAOI,cAAA,mBAPJ,0BAOI,cAAA,iBAPJ,2BAOI,cAAA,wBAPJ,0BAOI,cAAA,uBAPJ,2BAOI,cAAA,kBAPJ,qBAOI,WAAA,eAPJ,sBAOI,WAAA,qBAPJ,oBAOI,WAAA,mBAPJ,uBAOI,WAAA,iBAPJ,yBAOI,WAAA,mBAPJ,wBAOI,WAAA,kBAPJ,iBAOI,MAAA,aAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,gBAOI,MAAA,YAPJ,SAOI,OAAA,YAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,gBAPJ,SAOI,OAAA,eAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,eAPJ,YAOI,OAAA,eAPJ,UAOI,YAAA,YAAA,aAAA,YAPJ,UAOI,YAAA,iBAAA,aAAA,iBAPJ,UAOI,YAAA,gBAAA,aAAA,gBAPJ,UAOI,YAAA,eAAA,aAAA,eAPJ,UAOI,YAAA,iBAAA,aAAA,iBAPJ,UAOI,YAAA,eAAA,aAAA,eAPJ,aAOI,YAAA,eAAA,aAAA,eAPJ,UAOI,WAAA,YAAA,cAAA,YAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,gBAAA,cAAA,gBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,aAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,YAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,gBAPJ,UAOI,WAAA,eAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,eAPJ,aAOI,WAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,aAOI,YAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,aAOI,cAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,aAOI,aAAA,eAPJ,SAOI,QAAA,YAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,gBAPJ,SAOI,QAAA,eAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,eAPJ,UAOI,aAAA,YAAA,cAAA,YAPJ,UAOI,aAAA,iBAAA,cAAA,iBAPJ,UAOI,aAAA,gBAAA,cAAA,gBAPJ,UAOI,aAAA,eAAA,cAAA,eAPJ,UAOI,aAAA,iBAAA,cAAA,iBAPJ,UAOI,aAAA,eAAA,cAAA,eAPJ,UAOI,YAAA,YAAA,eAAA,YAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,gBAAA,eAAA,gBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,UAOI,eAAA,YAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,gBAPJ,UAOI,eAAA,eAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBChCZ,aDyBQ,gBAOI,QAAA,iBAPJ,sBAOI,QAAA,uBAPJ,eAOI,QAAA,gBAPJ,cAOI,QAAA,eAPJ,eAOI,QAAA,gBAPJ,mBAOI,QAAA,oBAPJ,oBAOI,QAAA,qBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,cAOI,QAAA","sourcesContent":["/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n$include-column-box-sizing: true !default;\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/lists\";\n@import \"mixins/breakpoints\";\n@import \"mixins/container\";\n@import \"mixins/grid\";\n@import \"mixins/utilities\";\n\n@import \"vendor/rfs\";\n\n@import \"containers\";\n@import \"grid\";\n\n@import \"utilities\";\n// Only use the utilities we need\n// stylelint-disable-next-line scss/dollar-variable-default\n$utilities: map-get-multiple(\n $utilities,\n (\n \"display\",\n \"order\",\n \"flex\",\n \"flex-direction\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-wrap\",\n \"justify-content\",\n \"align-items\",\n \"align-content\",\n \"align-self\",\n \"margin\",\n \"margin-x\",\n \"margin-y\",\n \"margin-top\",\n \"margin-end\",\n \"margin-bottom\",\n \"margin-start\",\n \"negative-margin\",\n \"negative-margin-x\",\n \"negative-margin-y\",\n \"negative-margin-top\",\n \"negative-margin-end\",\n \"negative-margin-bottom\",\n \"negative-margin-start\",\n \"padding\",\n \"padding-x\",\n \"padding-y\",\n \"padding-top\",\n \"padding-end\",\n \"padding-bottom\",\n \"padding-start\",\n )\n);\n\n@import \"utilities/api\";\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","/*!\n * Bootstrap Grid v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-left: var(--bs-gutter-x, 0.75rem);\n padding-right: var(--bs-gutter-x, 0.75rem);\n margin-left: auto;\n margin-right: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-left: calc(var(--bs-gutter-x) * .5);\n padding-right: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-right: 8.33333333%;\n}\n\n.offset-2 {\n margin-right: 16.66666667%;\n}\n\n.offset-3 {\n margin-right: 25%;\n}\n\n.offset-4 {\n margin-right: 33.33333333%;\n}\n\n.offset-5 {\n margin-right: 41.66666667%;\n}\n\n.offset-6 {\n margin-right: 50%;\n}\n\n.offset-7 {\n margin-right: 58.33333333%;\n}\n\n.offset-8 {\n margin-right: 66.66666667%;\n}\n\n.offset-9 {\n margin-right: 75%;\n}\n\n.offset-10 {\n margin-right: 83.33333333%;\n}\n\n.offset-11 {\n margin-right: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-right: 0;\n }\n\n .offset-sm-1 {\n margin-right: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-right: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-right: 25%;\n }\n\n .offset-sm-4 {\n margin-right: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-right: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-right: 50%;\n }\n\n .offset-sm-7 {\n margin-right: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-right: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-right: 75%;\n }\n\n .offset-sm-10 {\n margin-right: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-right: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-right: 0;\n }\n\n .offset-md-1 {\n margin-right: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-right: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-right: 25%;\n }\n\n .offset-md-4 {\n margin-right: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-right: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-right: 50%;\n }\n\n .offset-md-7 {\n margin-right: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-right: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-right: 75%;\n }\n\n .offset-md-10 {\n margin-right: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-right: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-right: 0;\n }\n\n .offset-lg-1 {\n margin-right: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-right: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-right: 25%;\n }\n\n .offset-lg-4 {\n margin-right: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-right: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-right: 50%;\n }\n\n .offset-lg-7 {\n margin-right: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-right: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-right: 75%;\n }\n\n .offset-lg-10 {\n margin-right: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-right: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-right: 0;\n }\n\n .offset-xl-1 {\n margin-right: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-right: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-right: 25%;\n }\n\n .offset-xl-4 {\n margin-right: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-right: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-right: 50%;\n }\n\n .offset-xl-7 {\n margin-right: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-right: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-right: 75%;\n }\n\n .offset-xl-10 {\n margin-right: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-right: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-right: 0;\n }\n\n .offset-xxl-1 {\n margin-right: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-right: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-right: 25%;\n }\n\n .offset-xxl-4 {\n margin-right: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-right: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-right: 50%;\n }\n\n .offset-xxl-7 {\n margin-right: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-right: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-right: 75%;\n }\n\n .offset-xxl-10 {\n margin-right: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-right: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n\n.mx-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n}\n\n.mx-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n}\n\n.mx-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n}\n\n.mx-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n}\n\n.mx-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n}\n\n.mx-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-left: 0 !important;\n}\n\n.me-1 {\n margin-left: 0.25rem !important;\n}\n\n.me-2 {\n margin-left: 0.5rem !important;\n}\n\n.me-3 {\n margin-left: 1rem !important;\n}\n\n.me-4 {\n margin-left: 1.5rem !important;\n}\n\n.me-5 {\n margin-left: 3rem !important;\n}\n\n.me-auto {\n margin-left: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-right: 0 !important;\n}\n\n.ms-1 {\n margin-right: 0.25rem !important;\n}\n\n.ms-2 {\n margin-right: 0.5rem !important;\n}\n\n.ms-3 {\n margin-right: 1rem !important;\n}\n\n.ms-4 {\n margin-right: 1.5rem !important;\n}\n\n.ms-5 {\n margin-right: 3rem !important;\n}\n\n.ms-auto {\n margin-right: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n}\n\n.px-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n}\n\n.px-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n}\n\n.px-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n}\n\n.px-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n}\n\n.px-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-left: 0 !important;\n}\n\n.pe-1 {\n padding-left: 0.25rem !important;\n}\n\n.pe-2 {\n padding-left: 0.5rem !important;\n}\n\n.pe-3 {\n padding-left: 1rem !important;\n}\n\n.pe-4 {\n padding-left: 1.5rem !important;\n}\n\n.pe-5 {\n padding-left: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-right: 0 !important;\n}\n\n.ps-1 {\n padding-right: 0.25rem !important;\n}\n\n.ps-2 {\n padding-right: 0.5rem !important;\n}\n\n.ps-3 {\n padding-right: 1rem !important;\n}\n\n.ps-4 {\n padding-right: 1.5rem !important;\n}\n\n.ps-5 {\n padding-right: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-left: 0 !important;\n }\n\n .me-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-left: 1rem !important;\n }\n\n .me-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-left: 3rem !important;\n }\n\n .me-sm-auto {\n margin-left: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-right: 0 !important;\n }\n\n .ms-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-right: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-right: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-right: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .px-sm-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n\n .px-sm-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-left: 0 !important;\n }\n\n .pe-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-left: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-left: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-right: 0 !important;\n }\n\n .ps-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-right: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n\n .mx-md-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n\n .mx-md-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n\n .mx-md-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-left: 0 !important;\n }\n\n .me-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-left: 1rem !important;\n }\n\n .me-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-left: 3rem !important;\n }\n\n .me-md-auto {\n margin-left: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-right: 0 !important;\n }\n\n .ms-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-right: 1rem !important;\n }\n\n .ms-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-right: 3rem !important;\n }\n\n .ms-md-auto {\n margin-right: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .px-md-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n\n .px-md-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-left: 0 !important;\n }\n\n .pe-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-left: 1rem !important;\n }\n\n .pe-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-left: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-right: 0 !important;\n }\n\n .ps-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-right: 1rem !important;\n }\n\n .ps-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-left: 0 !important;\n }\n\n .me-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-left: 1rem !important;\n }\n\n .me-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-left: 3rem !important;\n }\n\n .me-lg-auto {\n margin-left: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-right: 0 !important;\n }\n\n .ms-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-right: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-right: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-right: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .px-lg-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n\n .px-lg-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-left: 0 !important;\n }\n\n .pe-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-left: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-left: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-right: 0 !important;\n }\n\n .ps-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-right: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-left: 0 !important;\n }\n\n .me-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-left: 1rem !important;\n }\n\n .me-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-left: 3rem !important;\n }\n\n .me-xl-auto {\n margin-left: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-right: 0 !important;\n }\n\n .ms-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-right: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-right: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-right: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .px-xl-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n\n .px-xl-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-left: 0 !important;\n }\n\n .pe-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-left: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-left: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-right: 0 !important;\n }\n\n .ps-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-right: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-left: 0 !important;\n }\n\n .me-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-left: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-right: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-right: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n\n .px-xxl-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-left: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-left: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-right: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-right: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.rtl.css.map */","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n width: 100%;\n padding-right: var(--#{$variable-prefix}gutter-x, #{$gutter});\n padding-left: var(--#{$variable-prefix}gutter-x, #{$gutter});\n margin-right: auto;\n margin-left: auto;\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$variable-prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$variable-prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$variable-prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$variable-prefix}gutter-x: #{$gutter};\n --#{$variable-prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list\n margin-right: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n margin-left: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$variable-prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: divide(100%, $count);\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0%; // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is and invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (eg. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $value in $is-local-vars {\n --#{$variable-prefix}#{$local-var}: #{$value};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 0000000..c1bcf3b --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,427 @@ +/*! + * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + background-color: currentColor; + border: 0; + opacity: 0.25; +} + +hr:not([size]) { + height: 1px; +} + +h6, h5, h4, h3, h2, h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1 { + font-size: 2.5rem; + } +} + +h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2 { + font-size: 2rem; + } +} + +h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3 { + font-size: 1.75rem; + } +} + +h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4 { + font-size: 1.5rem; + } +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-bs-original-title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 0.875em; +} + +mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: #0d6efd; + text-decoration: underline; +} +a:hover { + color: #0a58ca; +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; + direction: ltr /* rtl:ignore */; + unicode-bidi: bidi-override; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: #d63384; + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 0.875em; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} +kbd kbd { + padding: 0; + font-size: 1em; + font-weight: 700; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: #6c757d; + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]::-webkit-calendar-picker-indicator { + display: none; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: calc(1.275rem + 0.3vw); + line-height: inherit; +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + outline-offset: -2px; + -webkit-appearance: textfield; +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::file-selector-button { + font: inherit; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map new file mode 100644 index 0000000..c06c13a --- /dev/null +++ b/Testers/EonaCat.LogStack.Test.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","bootstrap-reboot.css","../../scss/vendor/_rfs.scss","../../scss/_variables.scss","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"AAAA;;;;;;EAAA;ACeA;;;EAGE,sBAAA;ACPF;;ADsBI;EANJ;IAOM,uBAAA;EClBJ;AACF;;AD+BA;EACE,SAAA;EACA,uCAAA;EEmPI,mCALI;EF5OR,uCAAA;EACA,uCAAA;EACA,2BAAA;EACA,qCAAA;EACA,mCAAA;EACA,8BAAA;EACA,6CAAA;AC5BF;;ADsCA;EACE,cAAA;EACA,cGqkB4B;EHpkB5B,8BAAA;EACA,SAAA;EACA,aGokB4B;AFvmB9B;;ADsCA;EACE,WG8a4B;AFjd9B;;AD6CA;EACE,aAAA;EACA,qBG0gB4B;EHvgB5B,gBG0gB4B;EHzgB5B,gBG0gB4B;AFtjB9B;;ADgDA;EEwMQ,iCAAA;ADpPR;ACkFI;EFtCJ;IE+MQ,iBAAA;EDvPN;AACF;;AD4CA;EEmMQ,iCAAA;AD3OR;ACyEI;EFjCJ;IE0MQ,eAAA;ED9ON;AACF;;ADwCA;EE8LQ,+BAAA;ADlOR;ACgEI;EF5BJ;IEqMQ,kBAAA;EDrON;AACF;;ADoCA;EEyLQ,iCAAA;ADzNR;ACuDI;EFvBJ;IEgMQ,iBAAA;ED5NN;AACF;;ADgCA;EEgLM,kBALI;ADvMV;;ADiCA;EE2KM,eALI;ADnMV;;ADwCA;EACE,aAAA;EACA,mBGwT0B;AF7V5B;;ADgDA;;EAEE,yCAAA;EAAA,iCAAA;EACA,YAAA;EACA,sCAAA;EAAA,8BAAA;AC7CF;;ADmDA;EACE,mBAAA;EACA,kBAAA;EACA,oBAAA;AChDF;;ADsDA;;EAEE,kBAAA;ACnDF;;ADsDA;;;EAGE,aAAA;EACA,mBAAA;ACnDF;;ADsDA;;;;EAIE,gBAAA;ACnDF;;ADsDA;EACE,gBG6Y4B;AFhc9B;;ADwDA;EACE,qBAAA;EACA,cAAA;ACrDF;;AD2DA;EACE,gBAAA;ACxDF;;ADgEA;;EAEE,mBGsX4B;AFnb9B;;ADqEA;EE4EM,kBALI;ADxIV;;ADwEA;EACE,cGkb4B;EHjb5B,yBGyb4B;AF9f9B;;AD8EA;;EAEE,kBAAA;EEwDI,iBALI;EFjDR,cAAA;EACA,wBAAA;AC3EF;;AD8EA;EAAM,eAAA;AC1EN;;AD2EA;EAAM,WAAA;ACvEN;;AD4EA;EACE,cGpNQ;EHqNR,0BGwLwC;AFjQ1C;AD2EE;EACE,cGuLsC;AFhQ1C;;ADoFE;EAEE,cAAA;EACA,qBAAA;AClFJ;;ADyFA;;;;EAIE,iGGgS4B;EDlRxB,cALI;EFPR,+BAAA;EACA,2BAAA;ACtFF;;AD6FA;EACE,cAAA;EACA,aAAA;EACA,mBAAA;EACA,cAAA;EEAI,kBALI;ADpFV;AD8FE;EELI,kBALI;EFYN,cAAA;EACA,kBAAA;AC5FJ;;ADgGA;EEZM,kBALI;EFmBR,cG1QQ;EH2QR,qBAAA;AC7FF;ADgGE;EACE,cAAA;AC9FJ;;ADkGA;EACE,sBAAA;EExBI,kBALI;EF+BR,WGvTS;EHwTT,yBG/SS;ECEP,qBAAA;AH+MJ;ADiGE;EACE,UAAA;EE/BE,cALI;EFsCN,gBGgQ0B;AF/V9B;;ADwGA;EACE,gBAAA;ACrGF;;AD2GA;;EAEE,sBAAA;ACxGF;;ADgHA;EACE,oBAAA;EACA,yBAAA;AC7GF;;ADgHA;EACE,mBG8T4B;EH7T5B,sBG6T4B;EH5T5B,cG1VS;EH2VT,gBAAA;AC7GF;;ADoHA;EAEE,mBAAA;EACA,gCAAA;AClHF;;ADqHA;;;;;;EAME,qBAAA;EACA,mBAAA;EACA,eAAA;AClHF;;AD0HA;EACE,qBAAA;ACvHF;;AD6HA;EAEE,gBAAA;AC3HF;;ADmIA;EACE,UAAA;AChIF;;ADqIA;;;;;EAKE,SAAA;EACA,oBAAA;EE9HI,kBALI;EFqIR,oBAAA;AClIF;;ADsIA;;EAEE,oBAAA;ACnIF;;ADwIA;EACE,eAAA;ACrIF;;ADwIA;EAGE,iBAAA;ACvIF;AD0IE;EACE,UAAA;ACxIJ;;AD+IA;EACE,aAAA;AC5IF;;ADoJA;;;;EAIE,0BAAA;ACjJF;ADoJI;;;;EACE,eAAA;AC/IN;;ADsJA;EACE,UAAA;EACA,kBAAA;ACnJF;;ADwJA;EACE,gBAAA;ACrJF;;AD+JA;EACE,YAAA;EACA,UAAA;EACA,SAAA;EACA,SAAA;AC5JF;;ADoKA;EACE,WAAA;EACA,WAAA;EACA,UAAA;EACA,qBGmJ4B;EDtWtB,iCAAA;EFsNN,oBAAA;AClKF;ACtNI;EFiXJ;IExMQ,iBAAA;EDiDN;AACF;AD+JE;EACE,WAAA;AC7JJ;;ADoKA;;;;;;;EAOE,UAAA;ACjKF;;ADoKA;EACE,YAAA;ACjKF;;AD0KA;EACE,oBAAA;EACA,6BAAA;ACvKF;;AD+KA;;;;;;;CAAA;AAWA;EACE,wBAAA;AC/KF;;ADoLA;EACE,UAAA;ACjLF;;ADuLA;EACE,aAAA;ACpLF;;AD0LA;EACE,aAAA;EACA,0BAAA;ACvLF;;AD4LA;EACE,qBAAA;ACzLF;;AD8LA;EACE,SAAA;AC3LF;;ADkMA;EACE,kBAAA;EACA,eAAA;AC/LF;;ADuMA;EACE,wBAAA;ACpMF;;AD4MA;EACE,wBAAA;ACzMF","file":"bootstrap-reboot.css","sourcesContent":["/*!\n * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n// Prevent the usage of custom properties since we don't add them to `:root` in reboot\n$font-family-base: $font-family-sans-serif; // stylelint-disable-line scss/dollar-variable-default\n$font-family-code: $font-family-monospace; // stylelint-disable-line scss/dollar-variable-default\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`