├── resources ├── compiled │ ├── .gitignore │ └── ignition.css └── views │ ├── aiPrompt.php │ └── errorPage.php ├── src ├── Contracts │ └── ConfigManager.php ├── ErrorPage │ ├── Renderer.php │ └── ErrorPageViewModel.php ├── Config │ ├── FileConfigManager.php │ └── IgnitionConfig.php └── Ignition.php ├── LICENSE.md ├── composer.json └── README.md /resources/compiled/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !ignition.css 4 | !ignition.js 5 | -------------------------------------------------------------------------------- /src/Contracts/ConfigManager.php: -------------------------------------------------------------------------------- 1 | */ 8 | public function load(): array; 9 | 10 | /** @param array $options */ 11 | public function save(array $options): bool; 12 | 13 | /** @return array */ 14 | public function getPersistentInfo(): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/ErrorPage/Renderer.php: -------------------------------------------------------------------------------- 1 | $data 9 | * 10 | * @return void 11 | */ 12 | public function render(array $data, string $viewPath): void 13 | { 14 | $viewFile = $viewPath; 15 | 16 | extract($data, EXTR_OVERWRITE); 17 | 18 | include $viewFile; 19 | } 20 | 21 | public function renderAsString(array $date, string $viewPath): string 22 | { 23 | ob_start(); 24 | 25 | $this->render($date, $viewPath); 26 | 27 | return ob_get_clean(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/aiPrompt.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | You are a very skilled PHP programmer. 4 | 5 | applicationType()) { ?> 6 | You are working on a applicationType() ?> application. 7 | 8 | 9 | Use the following context to find a possible fix for the exception message at the end. Limit your answer to 4 or 5 sentences. Also include a few links to documentation that might help. 10 | 11 | Use this format in your answer, make sure links are json: 12 | 13 | FIX 14 | insert the possible fix here 15 | ENDFIX 16 | LINKS 17 | {"title": "Title link 1", "url": "URL link 1"} 18 | {"title": "Title link 2", "url": "URL link 2"} 19 | ENDLINKS 20 | --- 21 | 22 | Here comes the context and the exception message: 23 | 24 | Line: line() ?> 25 | 26 | File: 27 | file() ?> 28 | 29 | Snippet including line numbers: 30 | snippet() ?> 31 | 32 | Exception class: 33 | exceptionClass() ?> 34 | 35 | Exception message: 36 | exceptionMessage() ?> 37 | -------------------------------------------------------------------------------- /resources/views/errorPage.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <?= $viewModel->title() ?> 21 | 22 | 32 | 33 | 34 | 35 | customHtmlHead() ?> 36 | 37 | 38 | 39 | 40 | 51 | 52 | 53 | 56 | 57 |
58 | 59 | 64 | 65 | 68 | 69 | customHtmlBody() ?> 70 | 71 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "spatie/ignition", 3 | "description" : "A beautiful error page for PHP applications.", 4 | "keywords" : [ 5 | "error", 6 | "page", 7 | "laravel", 8 | "flare" 9 | ], 10 | "authors" : [ 11 | { 12 | "name" : "Spatie", 13 | "email" : "info@spatie.be", 14 | "role" : "Developer" 15 | } 16 | ], 17 | "homepage": "https://flareapp.io/ignition", 18 | "license": "MIT", 19 | "require": { 20 | "php": "^8.0", 21 | "ext-json": "*", 22 | "ext-mbstring": "*", 23 | "spatie/flare-client-php": "^1.7", 24 | "symfony/console": "^5.4|^6.0|^7.0", 25 | "symfony/var-dumper": "^5.4|^6.0|^7.0", 26 | "spatie/error-solutions": "^1.0" 27 | }, 28 | "require-dev" : { 29 | "illuminate/cache" : "^9.52|^10.0|^11.0|^12.0", 30 | "mockery/mockery" : "^1.4", 31 | "pestphp/pest" : "^1.20|^2.0", 32 | "phpstan/extension-installer" : "^1.1", 33 | "phpstan/phpstan-deprecation-rules" : "^1.0", 34 | "phpstan/phpstan-phpunit" : "^1.0", 35 | "psr/simple-cache-implementation" : "*", 36 | "symfony/cache" : "^5.4|^6.0|^7.0", 37 | "symfony/process" : "^5.4|^6.0|^7.0", 38 | "vlucas/phpdotenv" : "^5.5" 39 | }, 40 | "suggest" : { 41 | "openai-php/client" : "Require get solutions from OpenAI", 42 | "simple-cache-implementation" : "To cache solutions from OpenAI" 43 | }, 44 | "config" : { 45 | "sort-packages" : true, 46 | "allow-plugins" : { 47 | "phpstan/extension-installer": true, 48 | "pestphp/pest-plugin": true, 49 | "php-http/discovery": false 50 | } 51 | }, 52 | "autoload" : { 53 | "psr-4" : { 54 | "Spatie\\Ignition\\" : "src" 55 | } 56 | }, 57 | "autoload-dev" : { 58 | "psr-4" : { 59 | "Spatie\\Ignition\\Tests\\" : "tests" 60 | } 61 | }, 62 | "minimum-stability" : "dev", 63 | "prefer-stable" : true, 64 | "scripts" : { 65 | "analyse" : "vendor/bin/phpstan analyse", 66 | "baseline" : "vendor/bin/phpstan analyse --generate-baseline", 67 | "format" : "vendor/bin/php-cs-fixer fix --allow-risky=yes", 68 | "test" : "vendor/bin/pest", 69 | "test-coverage" : "vendor/bin/phpunit --coverage-html coverage" 70 | }, 71 | "support" : { 72 | "issues" : "https://github.com/spatie/ignition/issues", 73 | "forum" : "https://twitter.com/flareappio", 74 | "source" : "https://github.com/spatie/ignition", 75 | "docs" : "https://flareapp.io/docs/ignition-for-laravel/introduction" 76 | }, 77 | "extra" : { 78 | "branch-alias" : { 79 | "dev-main" : "1.5.x-dev" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ErrorPage/ErrorPageViewModel.php: -------------------------------------------------------------------------------- 1 | $solutions 19 | * @param string|null $solutionTransformerClass 20 | */ 21 | public function __construct( 22 | protected ?Throwable $throwable, 23 | protected IgnitionConfig $ignitionConfig, 24 | protected Report $report, 25 | protected array $solutions, 26 | protected ?string $solutionTransformerClass = null, 27 | protected string $customHtmlHead = '', 28 | protected string $customHtmlBody = '' 29 | ) { 30 | $this->solutionTransformerClass ??= SolutionTransformer::class; 31 | } 32 | 33 | public function throwableString(): string 34 | { 35 | if (! $this->throwable) { 36 | return ''; 37 | } 38 | 39 | $throwableString = sprintf( 40 | "%s: %s in file %s on line %d\n\n%s\n", 41 | get_class($this->throwable), 42 | $this->throwable->getMessage(), 43 | $this->throwable->getFile(), 44 | $this->throwable->getLine(), 45 | $this->report->getThrowable()?->getTraceAsString() 46 | ); 47 | 48 | return htmlspecialchars($throwableString); 49 | } 50 | 51 | public function title(): string 52 | { 53 | return htmlspecialchars($this->report->getMessage()); 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function config(): array 60 | { 61 | return $this->ignitionConfig->toArray(); 62 | } 63 | 64 | public function theme(): string 65 | { 66 | return $this->config()['theme'] ?? 'auto'; 67 | } 68 | 69 | /** 70 | * @return array 71 | */ 72 | public function solutions(): array 73 | { 74 | return array_map(function (Solution $solution) { 75 | /** @var class-string $transformerClass */ 76 | $transformerClass = $this->solutionTransformerClass; 77 | 78 | /** @var SolutionTransformer $transformer */ 79 | $transformer = new $transformerClass($solution); 80 | 81 | return ($transformer)->toArray(); 82 | }, $this->solutions); 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function report(): array 89 | { 90 | return $this->report->toArray(); 91 | } 92 | 93 | public function jsonEncode(mixed $data): string 94 | { 95 | $jsonOptions = JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; 96 | 97 | return (string) json_encode($data, $jsonOptions); 98 | } 99 | 100 | public function getAssetContents(string $asset): string 101 | { 102 | $assetPath = __DIR__."/../../resources/compiled/{$asset}"; 103 | 104 | return (string) file_get_contents($assetPath); 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | public function shareableReport(): array 111 | { 112 | return (new ReportTrimmer())->trim($this->report()); 113 | } 114 | 115 | public function updateConfigEndpoint(): string 116 | { 117 | // TODO: Should be based on Ignition config 118 | return '/_ignition/update-config'; 119 | } 120 | 121 | public function customHtmlHead(): string 122 | { 123 | return $this->customHtmlHead; 124 | } 125 | 126 | public function customHtmlBody(): string 127 | { 128 | return $this->customHtmlBody; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Config/FileConfigManager.php: -------------------------------------------------------------------------------- 1 | path = $this->initPath($path); 19 | $this->file = $this->initFile(); 20 | } 21 | 22 | protected function initPath(string $path): string 23 | { 24 | $path = $this->retrievePath($path); 25 | 26 | if (! $this->isValidWritablePath($path)) { 27 | return ''; 28 | } 29 | 30 | return $this->preparePath($path); 31 | } 32 | 33 | protected function retrievePath(string $path): string 34 | { 35 | if ($path !== '') { 36 | return $path; 37 | } 38 | 39 | return $this->initPathFromEnvironment(); 40 | } 41 | 42 | protected function isValidWritablePath(string $path): bool 43 | { 44 | return @file_exists($path) && @is_writable($path); 45 | } 46 | 47 | protected function preparePath(string $path): string 48 | { 49 | return rtrim($path, DIRECTORY_SEPARATOR); 50 | } 51 | 52 | protected function initPathFromEnvironment(): string 53 | { 54 | if (! empty($_SERVER['HOMEDRIVE']) && ! empty($_SERVER['HOMEPATH'])) { 55 | return $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; 56 | } 57 | 58 | if (! empty(getenv('HOME'))) { 59 | return getenv('HOME'); 60 | } 61 | 62 | return ''; 63 | } 64 | 65 | protected function initFile(): string 66 | { 67 | return $this->path . DIRECTORY_SEPARATOR . self::SETTINGS_FILE_NAME; 68 | } 69 | 70 | /** {@inheritDoc} */ 71 | public function load(): array 72 | { 73 | return $this->readFromFile(); 74 | } 75 | 76 | /** @return array */ 77 | protected function readFromFile(): array 78 | { 79 | if (! $this->isValidFile()) { 80 | return []; 81 | } 82 | 83 | $content = (string)file_get_contents($this->file); 84 | $settings = json_decode($content, true) ?? []; 85 | 86 | return $settings; 87 | } 88 | 89 | protected function isValidFile(): bool 90 | { 91 | return $this->isValidPath() && 92 | @file_exists($this->file) && 93 | @is_writable($this->file); 94 | } 95 | 96 | protected function isValidPath(): bool 97 | { 98 | return trim($this->path) !== ''; 99 | } 100 | 101 | /** {@inheritDoc} */ 102 | public function save(array $options): bool 103 | { 104 | if (! $this->createFile()) { 105 | return false; 106 | } 107 | 108 | return $this->saveToFile($options); 109 | } 110 | 111 | protected function createFile(): bool 112 | { 113 | if (! $this->isValidPath()) { 114 | return false; 115 | } 116 | 117 | if (@file_exists($this->file)) { 118 | return true; 119 | } 120 | 121 | return (file_put_contents($this->file, '') !== false); 122 | } 123 | 124 | /** 125 | * @param array $options 126 | * 127 | * @return bool 128 | */ 129 | protected function saveToFile(array $options): bool 130 | { 131 | try { 132 | $content = json_encode($options, JSON_THROW_ON_ERROR); 133 | } catch (Throwable) { 134 | return false; 135 | } 136 | 137 | return $this->writeToFile($content); 138 | } 139 | 140 | protected function writeToFile(string $content): bool 141 | { 142 | if (! $this->isValidFile()) { 143 | return false; 144 | } 145 | 146 | return (file_put_contents($this->file, $content) !== false); 147 | } 148 | 149 | /** {@inheritDoc} */ 150 | public function getPersistentInfo(): array 151 | { 152 | return [ 153 | 'name' => self::SETTINGS_FILE_NAME, 154 | 'path' => $this->path, 155 | 'file' => $this->file, 156 | ]; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Config/IgnitionConfig.php: -------------------------------------------------------------------------------- 1 | > */ 10 | class IgnitionConfig implements Arrayable 11 | { 12 | private ConfigManager $manager; 13 | 14 | public static function loadFromConfigFile(): self 15 | { 16 | return (new self())->loadConfigFile(); 17 | } 18 | 19 | /** 20 | * @param array $options 21 | */ 22 | public function __construct(protected array $options = []) 23 | { 24 | $defaultOptions = $this->getDefaultOptions(); 25 | 26 | $this->options = array_merge($defaultOptions, $options); 27 | $this->manager = $this->initConfigManager(); 28 | } 29 | 30 | public function setOption(string $name, string $value): self 31 | { 32 | $this->options[$name] = $value; 33 | 34 | return $this; 35 | } 36 | 37 | private function initConfigManager(): ConfigManager 38 | { 39 | try { 40 | /** @phpstan-ignore-next-line */ 41 | return app(ConfigManager::class); 42 | } catch (Throwable) { 43 | return new FileConfigManager(); 44 | } 45 | } 46 | 47 | /** @param array $options */ 48 | public function merge(array $options): self 49 | { 50 | $this->options = array_merge($this->options, $options); 51 | 52 | return $this; 53 | } 54 | 55 | public function loadConfigFile(): self 56 | { 57 | $this->merge($this->getConfigOptions()); 58 | 59 | return $this; 60 | } 61 | 62 | /** @return array */ 63 | public function getConfigOptions(): array 64 | { 65 | return $this->manager->load(); 66 | } 67 | 68 | /** 69 | * @param array $options 70 | * @return bool 71 | */ 72 | public function saveValues(array $options): bool 73 | { 74 | return $this->manager->save($options); 75 | } 76 | 77 | public function hideSolutions(): bool 78 | { 79 | return $this->options['hide_solutions'] ?? false; 80 | } 81 | 82 | public function editor(): ?string 83 | { 84 | return $this->options['editor'] ?? null; 85 | } 86 | 87 | /** 88 | * @return array $options 89 | */ 90 | public function editorOptions(): array 91 | { 92 | return $this->options['editor_options'] ?? []; 93 | } 94 | 95 | public function remoteSitesPath(): ?string 96 | { 97 | return $this->options['remote_sites_path'] ?? null; 98 | } 99 | 100 | public function localSitesPath(): ?string 101 | { 102 | return $this->options['local_sites_path'] ?? null; 103 | } 104 | 105 | public function theme(): ?string 106 | { 107 | return $this->options['theme'] ?? null; 108 | } 109 | 110 | public function shareButtonEnabled(): bool 111 | { 112 | return (bool)($this->options['enable_share_button'] ?? false); 113 | } 114 | 115 | public function shareEndpoint(): string 116 | { 117 | return $this->options['share_endpoint'] 118 | ?? $this->getDefaultOptions()['share_endpoint']; 119 | } 120 | 121 | public function runnableSolutionsEnabled(): bool 122 | { 123 | return (bool)($this->options['enable_runnable_solutions'] ?? false); 124 | } 125 | 126 | /** @return array> */ 127 | public function toArray(): array 128 | { 129 | return [ 130 | 'editor' => $this->editor(), 131 | 'theme' => $this->theme(), 132 | 'hideSolutions' => $this->hideSolutions(), 133 | 'remoteSitesPath' => $this->remoteSitesPath(), 134 | 'localSitesPath' => $this->localSitesPath(), 135 | 'enableShareButton' => $this->shareButtonEnabled(), 136 | 'enableRunnableSolutions' => $this->runnableSolutionsEnabled(), 137 | 'directorySeparator' => DIRECTORY_SEPARATOR, 138 | 'editorOptions' => $this->editorOptions(), 139 | 'shareEndpoint' => $this->shareEndpoint(), 140 | ]; 141 | } 142 | 143 | /** 144 | * @return array $options 145 | */ 146 | protected function getDefaultOptions(): array 147 | { 148 | return [ 149 | 'share_endpoint' => 'https://flareapp.io/api/public-reports', 150 | 'theme' => 'light', 151 | 'editor' => 'vscode', 152 | 'editor_options' => [ 153 | 'clipboard' => [ 154 | 'label' => 'Clipboard', 155 | 'url' => '%path:%line', 156 | 'clipboard' => true, 157 | ], 158 | 'sublime' => [ 159 | 'label' => 'Sublime', 160 | 'url' => 'subl://open?url=file://%path&line=%line', 161 | ], 162 | 'textmate' => [ 163 | 'label' => 'TextMate', 164 | 'url' => 'txmt://open?url=file://%path&line=%line', 165 | ], 166 | 'emacs' => [ 167 | 'label' => 'Emacs', 168 | 'url' => 'emacs://open?url=file://%path&line=%line', 169 | ], 170 | 'macvim' => [ 171 | 'label' => 'MacVim', 172 | 'url' => 'mvim://open/?url=file://%path&line=%line', 173 | ], 174 | 'phpstorm' => [ 175 | 'label' => 'PhpStorm', 176 | 'url' => 'phpstorm://open?file=%path&line=%line', 177 | ], 178 | 'phpstorm-remote' => [ 179 | 'label' => 'PHPStorm Remote', 180 | 'url' => 'javascript:r = new XMLHttpRequest;r.open("get", "http://localhost:63342/api/file/%path:%line");r.send()', 181 | ], 182 | 'idea' => [ 183 | 'label' => 'Idea', 184 | 'url' => 'idea://open?file=%path&line=%line', 185 | ], 186 | 'vscode' => [ 187 | 'label' => 'VS Code', 188 | 'url' => 'vscode://file/%path:%line', 189 | ], 190 | 'vscode-insiders' => [ 191 | 'label' => 'VS Code Insiders', 192 | 'url' => 'vscode-insiders://file/%path:%line', 193 | ], 194 | 'vscode-remote' => [ 195 | 'label' => 'VS Code Remote', 196 | 'url' => 'vscode://vscode-remote/%path:%line', 197 | ], 198 | 'vscode-insiders-remote' => [ 199 | 'label' => 'VS Code Insiders Remote', 200 | 'url' => 'vscode-insiders://vscode-remote/%path:%line', 201 | ], 202 | 'vscodium' => [ 203 | 'label' => 'VS Codium', 204 | 'url' => 'vscodium://file/%path:%line', 205 | ], 206 | 'cursor' => [ 207 | 'label' => 'Cursor', 208 | 'url' => 'cursor://file/%path:%line', 209 | ], 210 | 'atom' => [ 211 | 'label' => 'Atom', 212 | 'url' => 'atom://core/open/file?filename=%path&line=%line', 213 | ], 214 | 'nova' => [ 215 | 'label' => 'Nova', 216 | 'url' => 'nova://open?path=%path&line=%line', 217 | ], 218 | 'netbeans' => [ 219 | 'label' => 'NetBeans', 220 | 'url' => 'netbeans://open/?f=%path:%line', 221 | ], 222 | 'xdebug' => [ 223 | 'label' => 'Xdebug', 224 | 'url' => 'xdebug://%path@%line', 225 | ], 226 | ], 227 | ]; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/Ignition.php: -------------------------------------------------------------------------------- 1 | */ 37 | protected array $middleware = []; 38 | 39 | protected IgnitionConfig $ignitionConfig; 40 | 41 | protected ContextProviderDetector $contextProviderDetector; 42 | 43 | protected SolutionProviderRepositoryContract $solutionProviderRepository; 44 | 45 | protected ?bool $inProductionEnvironment = null; 46 | 47 | protected ?string $solutionTransformerClass = null; 48 | 49 | /** @var ArrayObject */ 50 | protected ArrayObject $documentationLinkResolvers; 51 | 52 | protected string $customHtmlHead = ''; 53 | 54 | protected string $customHtmlBody = ''; 55 | 56 | public static function make(): self 57 | { 58 | return new self(); 59 | } 60 | 61 | public function __construct( 62 | ?Flare $flare = null, 63 | ) { 64 | $this->flare = $flare ?? Flare::make(); 65 | 66 | $this->ignitionConfig = IgnitionConfig::loadFromConfigFile(); 67 | 68 | $this->solutionProviderRepository = new SolutionProviderRepository($this->getDefaultSolutionProviders()); 69 | 70 | $this->documentationLinkResolvers = new ArrayObject(); 71 | 72 | $this->contextProviderDetector = new BaseContextProviderDetector(); 73 | 74 | $this->middleware[] = new AddSolutions($this->solutionProviderRepository); 75 | $this->middleware[] = new AddDocumentationLinks($this->documentationLinkResolvers); 76 | } 77 | 78 | public function setSolutionTransformerClass(string $solutionTransformerClass): self 79 | { 80 | $this->solutionTransformerClass = $solutionTransformerClass; 81 | 82 | return $this; 83 | } 84 | 85 | /** @param callable(Throwable): mixed $callable */ 86 | public function resolveDocumentationLink(callable $callable): self 87 | { 88 | $this->documentationLinkResolvers[] = $callable; 89 | 90 | return $this; 91 | } 92 | 93 | public function setConfig(IgnitionConfig $ignitionConfig): self 94 | { 95 | $this->ignitionConfig = $ignitionConfig; 96 | 97 | return $this; 98 | } 99 | 100 | public function runningInProductionEnvironment(bool $boolean = true): self 101 | { 102 | $this->inProductionEnvironment = $boolean; 103 | 104 | return $this; 105 | } 106 | 107 | public function getFlare(): Flare 108 | { 109 | return $this->flare; 110 | } 111 | 112 | public function setFlare(Flare $flare): self 113 | { 114 | $this->flare = $flare; 115 | 116 | return $this; 117 | } 118 | 119 | public function setSolutionProviderRepository(SolutionProviderRepositoryContract $solutionProviderRepository): self 120 | { 121 | $this->solutionProviderRepository = $solutionProviderRepository; 122 | 123 | return $this; 124 | } 125 | 126 | public function shouldDisplayException(bool $shouldDisplayException): self 127 | { 128 | $this->shouldDisplayException = $shouldDisplayException; 129 | 130 | return $this; 131 | } 132 | 133 | public function applicationPath(string $applicationPath): self 134 | { 135 | $this->applicationPath = $applicationPath; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @param string $name 142 | * @param string $messageLevel 143 | * @param array $metaData 144 | * 145 | * @return $this 146 | */ 147 | public function glow( 148 | string $name, 149 | string $messageLevel = MessageLevels::INFO, 150 | array $metaData = [] 151 | ): self { 152 | $this->flare->glow($name, $messageLevel, $metaData); 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * @param array> $solutionProviders 159 | * 160 | * @return $this 161 | */ 162 | public function addSolutionProviders(array $solutionProviders): self 163 | { 164 | $this->solutionProviderRepository->registerSolutionProviders($solutionProviders); 165 | 166 | return $this; 167 | } 168 | 169 | /** @deprecated Use `setTheme('dark')` instead */ 170 | public function useDarkMode(): self 171 | { 172 | return $this->setTheme('dark'); 173 | } 174 | 175 | /** @deprecated Use `setTheme($theme)` instead */ 176 | public function theme(string $theme): self 177 | { 178 | return $this->setTheme($theme); 179 | } 180 | 181 | public function setTheme(string $theme): self 182 | { 183 | $this->ignitionConfig->setOption('theme', $theme); 184 | 185 | return $this; 186 | } 187 | 188 | public function setEditor(string $editor): self 189 | { 190 | $this->ignitionConfig->setOption('editor', $editor); 191 | 192 | return $this; 193 | } 194 | 195 | public function sendToFlare(?string $apiKey): self 196 | { 197 | $this->flareApiKey = $apiKey ?? ''; 198 | 199 | return $this; 200 | } 201 | 202 | public function configureFlare(callable $callable): self 203 | { 204 | ($callable)($this->flare); 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @param FlareMiddleware|array $middleware 211 | * 212 | * @return $this 213 | */ 214 | public function registerMiddleware(array|FlareMiddleware $middleware): self 215 | { 216 | if (! is_array($middleware)) { 217 | $middleware = [$middleware]; 218 | } 219 | 220 | foreach ($middleware as $singleMiddleware) { 221 | $this->middleware = array_merge($this->middleware, $middleware); 222 | } 223 | 224 | return $this; 225 | } 226 | 227 | public function setContextProviderDetector(ContextProviderDetector $contextProviderDetector): self 228 | { 229 | $this->contextProviderDetector = $contextProviderDetector; 230 | 231 | return $this; 232 | } 233 | 234 | public function reset(): self 235 | { 236 | $this->flare->reset(); 237 | 238 | return $this; 239 | } 240 | 241 | public function register(?int $errorLevels = null): self 242 | { 243 | error_reporting($errorLevels ?? -1); 244 | 245 | $errorLevels 246 | ? set_error_handler([$this, 'renderError'], $errorLevels) 247 | : set_error_handler([$this, 'renderError']); 248 | 249 | set_exception_handler([$this, 'handleException']); 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * @param int $level 256 | * @param string $message 257 | * @param string $file 258 | * @param int $line 259 | * @param array $context 260 | * 261 | * @return void 262 | * @throws \ErrorException 263 | */ 264 | public function renderError( 265 | int $level, 266 | string $message, 267 | string $file = '', 268 | int $line = 0, 269 | array $context = [] 270 | ): void { 271 | if (error_reporting() === (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE)) { 272 | // This happens when PHP version is >=8 and we caught an error that was suppressed with the "@" operator 273 | // See the first warning box in https://www.php.net/manual/en/language.operators.errorcontrol.php 274 | return; 275 | } 276 | 277 | throw new ErrorException($message, 0, $level, $file, $line); 278 | } 279 | 280 | /** 281 | * This is the main entry point for the framework agnostic Ignition package. 282 | * Displays the Ignition page and optionally sends a report to Flare. 283 | */ 284 | public function handleException(Throwable $throwable): Report 285 | { 286 | $this->setUpFlare(); 287 | 288 | $report = $this->createReport($throwable); 289 | 290 | if ($this->shouldDisplayException && $this->inProductionEnvironment !== true) { 291 | $this->renderException($throwable, $report); 292 | } 293 | 294 | if ($this->flare->apiTokenSet() && $this->inProductionEnvironment !== false) { 295 | $this->flare->report($throwable, report: $report); 296 | } 297 | 298 | return $report; 299 | } 300 | 301 | /** 302 | * This is the main entrypoint for laravel-ignition. It only renders the exception. 303 | * Sending the report to Flare is handled in the laravel-ignition log handler. 304 | */ 305 | public function renderException(Throwable $throwable, ?Report $report = null): void 306 | { 307 | $this->setUpFlare(); 308 | 309 | $report ??= $this->createReport($throwable); 310 | 311 | $viewModel = new ErrorPageViewModel( 312 | $throwable, 313 | $this->ignitionConfig, 314 | $report, 315 | $this->solutionProviderRepository->getSolutionsForThrowable($throwable), 316 | $this->solutionTransformerClass, 317 | $this->customHtmlHead, 318 | $this->customHtmlBody, 319 | ); 320 | 321 | (new Renderer())->render(['viewModel' => $viewModel], self::viewPath('errorPage')); 322 | } 323 | 324 | public static function viewPath(string $viewName): string 325 | { 326 | return __DIR__ . "/../resources/views/{$viewName}.php"; 327 | } 328 | 329 | /** 330 | * Add custom HTML which will be added to the head tag of the error page. 331 | */ 332 | public function addCustomHtmlToHead(string $html): self 333 | { 334 | $this->customHtmlHead .= $html; 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * Add custom HTML which will be added to the body tag of the error page. 341 | */ 342 | public function addCustomHtmlToBody(string $html): self 343 | { 344 | $this->customHtmlBody .= $html; 345 | 346 | return $this; 347 | } 348 | 349 | protected function setUpFlare(): self 350 | { 351 | if (! $this->flare->apiTokenSet()) { 352 | $this->flare->setApiToken($this->flareApiKey ?? ''); 353 | } 354 | 355 | $this->flare->setContextProviderDetector($this->contextProviderDetector); 356 | 357 | foreach ($this->middleware as $singleMiddleware) { 358 | $this->flare->registerMiddleware($singleMiddleware); 359 | } 360 | 361 | if ($this->applicationPath !== '') { 362 | $this->flare->applicationPath($this->applicationPath); 363 | } 364 | 365 | return $this; 366 | } 367 | 368 | /** @return array> */ 369 | protected function getDefaultSolutionProviders(): array 370 | { 371 | return [ 372 | BadMethodCallSolutionProvider::class, 373 | MergeConflictSolutionProvider::class, 374 | UndefinedPropertySolutionProvider::class, 375 | ]; 376 | } 377 | 378 | protected function createReport(Throwable $throwable): Report 379 | { 380 | return $this->flare->createReport($throwable); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ignition: a beautiful error page for PHP apps 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/ignition.svg?style=flat-square)](https://packagist.org/packages/spatie/ignition) 4 | [![Run tests](https://github.com/spatie/ignition/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/ignition/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/ignition.svg?style=flat-square)](https://packagist.org/packages/spatie/ignition) 6 | 7 | [Ignition](https://flareapp.io/docs/ignition-for-laravel/introduction) is a beautiful and customizable error page for 8 | PHP applications 9 | 10 | Here's a minimal example on how to register ignition. 11 | 12 | ```php 13 | use Spatie\Ignition\Ignition; 14 | 15 | include 'vendor/autoload.php'; 16 | 17 | Ignition::make()->register(); 18 | ``` 19 | 20 | Let's now throw an exception during a web request. 21 | 22 | ```php 23 | throw new Exception('Bye world'); 24 | ``` 25 | 26 | This is what you'll see in the browser. 27 | 28 | ![Screenshot of ignition](https://spatie.github.io/ignition/ignition.png) 29 | 30 | There's also a beautiful dark mode. 31 | 32 | ![Screenshot of ignition in dark mode](https://spatie.github.io/ignition/ignition-dark.png) 33 | 34 | ## Are you a visual learner? 35 | 36 | In [this video on YouTube](https://youtu.be/LEY0N0Bteew?t=739), you'll see a demo of all of the features. 37 | 38 | Do know more about the design decisions we made, read [this blog post](https://freek.dev/2168-ignition-the-most-beautiful-error-page-for-laravel-and-php-got-a-major-redesign). 39 | 40 | ## Support us 41 | 42 | [](https://spatie.be/github-ad-click/ignition) 43 | 44 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can 45 | support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 46 | 47 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 48 | You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards 49 | on [our virtual postcard wall](https://spatie.be/open-source/postcards). 50 | 51 | ## Installation 52 | 53 | For Laravel apps, head over to [laravel-ignition](https://github.com/spatie/laravel-ignition). 54 | 55 | For Symfony apps, go to [symfony-ignition-bundle](https://github.com/spatie/symfony-ignition-bundle). 56 | 57 | For Drupal 10+ websites, use the [Ignition module](https://www.drupal.org/project/ignition). 58 | 59 | For OpenMage websites, use the [Ignition module](https://github.com/empiricompany/openmage_ignition). 60 | 61 | For all other PHP projects, install the package via composer: 62 | 63 | ```bash 64 | composer require spatie/ignition 65 | ``` 66 | 67 | ## Usage 68 | 69 | In order to display the Ignition error page when an error occurs in your project, you must add this code. Typically, this would be done in the bootstrap part of your application. 70 | 71 | ```php 72 | \Spatie\Ignition\Ignition::make()->register(); 73 | ``` 74 | 75 | ### Setting the application path 76 | 77 | When setting the application path, Ignition will trim the given value from all paths. This will make the error page look 78 | more cleaner. 79 | 80 | ```php 81 | \Spatie\Ignition\Ignition::make() 82 | ->applicationPath($basePathOfYourApplication) 83 | ->register(); 84 | ``` 85 | 86 | ### Using dark mode 87 | 88 | By default, Ignition uses a nice white based theme. If this is too bright for your eyes, you can use dark mode. 89 | 90 | ```php 91 | \Spatie\Ignition\Ignition::make() 92 | ->setTheme('dark') 93 | ->register(); 94 | ``` 95 | 96 | ### Avoid rendering Ignition in a production environment 97 | 98 | You don't want to render the Ignition error page in a production environment, as it potentially can display sensitive 99 | information. 100 | 101 | To avoid rendering Ignition, you can call `shouldDisplayException` and pass it a falsy value. 102 | 103 | ```php 104 | \Spatie\Ignition\Ignition::make() 105 | ->shouldDisplayException($inLocalEnvironment) 106 | ->register(); 107 | ``` 108 | 109 | ### Displaying solutions 110 | 111 | In addition to displaying an exception, Ignition can display a solution as well. 112 | 113 | Out of the box, Ignition will display solutions for common errors such as bad methods calls, or using undefined properties. 114 | 115 | #### Adding a solution directly to an exception 116 | 117 | To add a solution text to your exception, let the exception implement the `Spatie\Ignition\Contracts\ProvidesSolution` 118 | interface. 119 | 120 | This interface requires you to implement one method, which is going to return the `Solution` that users will see when 121 | the exception gets thrown. 122 | 123 | ```php 124 | use Spatie\Ignition\Contracts\Solution; 125 | use Spatie\Ignition\Contracts\ProvidesSolution; 126 | 127 | class CustomException extends Exception implements ProvidesSolution 128 | { 129 | public function getSolution(): Solution 130 | { 131 | return new CustomSolution(); 132 | } 133 | } 134 | ``` 135 | 136 | ```php 137 | use Spatie\Ignition\Contracts\Solution; 138 | 139 | class CustomSolution implements Solution 140 | { 141 | public function getSolutionTitle(): string 142 | { 143 | return 'The solution title goes here'; 144 | } 145 | 146 | public function getSolutionDescription(): string 147 | { 148 | return 'This is a longer description of the solution that you want to show.'; 149 | } 150 | 151 | public function getDocumentationLinks(): array 152 | { 153 | return [ 154 | 'Your documentation' => 'https://your-project.com/relevant-docs-page', 155 | ]; 156 | } 157 | } 158 | ``` 159 | 160 | This is how the exception would be displayed if you were to throw it. 161 | 162 | ![Screenshot of solution](https://spatie.github.io/ignition/solution.png) 163 | 164 | #### Using solution providers 165 | 166 | Instead of adding solutions to exceptions directly, you can also create a solution provider. While exceptions that 167 | return a solution, provide the solution directly to Ignition, a solution provider allows you to figure out if an 168 | exception can be solved. 169 | 170 | For example, you could create a custom "Stack Overflow solution provider", that will look up if a solution can be found 171 | for a given throwable. 172 | 173 | Solution providers can be added by third party packages or within your own application. 174 | 175 | A solution provider is any class that implements the \Spatie\Ignition\Contracts\HasSolutionsForThrowable interface. 176 | 177 | This is how the interface looks like: 178 | 179 | ```php 180 | interface HasSolutionsForThrowable 181 | { 182 | public function canSolve(Throwable $throwable): bool; 183 | 184 | /** @return \Spatie\Ignition\Contracts\Solution[] */ 185 | public function getSolutions(Throwable $throwable): array; 186 | } 187 | ``` 188 | 189 | When an error occurs in your app, the class will receive the `Throwable` in the `canSolve` method. In that method you 190 | can decide if your solution provider is applicable to the `Throwable` passed. If you return `true`, `getSolutions` will 191 | get called. 192 | 193 | To register a solution provider to Ignition you must call the `addSolutionProviders` method. 194 | 195 | ```php 196 | \Spatie\Ignition\Ignition::make() 197 | ->addSolutionProviders([ 198 | YourSolutionProvider::class, 199 | AnotherSolutionProvider::class, 200 | ]) 201 | ->register(); 202 | ``` 203 | 204 | ### AI powered solutions 205 | 206 | Ignition can send your exception to Open AI that will attempt to automatically suggest a solution. In many cases, the suggested solutions is quite useful, but keep in mind that the solution may not be 100% correct for your context. 207 | 208 | To generate AI powered solutions, you must first install this optional dependency. 209 | 210 | ```bash 211 | composer require openai-php/client 212 | ``` 213 | 214 | To start sending your errors to OpenAI, you must instanciate the `OpenAiSolutionProvider`. The constructor expects a OpenAI API key to be passed, you should generate this key [at OpenAI](https://platform.openai.com). 215 | 216 | ```php 217 | use \Spatie\Ignition\Solutions\OpenAi\OpenAiSolutionProvider; 218 | 219 | $aiSolutionProvider = new OpenAiSolutionProvider($openAiKey); 220 | ``` 221 | 222 | To use the solution provider, you should pass it to `addSolutionProviders` when registering Ignition. 223 | 224 | ```php 225 | \Spatie\Ignition\Ignition::make() 226 | ->addSolutionProviders([ 227 | $aiSolutionProvider, 228 | // other solution providers... 229 | ]) 230 | ->register(); 231 | ``` 232 | 233 | By default, the solution provider will send these bits of info to Open AI: 234 | 235 | - the error message 236 | - the error class 237 | - the stack frame 238 | - other small bits of info of context surrounding your error 239 | 240 | It will not send the request payload or any environment variables to avoid sending sensitive data to OpenAI. 241 | 242 | #### Caching requests to AI 243 | 244 | By default, all errors will be sent to OpenAI. Optionally, you can add caching so similar errors will only get sent to OpenAI once. To cache errors, you can call `useCache` on `$aiSolutionProvider`. You should pass [a simple-cache-implementation](https://packagist.org/providers/psr/simple-cache-implementation). Here's the signature of the `useCache` method. 245 | 246 | ```php 247 | public function useCache(CacheInterface $cache, int $cacheTtlInSeconds = 60 * 60) 248 | ``` 249 | 250 | #### Hinting the application type 251 | 252 | To increase the quality of the suggested solutions, you can send along the application type (Symfony, Drupal, WordPress, ...) to the AI. 253 | 254 | To send the application type call `applicationType` on the solution provider. 255 | 256 | ```php 257 | $aiSolutionProvider->applicationType('WordPress 6.2') 258 | ``` 259 | 260 | ### Sending exceptions to Flare 261 | 262 | Ignition comes with the ability to send exceptions to [Flare](https://flareapp.io), an exception monitoring service. Flare 263 | can notify you when new exceptions are occurring in your production environment. 264 | 265 | To send exceptions to Flare, simply call the `sendToFlareMethod` and pass it the API key you got when creating a project 266 | on Flare. 267 | 268 | You probably want to combine this with calling `runningInProductionEnvironment`. That method will, when passed a truthy 269 | value, not display the Ignition error page, but only send the exception to Flare. 270 | 271 | ```php 272 | \Spatie\Ignition\Ignition::make() 273 | ->runningInProductionEnvironment($boolean) 274 | ->sendToFlare($yourApiKey) 275 | ->register(); 276 | ``` 277 | 278 | When you pass a falsy value to `runningInProductionEnvironment`, the Ignition error page will get shown, but no 279 | exceptions will be sent to Flare. 280 | 281 | ### Sending custom context to Flare 282 | 283 | When you send an error to Flare, you can add custom information that will be sent along with every exception that 284 | happens in your application. This can be very useful if you want to provide key-value related information that 285 | furthermore helps you to debug a possible exception. 286 | 287 | ```php 288 | use Spatie\FlareClient\Flare; 289 | 290 | \Spatie\Ignition\Ignition::make() 291 | ->runningInProductionEnvironment($boolean) 292 | ->sendToFlare($yourApiKey) 293 | ->configureFlare(function(Flare $flare) { 294 | $flare->context('Tenant', 'My-Tenant-Identifier'); 295 | }) 296 | ->register(); 297 | ``` 298 | 299 | Sometimes you may want to group your context items by a key that you provide to have an easier visual differentiation 300 | when you look at your custom context items. 301 | 302 | The Flare client allows you to also provide your own custom context groups like this: 303 | 304 | ```php 305 | use Spatie\FlareClient\Flare; 306 | 307 | \Spatie\Ignition\Ignition::make() 308 | ->runningInProductionEnvironment($boolean) 309 | ->sendToFlare($yourApiKey) 310 | ->configureFlare(function(Flare $flare) { 311 | $flare->group('Custom information', [ 312 | 'key' => 'value', 313 | 'another key' => 'another value', 314 | ]); 315 | }) 316 | ->register(); 317 | ``` 318 | 319 | ### Anonymize request to Flare 320 | 321 | By default, the Ignition collects information about the IP address of your application users. If you don't want to send this information to Flare, call `anonymizeIp()`. 322 | 323 | ```php 324 | use Spatie\FlareClient\Flare; 325 | 326 | \Spatie\Ignition\Ignition::make() 327 | ->runningInProductionEnvironment($boolean) 328 | ->sendToFlare($yourApiKey) 329 | ->configureFlare(function(Flare $flare) { 330 | $flare->anonymizeIp(); 331 | }) 332 | ->register(); 333 | ``` 334 | 335 | ### Censoring request body fields 336 | 337 | When an exception occurs in a web request, the Flare client will pass on any request fields that are present in the body. 338 | 339 | In some cases, such as a login page, these request fields may contain a password that you don't want to send to Flare. 340 | 341 | To censor out values of certain fields, you can use `censorRequestBodyFields`. You should pass it the names of the fields you wish to censor. 342 | 343 | ```php 344 | use Spatie\FlareClient\Flare; 345 | 346 | \Spatie\Ignition\Ignition::make() 347 | ->runningInProductionEnvironment($boolean) 348 | ->sendToFlare($yourApiKey) 349 | ->configureFlare(function(Flare $flare) { 350 | $flare->censorRequestBodyFields(['password']); 351 | }) 352 | ->register(); 353 | ``` 354 | 355 | This will replace the value of any sent fields named "password" with the value "". 356 | 357 | ### Using middleware to modify data sent to Flare 358 | 359 | Before Flare receives the data that was collected from your local exception, we give you the ability to call custom middleware methods. These methods retrieve the report that should be sent to Flare and allow you to add custom information to that report. 360 | 361 | A valid middleware is any class that implements `FlareMiddleware`. 362 | 363 | ```php 364 | use Spatie\FlareClient\Report; 365 | 366 | use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; 367 | 368 | class MyMiddleware implements FlareMiddleware 369 | { 370 | public function handle(Report $report, Closure $next) 371 | { 372 | $report->message("{$report->getMessage()}, now modified"); 373 | 374 | return $next($report); 375 | } 376 | } 377 | ``` 378 | 379 | ```php 380 | use Spatie\FlareClient\Flare; 381 | 382 | \Spatie\Ignition\Ignition::make() 383 | ->runningInProductionEnvironment($boolean) 384 | ->sendToFlare($yourApiKey) 385 | ->configureFlare(function(Flare $flare) { 386 | $flare->registerMiddleware([ 387 | MyMiddleware::class, 388 | ]) 389 | }) 390 | ->register(); 391 | ``` 392 | 393 | ### Changelog 394 | 395 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 396 | 397 | ## Contributing 398 | 399 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 400 | 401 | ## Dev setup 402 | 403 | Here are the steps you'll need to perform if you want to work on the UI of Ignition. 404 | 405 | - clone (or move) `spatie/ignition`, `spatie/ignition-ui`, `spatie/laravel-ignition`, `spatie/flare-client-php` and `spatie/ignition-test` into the same directory (e.g. `~/code/flare`) 406 | - create a new `package.json` file in `~/code/flare` directory: 407 | ```json 408 | { 409 | "private": true, 410 | "workspaces": [ 411 | "ignition-ui", 412 | "ignition" 413 | ] 414 | } 415 | ``` 416 | - run `yarn install` in the `~/code/flare` directory 417 | - in the `~/code/flare/ignition-test` directory 418 | - run `composer update` 419 | - run `cp .env.example .env` 420 | - run `php artisan key:generate` 421 | - run `yarn dev` in both the `ignition` and `ignition-ui` project 422 | - http://ignition-test.test/ should now work (= show the new UI). If you use valet, you might want to run `valet park` inside the `~/code/flare` directory. 423 | - http://ignition-test.test/ has a bit of everything 424 | - http://ignition-test.test/sql-error has a solution and SQL exception 425 | 426 | ## Security Vulnerabilities 427 | 428 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 429 | 430 | ## Credits 431 | 432 | - [Spatie](https://spatie.be) 433 | - [All Contributors](../../contributors) 434 | 435 | ## License 436 | 437 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 438 | -------------------------------------------------------------------------------- /resources/compiled/ignition.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com 3 | */*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent;--tw-shadow:0 0 transparent;--tw-shadow-colored:0 0 transparent}::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent;--tw-shadow:0 0 transparent;--tw-shadow-colored:0 0 transparent}::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent;--tw-shadow:0 0 transparent;--tw-shadow-colored:0 0 transparent}html{font-size:max(13px,min(1.3vw,16px));overflow-x:hidden;overflow-y:scroll;font-feature-settings:"calt" 0;-webkit-marquee-increment:1vw}:after,:before,:not(iframe){position:relative}:focus{outline:0!important}body{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;width:100%;color:rgba(31,41,55,var(--tw-text-opacity))}.dark body,body{--tw-text-opacity:1}.dark body{color:rgba(229,231,235,var(--tw-text-opacity))}body{background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark body,body{--tw-bg-opacity:1}.dark body{background-color:rgba(17,24,39,var(--tw-bg-opacity))}@media (color-index:48){html.auto body{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}}@media (color:48842621){html.auto body{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}}@media (prefers-color-scheme:dark){html.auto body{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}}.scroll-target:target{content:"";display:block;position:absolute;top:-6rem}pre.sf-dump{display:block;white-space:pre;padding:5px;overflow:visible!important;overflow:initial!important}pre.sf-dump:after{content:"";visibility:hidden;display:block;height:0;clear:both}pre.sf-dump span{display:inline}pre.sf-dump a{text-decoration:none;cursor:pointer;border:0;outline:none;color:inherit}pre.sf-dump img{max-width:50em;max-height:50em;margin:.5em 0 0;padding:0;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAHUlEQVQY02O8zAABilCaiQEN0EeA8QuUcX9g3QEAAjcC5piyhyEAAAAASUVORK5CYII=) #d3d3d3}pre.sf-dump .sf-dump-ellipsis{display:inline-block;overflow:visible;text-overflow:ellipsis;max-width:5em;white-space:nowrap;overflow:hidden;vertical-align:top}pre.sf-dump .sf-dump-ellipsis+.sf-dump-ellipsis{max-width:none}pre.sf-dump code{display:inline;padding:0;background:none}.sf-dump-key.sf-dump-highlight,.sf-dump-private.sf-dump-highlight,.sf-dump-protected.sf-dump-highlight,.sf-dump-public.sf-dump-highlight,.sf-dump-str.sf-dump-highlight{background:rgba(111,172,204,.3);border:1px solid #7da0b1;border-radius:3px}.sf-dump-key.sf-dump-highlight-active,.sf-dump-private.sf-dump-highlight-active,.sf-dump-protected.sf-dump-highlight-active,.sf-dump-public.sf-dump-highlight-active,.sf-dump-str.sf-dump-highlight-active{background:rgba(253,175,0,.4);border:1px solid orange;border-radius:3px}pre.sf-dump .sf-dump-search-hidden{display:none!important}pre.sf-dump .sf-dump-search-wrapper{font-size:0;white-space:nowrap;margin-bottom:5px;display:flex;position:-webkit-sticky;position:sticky;top:5px}pre.sf-dump .sf-dump-search-wrapper>*{vertical-align:top;box-sizing:border-box;height:21px;font-weight:400;border-radius:0;background:#fff;color:#757575;border:1px solid #bbb}pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{padding:3px;height:21px;font-size:12px;border-right:none;border-top-left-radius:3px;border-bottom-left-radius:3px;color:#000;min-width:15px;width:100%}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next,pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous{background:#f2f2f2;outline:none;border-left:none;font-size:0;line-height:0}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next{border-top-right-radius:3px;border-bottom-right-radius:3px}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next>svg,pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous>svg{pointer-events:none;width:12px;height:12px}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-count{display:inline-block;padding:0 5px;margin:0;border-left:none;line-height:21px;font-size:12px}.hljs-comment,.hljs-quote{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark .hljs-comment,.dark .hljs-quote{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.hljs-comment.hljs-doctag{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.dark .hljs-comment.hljs-doctag{--tw-text-opacity:1;color:rgba(209,213,219,var(--tw-text-opacity))}.hljs-doctag,.hljs-formula,.hljs-keyword,.hljs-name{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.dark .hljs-doctag,.dark .hljs-formula,.dark .hljs-keyword,.dark .hljs-name{--tw-text-opacity:1;color:rgba(248,113,113,var(--tw-text-opacity))}.hljs-attr,.hljs-deletion,.hljs-function.hljs-keyword,.hljs-literal,.hljs-section,.hljs-selector-tag{--tw-text-opacity:1;color:rgba(139,92,246,var(--tw-text-opacity))}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{--tw-text-opacity:1;color:rgba(37,99,235,var(--tw-text-opacity))}.dark .hljs-addition,.dark .hljs-attribute,.dark .hljs-meta-string,.dark .hljs-regexp,.dark .hljs-string{--tw-text-opacity:1;color:rgba(96,165,250,var(--tw-text-opacity))}.hljs-built_in,.hljs-class .hljs-title,.hljs-template-tag,.hljs-template-variable{--tw-text-opacity:1;color:rgba(249,115,22,var(--tw-text-opacity))}.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-string.hljs-subst,.hljs-type{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.dark .hljs-number,.dark .hljs-selector-attr,.dark .hljs-selector-class,.dark .hljs-selector-pseudo,.dark .hljs-string.hljs-subst,.dark .hljs-type{--tw-text-opacity:1;color:rgba(52,211,153,var(--tw-text-opacity))}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-operator,.hljs-selector-id,.hljs-symbol,.hljs-title,.hljs-variable{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.dark .hljs-bullet,.dark .hljs-link,.dark .hljs-meta,.dark .hljs-operator,.dark .hljs-selector-id,.dark .hljs-symbol,.dark .hljs-title,.dark .hljs-variable{--tw-text-opacity:1;color:rgba(129,140,248,var(--tw-text-opacity))}.hljs-strong,.hljs-title{font-weight:700}.hljs-emphasis{font-style:italic}.hljs-link{-webkit-text-decoration-line:underline;text-decoration-line:underline}.language-sql .hljs-keyword{text-transform:uppercase}.mask-fade-x{-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 1rem,#000 calc(100% - 3rem),transparent calc(100% - 1rem))}.mask-fade-r{-webkit-mask-image:linear-gradient(90deg,#000 0,#000 calc(100% - 3rem),transparent calc(100% - 1rem))}.mask-fade-y{-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - 2.5rem),transparent)}.mask-fade-frames{-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - 4rem),transparent)}.scrollbar::-webkit-scrollbar,.scrollbar::-webkit-scrollbar-corner{width:2px;height:2px}.scrollbar::-webkit-scrollbar-track{background-color:transparent}.scrollbar::-webkit-scrollbar-thumb{background-color:rgba(239,68,68,.9)}.scrollbar-lg::-webkit-scrollbar,.scrollbar-lg::-webkit-scrollbar-corner{width:4px;height:4px}.scrollbar-lg::-webkit-scrollbar-track{background-color:transparent}.scrollbar-lg::-webkit-scrollbar-thumb{background-color:rgba(239,68,68,.9)}.scrollbar-hidden-x{-ms-overflow-style:none;scrollbar-width:none;overflow-x:scroll}.scrollbar-hidden-x::-webkit-scrollbar{display:none}.scrollbar-hidden-y{-ms-overflow-style:none;scrollbar-width:none;overflow-y:scroll}.scrollbar-hidden-y::-webkit-scrollbar{display:none}main pre.sf-dump{display:block!important;z-index:0!important;padding:0!important;font-size:.875rem!important;line-height:1.25rem!important}.sf-dump-key.sf-dump-highlight,.sf-dump-private.sf-dump-highlight,.sf-dump-protected.sf-dump-highlight,.sf-dump-public.sf-dump-highlight,.sf-dump-str.sf-dump-highlight{background-color:rgba(139,92,246,.1)!important}.sf-dump-key.sf-dump-highlight-active,.sf-dump-private.sf-dump-highlight-active,.sf-dump-protected.sf-dump-highlight-active,.sf-dump-public.sf-dump-highlight-active,.sf-dump-str.sf-dump-highlight-active{background-color:rgba(245,158,11,.1)!important}pre.sf-dump .sf-dump-search-wrapper{align-items:center}pre.sf-dump .sf-dump-search-wrapper>*{border-width:0!important}pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{font-size:.75rem!important;line-height:1rem!important;--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.dark pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity))}pre.sf-dump .sf-dump-search-wrapper>input.sf-dump-search-input{height:2rem!important;padding-left:.5rem!important;padding-right:.5rem!important}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next,pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous{background-color:transparent!important;--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next,.dark pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next:hover,pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous:hover{--tw-text-opacity:1!important;color:rgba(99,102,241,var(--tw-text-opacity))!important}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-next,pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-input-previous{padding-left:.25rem;padding-right:.25rem}pre.sf-dump .sf-dump-search-wrapper svg path{fill:currentColor}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-count{font-size:.75rem!important;line-height:1rem!important;line-height:1.5!important;padding-left:1rem!important;padding-right:1rem!important;--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-count{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}pre.sf-dump .sf-dump-search-wrapper>.sf-dump-search-count{background-color:transparent!important}pre.sf-dump,pre.sf-dump .sf-dump-default{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important;background-color:transparent!important;--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.dark pre.sf-dump,.dark pre.sf-dump .sf-dump-default{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity))}pre.sf-dump .sf-dump-num{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-num{--tw-text-opacity:1;color:rgba(52,211,153,var(--tw-text-opacity))}pre.sf-dump .sf-dump-const{font-weight:400!important;--tw-text-opacity:1!important;color:rgba(139,92,246,var(--tw-text-opacity))!important}pre.sf-dump .sf-dump-str{font-weight:400!important;--tw-text-opacity:1;color:rgba(37,99,235,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-str{--tw-text-opacity:1;color:rgba(96,165,250,var(--tw-text-opacity))}pre.sf-dump .sf-dump-note{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-note{--tw-text-opacity:1;color:rgba(129,140,248,var(--tw-text-opacity))}pre.sf-dump .sf-dump-ref{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-ref{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}pre.sf-dump .sf-dump-private,pre.sf-dump .sf-dump-protected,pre.sf-dump .sf-dump-public{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-private,.dark pre.sf-dump .sf-dump-protected,.dark pre.sf-dump .sf-dump-public{--tw-text-opacity:1;color:rgba(248,113,113,var(--tw-text-opacity))}pre.sf-dump .sf-dump-meta{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-meta{--tw-text-opacity:1;color:rgba(129,140,248,var(--tw-text-opacity))}pre.sf-dump .sf-dump-key{--tw-text-opacity:1;color:rgba(124,58,237,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-key{--tw-text-opacity:1;color:rgba(167,139,250,var(--tw-text-opacity))}pre.sf-dump .sf-dump-index{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-index{--tw-text-opacity:1;color:rgba(52,211,153,var(--tw-text-opacity))}pre.sf-dump .sf-dump-ellipsis{--tw-text-opacity:1;color:rgba(124,58,237,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-ellipsis{--tw-text-opacity:1;color:rgba(167,139,250,var(--tw-text-opacity))}pre.sf-dump .sf-dump-toggle{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark pre.sf-dump .sf-dump-toggle{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}pre.sf-dump .sf-dump-toggle:hover{--tw-text-opacity:1!important;color:rgba(99,102,241,var(--tw-text-opacity))!important}pre.sf-dump .sf-dump-toggle span{display:inline-flex!important;align-items:center!important;justify-content:center!important;width:1rem!important;height:1rem!important;font-size:9px;background-color:rgba(107,114,128,.05)}.dark pre.sf-dump .sf-dump-toggle span{background-color:rgba(0,0,0,.1)}pre.sf-dump .sf-dump-toggle span:hover{--tw-bg-opacity:1!important;background-color:rgba(255,255,255,var(--tw-bg-opacity))!important}.dark pre.sf-dump .sf-dump-toggle span:hover{--tw-bg-opacity:1!important;background-color:rgba(17,24,39,var(--tw-bg-opacity))!important}pre.sf-dump .sf-dump-toggle span{border-radius:9999px;--tw-shadow:0 1px 2px 0 rgba(0,0,0,0.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}pre.sf-dump .sf-dump-toggle span,pre.sf-dump .sf-dump-toggle span:hover{box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}pre.sf-dump .sf-dump-toggle span:hover{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px -1px rgba(0,0,0,0.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);--tw-text-opacity:1!important;color:rgba(99,102,241,var(--tw-text-opacity))!important}pre.sf-dump .sf-dump-toggle span{top:-2px}pre.sf-dump .sf-dump-toggle:hover span{--tw-bg-opacity:1!important;background-color:rgba(255,255,255,var(--tw-bg-opacity))!important}.dark pre.sf-dump .sf-dump-toggle:hover span{--tw-bg-opacity:1!important;background-color:rgba(17,24,39,var(--tw-bg-opacity))!important}.\~text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.dark .\~text-gray-500{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.\~text-violet-500{--tw-text-opacity:1;color:rgba(139,92,246,var(--tw-text-opacity))}.dark .\~text-violet-500{--tw-text-opacity:1;color:rgba(167,139,250,var(--tw-text-opacity))}.\~text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.dark .\~text-gray-600{--tw-text-opacity:1;color:rgba(209,213,219,var(--tw-text-opacity))}.\~text-indigo-600{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.dark .\~text-indigo-600{--tw-text-opacity:1;color:rgba(129,140,248,var(--tw-text-opacity))}.hover\:\~text-indigo-600:hover{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}:is(.dark .hover\:\~text-indigo-600):hover{--tw-text-opacity:1;color:rgba(129,140,248,var(--tw-text-opacity))}.\~text-blue-600{--tw-text-opacity:1;color:rgba(37,99,235,var(--tw-text-opacity))}.dark .\~text-blue-600{--tw-text-opacity:1;color:rgba(96,165,250,var(--tw-text-opacity))}.\~text-violet-600{--tw-text-opacity:1;color:rgba(124,58,237,var(--tw-text-opacity))}.dark .\~text-violet-600{--tw-text-opacity:1;color:rgba(167,139,250,var(--tw-text-opacity))}.hover\:\~text-violet-600:hover{--tw-text-opacity:1;color:rgba(124,58,237,var(--tw-text-opacity))}:is(.dark .hover\:\~text-violet-600):hover{--tw-text-opacity:1;color:rgba(196,181,253,var(--tw-text-opacity))}.\~text-emerald-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.dark .\~text-emerald-600{--tw-text-opacity:1;color:rgba(52,211,153,var(--tw-text-opacity))}.\~text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.dark .\~text-red-600{--tw-text-opacity:1;color:rgba(248,113,113,var(--tw-text-opacity))}.\~text-orange-600{--tw-text-opacity:1;color:rgba(234,88,12,var(--tw-text-opacity))}.dark .\~text-orange-600{--tw-text-opacity:1;color:rgba(251,146,60,var(--tw-text-opacity))}.\~text-gray-700{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.dark .\~text-gray-700{--tw-text-opacity:1;color:rgba(209,213,219,var(--tw-text-opacity))}.\~text-indigo-700{--tw-text-opacity:1;color:rgba(67,56,202,var(--tw-text-opacity))}.dark .\~text-indigo-700{--tw-text-opacity:1;color:rgba(199,210,254,var(--tw-text-opacity))}.\~text-blue-700{--tw-text-opacity:1;color:rgba(29,78,216,var(--tw-text-opacity))}.dark .\~text-blue-700{--tw-text-opacity:1;color:rgba(191,219,254,var(--tw-text-opacity))}.\~text-violet-700{--tw-text-opacity:1;color:rgba(109,40,217,var(--tw-text-opacity))}.dark .\~text-violet-700{--tw-text-opacity:1;color:rgba(221,214,254,var(--tw-text-opacity))}.\~text-emerald-700{--tw-text-opacity:1;color:rgba(4,120,87,var(--tw-text-opacity))}.dark .\~text-emerald-700{--tw-text-opacity:1;color:rgba(167,243,208,var(--tw-text-opacity))}.\~text-red-700{--tw-text-opacity:1;color:rgba(185,28,28,var(--tw-text-opacity))}.dark .\~text-red-700{--tw-text-opacity:1;color:rgba(254,202,202,var(--tw-text-opacity))}.\~text-orange-700{--tw-text-opacity:1;color:rgba(194,65,12,var(--tw-text-opacity))}.dark .\~text-orange-700{--tw-text-opacity:1;color:rgba(254,215,170,var(--tw-text-opacity))}.\~text-gray-800{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.dark .\~text-gray-800{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity))}.\~bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.dark .\~bg-white{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.\~bg-body{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark .\~bg-body{--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}.\~bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.dark .\~bg-gray-100{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.\~bg-gray-200\/50{background-color:rgba(229,231,235,.5)}.dark .\~bg-gray-200\/50{background-color:rgba(55,65,81,.1)}.\~bg-gray-500\/5{background-color:rgba(107,114,128,.05)}.dark .\~bg-gray-500\/5{background-color:rgba(0,0,0,.1)}.hover\:\~bg-gray-500\/5:hover{background-color:rgba(107,114,128,.05)}.dark .hover\:\~bg-gray-500\/5:hover{background-color:rgba(17,24,39,.2)}.\~bg-gray-500\/10{background-color:rgba(107,114,128,.1)}.dark .\~bg-gray-500\/10{background-color:rgba(17,24,39,.4)}.\~bg-red-500\/10{background-color:rgba(239,68,68,.1)}.dark .\~bg-red-500\/10{background-color:rgba(239,68,68,.2)}.hover\:\~bg-red-500\/10:hover{background-color:rgba(239,68,68,.1)}.\~bg-red-500\/20,.dark .hover\:\~bg-red-500\/10:hover{background-color:rgba(239,68,68,.2)}.dark .\~bg-red-500\/20{background-color:rgba(239,68,68,.4)}.\~bg-red-500\/30{background-color:rgba(239,68,68,.3)}.dark .\~bg-red-500\/30{background-color:rgba(239,68,68,.6)}.\~bg-dropdown{--tw-bg-opacity:1!important;background-color:rgba(255,255,255,var(--tw-bg-opacity))!important}.dark .\~bg-dropdown{--tw-bg-opacity:1!important;background-color:rgba(55,65,81,var(--tw-bg-opacity))!important}.\~border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.dark .\~border-gray-200{border-color:rgba(107,114,128,.2)}.\~border-b-dropdown{--tw-border-opacity:1!important;border-bottom-color:rgba(255,255,255,var(--tw-border-opacity))!important}.dark .\~border-b-dropdown{--tw-border-opacity:1!important;border-bottom-color:rgba(55,65,81,var(--tw-border-opacity))!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{right:0;left:0}.inset-0,.inset-y-0{top:0;bottom:0}.-bottom-3{bottom:-.75rem}.-right-3{right:-.75rem}.-top-3{top:-.75rem}.-top-\[\.1rem\]{top:-.1rem}.left-0{left:0}.left-0\.5{left:.125rem}.left-1\/2{left:50%}.left-10{left:2.5rem}.left-4{left:1rem}.right-0{right:0}.right-1\/2{right:50%}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-10{top:2.5rem}.top-2{top:.5rem}.top-2\.5{top:.625rem}.top-3{top:.75rem}.top-\[7\.5rem\]{top:7.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.col-span-2{grid-column:span 2/span 2}.-my-5{margin-top:-1.25rem;margin-bottom:-1.25rem}.-my-px{margin-top:-1px;margin-bottom:-1px}.mx-0{margin-left:0;margin-right:0}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-2{margin-bottom:-.5rem}.-ml-1{margin-left:-.25rem}.-ml-3{margin-left:-.75rem}.-ml-6{margin-left:-1.5rem}.-mr-3{margin-right:-.75rem}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-0{margin-right:0}.mr-0\.5{margin-right:.125rem}.mr-1{margin-right:.25rem}.mr-1\.5{margin-right:.375rem}.mr-10{margin-right:2.5rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mr-px{margin-right:1px}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-\[-4px\]{margin-top:-4px}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.line-clamp-none{overflow:visible;display:block;-webkit-box-orient:horizontal;-webkit-line-clamp:none}.block{display:block}.inline-block{display:inline-block}.\!inline{display:inline!important}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-0{height:0}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[\.9rem\]{height:.9rem}.h-\[4px\]{height:4px}.h-full{height:100%}.max-h-32{max-height:8rem}.max-h-\[33vh\]{max-height:33vh}.w-0{width:0}.w-2{width:.5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[2rem\]{width:2rem}.w-\[8rem\]{width:8rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[1rem\]{min-width:1rem}.min-w-\[2rem\]{min-width:2rem}.min-w-\[8rem\]{min-width:8rem}.max-w-4xl{max-width:56rem}.max-w-max{max-width:-webkit-max-content;max-width:-moz-max-content;max-width:max-content}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.origin-bottom{transform-origin:bottom}.origin-top-right{transform-origin:top right}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-6{--tw-translate-x:1.5rem}.translate-x-6,.translate-x-8{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-8{--tw-translate-x:2rem}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.translate-y-10{--tw-translate-y:2.5rem}.translate-y-10,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.-rotate-180{--tw-rotate:-180deg}.-rotate-90,.-rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate:-90deg}.rotate-180{--tw-rotate:180deg}.rotate-90,.rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate:90deg}.scale-90{--tw-scale-x:.9;--tw-scale-y:.9}.scale-90,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-col{grid-auto-flow:column}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-px{gap:1px}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-2{row-gap:.5rem}.space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1px*var(--tw-space-x-reverse));margin-left:calc(1px*(1 - var(--tw-space-x-reverse)))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-hidden{overflow-y:hidden}.overflow-x-scroll{overflow-x:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-l-full{border-top-left-radius:9999px;border-bottom-left-radius:9999px}.rounded-r-full{border-top-right-radius:9999px;border-bottom-right-radius:9999px}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.rounded-bl-lg{border-bottom-left-radius:.5rem}.rounded-br-lg{border-bottom-right-radius:.5rem}.border{border-width:1px}.border-\[10px\]{border-width:10px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0}.border-emerald-500\/25{border-color:rgba(16,185,129,.25)}.border-emerald-500\/50{border-color:rgba(16,185,129,.5)}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-500\/50{border-color:rgba(107,114,128,.5)}.border-gray-800\/20{border-color:rgba(31,41,55,.2)}.border-indigo-500\/50{border-color:rgba(99,102,241,.5)}.border-orange-500\/50{border-color:rgba(249,115,22,.5)}.border-red-500\/25{border-color:rgba(239,68,68,.25)}.border-red-500\/50{border-color:rgba(239,68,68,.5)}.border-transparent{border-color:transparent}.border-violet-500\/25{border-color:rgba(139,92,246,.25)}.border-violet-600\/50{border-color:rgba(124,58,237,.5)}.bg-emerald-300{--tw-bg-opacity:1;background-color:rgba(110,231,183,var(--tw-bg-opacity))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgba(16,185,129,var(--tw-bg-opacity))}.bg-emerald-500\/5{background-color:rgba(16,185,129,.05)}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgba(5,150,105,var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-25{--tw-bg-opacity:1;background-color:rgba(252,252,253,var(--tw-bg-opacity))}.bg-gray-300\/50{background-color:rgba(209,213,219,.5)}.bg-gray-500\/5{background-color:rgba(107,114,128,.05)}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99,102,241,var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgba(79,70,229,var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgba(254,202,202,var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254,242,242,var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgba(239,68,68,var(--tw-bg-opacity))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/30{background-color:rgba(239,68,68,.3)}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-red-800\/5{background-color:rgba(153,27,27,.05)}.bg-violet-500{--tw-bg-opacity:1;background-color:rgba(139,92,246,var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(254,252,232,var(--tw-bg-opacity))}.bg-opacity-20{--tw-bg-opacity:0.2}.bg-dots-darker{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='30' height='30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.227 0c.687 0 1.227.54 1.227 1.227s-.54 1.227-1.227 1.227S0 1.914 0 1.227.54 0 1.227 0z' fill='rgba(0,0,0,0.07)'/%3E%3C/svg%3E")}.bg-gradient-to-bl{background-image:linear-gradient(to bottom left,var(--tw-gradient-stops))}.from-gray-700\/50{--tw-gradient-from:rgba(55,65,81,0.5) var(--tw-gradient-from-position);--tw-gradient-to:rgba(55,65,81,0) var(--tw-gradient-to-position)}.from-gray-700\/50,.from-white{--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white{--tw-gradient-from:#fff var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position)}.via-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),transparent var(--tw-gradient-via-position),var(--tw-gradient-to)}.bg-center{background-position:50%}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-1\.5{padding-bottom:.375rem}.pb-10{padding-bottom:2.5rem}.pb-16{padding-bottom:4rem}.pl-4{padding-left:1rem}.pl-6{padding-left:1.5rem}.pl-px{padding-left:1px}.pr-10{padding-right:2.5rem}.pr-12{padding-right:3rem}.pr-8{padding-right:2rem}.pt-10{padding-top:2.5rem}.pt-2{padding-top:.5rem}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[8px\]{font-size:8px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-loose{line-height:2}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-emerald-500{--tw-text-opacity:1;color:rgba(16,185,129,var(--tw-text-opacity))}.text-emerald-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.text-emerald-700{--tw-text-opacity:1;color:rgba(4,120,87,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgba(21,128,61,var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity:1;color:rgba(224,231,255,var(--tw-text-opacity))}.text-indigo-500{--tw-text-opacity:1;color:rgba(99,102,241,var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.text-orange-600{--tw-text-opacity:1;color:rgba(234,88,12,var(--tw-text-opacity))}.text-red-100{--tw-text-opacity:1;color:rgba(254,226,226,var(--tw-text-opacity))}.text-red-50{--tw-text-opacity:1;color:rgba(254,242,242,var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgba(185,28,28,var(--tw-text-opacity))}.text-violet-500{--tw-text-opacity:1;color:rgba(139,92,246,var(--tw-text-opacity))}.text-violet-600{--tw-text-opacity:1;color:rgba(124,58,237,var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(234,179,8,var(--tw-text-opacity))}.text-opacity-75{--tw-text-opacity:0.75}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px -1px rgba(0,0,0,0.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,0.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,0.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-lg{box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-gray-500\/20{--tw-shadow-color:rgba(107,114,128,0.2);--tw-shadow:var(--tw-shadow-colored)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-animation{transition-property:transform,box-shadow,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-100{transition-delay:.1s}.duration-100{transition-duration:.1s}.duration-1000{transition-duration:1s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\@container{container-type:inline-size}.first-letter\:uppercase:first-letter{text-transform:uppercase}.hover\:text-emerald-700:hover{--tw-text-opacity:1;color:rgba(4,120,87,var(--tw-text-opacity))}.hover\:text-emerald-800:hover{--tw-text-opacity:1;color:rgba(6,95,70,var(--tw-text-opacity))}.hover\:text-indigo-500:hover{--tw-text-opacity:1;color:rgba(99,102,241,var(--tw-text-opacity))}.hover\:text-purple-500:hover{--tw-text-opacity:1;color:rgba(168,85,247,var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}.hover\:text-red-800:hover{--tw-text-opacity:1;color:rgba(153,27,27,var(--tw-text-opacity))}.hover\:text-violet-500:hover{--tw-text-opacity:1;color:rgba(139,92,246,var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.hover\:underline:hover{-webkit-text-decoration-line:underline;text-decoration-line:underline}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-md:hover{box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:shadow-inner:active{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,0.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.active\:shadow-inner:active,.active\:shadow-sm:active{box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.active\:shadow-sm:active{--tw-shadow:0 1px 2px 0 rgba(0,0,0,0.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.group:hover .group-hover\:scale-100{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-amber-300{--tw-text-opacity:1;color:rgba(252,211,77,var(--tw-text-opacity))}.group:hover .group-hover\:text-amber-400{--tw-text-opacity:1;color:rgba(251,191,36,var(--tw-text-opacity))}.group:hover .group-hover\:text-indigo-500{--tw-text-opacity:1;color:rgba(99,102,241,var(--tw-text-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}.group:hover .group-hover\:opacity-50{opacity:.5}.peer:checked~.peer-checked\:translate-x-2{--tw-translate-x:0.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:checked~.peer-checked\:bg-emerald-300{--tw-bg-opacity:1;background-color:rgba(110,231,183,var(--tw-bg-opacity))}@container (min-width: 32rem){.\@lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}}@container (min-width: 42rem){.\@2xl\:block{display:block}}@container (min-width: 56rem){.\@4xl\:absolute{position:absolute}.\@4xl\:col-span-2{grid-column:span 2/span 2}.\@4xl\:mr-20{margin-right:5rem}.\@4xl\:flex{display:flex}.\@4xl\:max-h-\[none\]{max-height:none}.\@4xl\:grid-cols-\[33\.33\%_66\.66\%\]{grid-template-columns:33.33% 66.66%}.\@4xl\:grid-rows-\[57rem\]{grid-template-rows:57rem}.\@4xl\:rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.\@4xl\:rounded-bl-none{border-bottom-left-radius:0}.\@4xl\:border-t-0{border-top-width:0}}.dark .dark\:bg-black\/10{background-color:rgba(0,0,0,.1)}.dark .dark\:bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.dark .dark\:bg-red-500\/10{background-color:rgba(239,68,68,.1)}.dark .dark\:bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.dark .dark\:bg-dots-lighter{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='30' height='30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.227 0c.687 0 1.227.54 1.227 1.227s-.54 1.227-1.227 1.227S0 1.914 0 1.227.54 0 1.227 0z' fill='rgba(255,255,255,0.07)'/%3E%3C/svg%3E")}.dark .dark\:bg-gradient-to-bl{background-image:linear-gradient(to bottom left,var(--tw-gradient-stops))}.dark .dark\:from-gray-700\/50{--tw-gradient-from:rgba(55,65,81,0.5) var(--tw-gradient-from-position);--tw-gradient-to:rgba(55,65,81,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark .dark\:shadow-none{--tw-shadow:0 0 transparent;--tw-shadow-colored:0 0 transparent;box-shadow:0 0 transparent,0 0 transparent,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.dark .dark\:ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 transparent;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}.dark .dark\:ring-inset{--tw-ring-inset:inset}.dark .dark\:ring-white\/5{--tw-ring-color:hsla(0,0%,100%,0.05)}@media (min-width:640px){.sm\:-ml-5{margin-left:-1.25rem}.sm\:-mr-5{margin-right:-1.25rem}.sm\:inline-flex{display:inline-flex}.sm\:px-10{padding-left:2.5rem;padding-right:2.5rem}.sm\:px-5{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.lg\:flex{display:flex}.lg\:w-1\/3{width:33.333333%}.lg\:w-2\/5{width:40%}.lg\:max-w-\[90rem\]{max-width:90rem}.lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}} --------------------------------------------------------------------------------