diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..88dec24
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,19 @@
+{
+ "tabWidth": 4,
+ "useTabs": false,
+ "singleQuote": false,
+ "semi": true,
+ "trailingComma": "es5",
+ "bracketSpacing": true,
+ "bracketSameLine": false,
+ "arrowParens": "always",
+ "endOfLine": "lf",
+ "printWidth": 80,
+ "proseWrap": "preserve",
+ "quoteProps": "as-needed",
+ "requirePragma": false,
+ "embeddedLanguageFormatting": "auto",
+ "vueIndentScriptAndStyle": false,
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false
+}
\ No newline at end of file
diff --git a/IDEAS.md b/IDEAS.md
new file mode 100644
index 0000000..822a351
--- /dev/null
+++ b/IDEAS.md
@@ -0,0 +1,6 @@
+# random things I kinda wanna do
+
+- Quick links (eg tiny buttons on the bottom right of the main section)
+- add the ability to change cards links
+
+TODO: fixup readme
diff --git a/README.md b/README.md
index 128c949..39946e3 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,16 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
### Prerequisites
-- [ZQDGR](https://github.com/juls0730/zqdgr)
-- [Go](https://go.dev/doc/install)
-- [sqlite3](https://www.sqlite.org/download.html)
-- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest)
+- [ZQDGR](https://github.com/juls0730/zqdgr)
+- [Go](https://go.dev/doc/install)
+- [sqlite3](https://www.sqlite.org/download.html)
+- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest)
## Usage
### Docker
-Passport is available as a Docker image via this repository. This is the recommended way to run Passport.
+Passport is available as a Docker image via this repository for both amd64 and arm64. This is the recommended way to run Passport.
```bash
docker run -d --name passport -p 3000:3000 -v passport_data:/data -e PASSPORT_ADMIN_USERNAME=admin -e PASSPORT_ADMIN_PASSWORD=password ghcr.io/juls0730/passport:latest
@@ -35,9 +35,11 @@ If you want to build from source, you will need to install the dependencies firs
# note entirely necessary, but strongly recommended
go install github.com/juls0730/zqdgr@latest
-curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.13/tailwindcss-linux-x64
-chmod +x tailwindcss-linux-x64
-mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
+# install bun
+curl -fsSL https://bun.sh/install | bash
+
+# install css build deps
+bun install
# you may also have to install sqlite3...
```
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..e620136
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,512 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "passport-css-compiler",
+ "devDependencies": {
+ "@fullhuman/postcss-purgecss": "^7.0.2",
+ "cssnano": "^7.1.1",
+ "postcss": "^8.4.35",
+ "postcss-cli": "^11.0.0",
+ "postcss-preset-env": "^10.4.0",
+ },
+ },
+ },
+ "packages": {
+ "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@2.0.5", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+
+ "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@4.0.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ=="],
+
+ "@csstools/postcss-alpha-function": ["@csstools/postcss-alpha-function@1.0.1", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w=="],
+
+ "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@5.0.2", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg=="],
+
+ "@csstools/postcss-color-function": ["@csstools/postcss-color-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA=="],
+
+ "@csstools/postcss-color-function-display-p3-linear": ["@csstools/postcss-color-function-display-p3-linear@1.0.1", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg=="],
+
+ "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@3.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag=="],
+
+ "@csstools/postcss-color-mix-variadic-function-arguments": ["@csstools/postcss-color-mix-variadic-function-arguments@1.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ=="],
+
+ "@csstools/postcss-content-alt-text": ["@csstools/postcss-content-alt-text@2.0.8", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q=="],
+
+ "@csstools/postcss-contrast-color-function": ["@csstools/postcss-contrast-color-function@2.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA=="],
+
+ "@csstools/postcss-exponential-functions": ["@csstools/postcss-exponential-functions@2.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw=="],
+
+ "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@4.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw=="],
+
+ "@csstools/postcss-gamut-mapping": ["@csstools/postcss-gamut-mapping@2.0.11", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw=="],
+
+ "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@5.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow=="],
+
+ "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA=="],
+
+ "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@4.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg=="],
+
+ "@csstools/postcss-initial": ["@csstools/postcss-initial@2.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg=="],
+
+ "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@5.0.3", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ=="],
+
+ "@csstools/postcss-light-dark-function": ["@csstools/postcss-light-dark-function@2.0.11", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA=="],
+
+ "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ=="],
+
+ "@csstools/postcss-logical-overflow": ["@csstools/postcss-logical-overflow@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA=="],
+
+ "@csstools/postcss-logical-overscroll-behavior": ["@csstools/postcss-logical-overscroll-behavior@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w=="],
+
+ "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@3.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg=="],
+
+ "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@3.0.4", "", { "dependencies": { "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ=="],
+
+ "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@2.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig=="],
+
+ "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg=="],
+
+ "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@4.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A=="],
+
+ "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@4.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q=="],
+
+ "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg=="],
+
+ "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@4.2.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw=="],
+
+ "@csstools/postcss-random-function": ["@csstools/postcss-random-function@2.0.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w=="],
+
+ "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@3.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw=="],
+
+ "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@4.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q=="],
+
+ "@csstools/postcss-sign-functions": ["@csstools/postcss-sign-functions@1.1.4", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg=="],
+
+ "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@4.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA=="],
+
+ "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@4.0.3", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA=="],
+
+ "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@4.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A=="],
+
+ "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@4.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA=="],
+
+ "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@3.1.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g=="],
+
+ "@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="],
+
+ "@csstools/utilities": ["@csstools/utilities@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ=="],
+
+ "@fullhuman/postcss-purgecss": ["@fullhuman/postcss-purgecss@7.0.2", "", { "dependencies": { "purgecss": "^7.0.2" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ=="],
+
+ "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
+
+ "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
+
+ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
+ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA=="],
+
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+
+ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="],
+
+ "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001746", "", {}, "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA=="],
+
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
+
+ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "css-blank-pseudo": ["css-blank-pseudo@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag=="],
+
+ "css-declaration-sorter": ["css-declaration-sorter@7.3.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ=="],
+
+ "css-has-pseudo": ["css-has-pseudo@7.0.3", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA=="],
+
+ "css-prefers-color-scheme": ["css-prefers-color-scheme@10.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ=="],
+
+ "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
+
+ "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
+
+ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
+
+ "cssdb": ["cssdb@8.4.2", "", {}, "sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "cssnano": ["cssnano@7.1.1", "", { "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ=="],
+
+ "cssnano-preset-default": ["cssnano-preset-default@7.0.9", "", { "dependencies": { "browserslist": "^4.25.1", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", "postcss-colormin": "^7.0.4", "postcss-convert-values": "^7.0.7", "postcss-discard-comments": "^7.0.4", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", "postcss-merge-rules": "^7.0.6", "postcss-minify-font-values": "^7.0.1", "postcss-minify-gradients": "^7.0.1", "postcss-minify-params": "^7.0.4", "postcss-minify-selectors": "^7.0.5", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", "postcss-normalize-positions": "^7.0.1", "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", "postcss-normalize-unicode": "^7.0.4", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", "postcss-reduce-initial": "^7.0.4", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.1.0", "postcss-unique-selectors": "^7.0.4" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA=="],
+
+ "cssnano-utils": ["cssnano-utils@5.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg=="],
+
+ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
+
+ "dependency-graph": ["dependency-graph@1.0.0", "", {}, "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg=="],
+
+ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.229", "", {}, "sha512-cwhDcZKGcT/rEthLRJ9eBlMDkh1sorgsuk+6dpsehV0g9CABsIqBxU4rLRjG+d/U6pYU1s37A4lSKrVc5lSQYg=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
+
+ "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
+
+ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
+
+ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
+
+ "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
+
+ "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
+
+ "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
+
+ "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
+
+ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
+
+ "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
+
+ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
+
+ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
+
+ "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
+
+ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw=="],
+
+ "postcss-calc": ["postcss-calc@10.1.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.38" } }, "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw=="],
+
+ "postcss-clamp": ["postcss-clamp@4.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.6" } }, "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow=="],
+
+ "postcss-cli": ["postcss-cli@11.0.1", "", { "dependencies": { "chokidar": "^3.3.0", "dependency-graph": "^1.0.0", "fs-extra": "^11.0.0", "picocolors": "^1.0.0", "postcss-load-config": "^5.0.0", "postcss-reporter": "^7.0.0", "pretty-hrtime": "^1.0.3", "read-cache": "^1.0.0", "slash": "^5.0.0", "tinyglobby": "^0.2.12", "yargs": "^17.0.0" }, "peerDependencies": { "postcss": "^8.0.0" }, "bin": { "postcss": "index.js" } }, "sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g=="],
+
+ "postcss-color-functional-notation": ["postcss-color-functional-notation@7.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw=="],
+
+ "postcss-color-hex-alpha": ["postcss-color-hex-alpha@10.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w=="],
+
+ "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@10.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ=="],
+
+ "postcss-colormin": ["postcss-colormin@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw=="],
+
+ "postcss-convert-values": ["postcss-convert-values@7.0.7", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A=="],
+
+ "postcss-custom-media": ["postcss-custom-media@11.0.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw=="],
+
+ "postcss-custom-properties": ["postcss-custom-properties@14.0.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ=="],
+
+ "postcss-custom-selectors": ["postcss-custom-selectors@8.0.5", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg=="],
+
+ "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@9.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA=="],
+
+ "postcss-discard-comments": ["postcss-discard-comments@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg=="],
+
+ "postcss-discard-duplicates": ["postcss-discard-duplicates@7.0.2", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w=="],
+
+ "postcss-discard-empty": ["postcss-discard-empty@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg=="],
+
+ "postcss-discard-overridden": ["postcss-discard-overridden@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg=="],
+
+ "postcss-double-position-gradients": ["postcss-double-position-gradients@6.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g=="],
+
+ "postcss-focus-visible": ["postcss-focus-visible@10.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA=="],
+
+ "postcss-focus-within": ["postcss-focus-within@9.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw=="],
+
+ "postcss-font-variant": ["postcss-font-variant@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA=="],
+
+ "postcss-gap-properties": ["postcss-gap-properties@6.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw=="],
+
+ "postcss-image-set-function": ["postcss-image-set-function@7.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA=="],
+
+ "postcss-lab-function": ["postcss-lab-function@7.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w=="],
+
+ "postcss-load-config": ["postcss-load-config@5.1.0", "", { "dependencies": { "lilconfig": "^3.1.1", "yaml": "^2.4.2" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1" }, "optionalPeers": ["jiti", "postcss", "tsx"] }, "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA=="],
+
+ "postcss-logical": ["postcss-logical@8.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA=="],
+
+ "postcss-merge-longhand": ["postcss-merge-longhand@7.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^7.0.5" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw=="],
+
+ "postcss-merge-rules": ["postcss-merge-rules@7.0.6", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ=="],
+
+ "postcss-minify-font-values": ["postcss-minify-font-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ=="],
+
+ "postcss-minify-gradients": ["postcss-minify-gradients@7.0.1", "", { "dependencies": { "colord": "^2.9.3", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A=="],
+
+ "postcss-minify-params": ["postcss-minify-params@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw=="],
+
+ "postcss-minify-selectors": ["postcss-minify-selectors@7.0.5", "", { "dependencies": { "cssesc": "^3.0.0", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug=="],
+
+ "postcss-nesting": ["postcss-nesting@13.0.2", "", { "dependencies": { "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ=="],
+
+ "postcss-normalize-charset": ["postcss-normalize-charset@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ=="],
+
+ "postcss-normalize-display-values": ["postcss-normalize-display-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ=="],
+
+ "postcss-normalize-positions": ["postcss-normalize-positions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ=="],
+
+ "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ=="],
+
+ "postcss-normalize-string": ["postcss-normalize-string@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ=="],
+
+ "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg=="],
+
+ "postcss-normalize-unicode": ["postcss-normalize-unicode@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g=="],
+
+ "postcss-normalize-url": ["postcss-normalize-url@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ=="],
+
+ "postcss-normalize-whitespace": ["postcss-normalize-whitespace@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA=="],
+
+ "postcss-opacity-percentage": ["postcss-opacity-percentage@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ=="],
+
+ "postcss-ordered-values": ["postcss-ordered-values@7.0.2", "", { "dependencies": { "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw=="],
+
+ "postcss-overflow-shorthand": ["postcss-overflow-shorthand@6.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q=="],
+
+ "postcss-page-break": ["postcss-page-break@3.0.4", "", { "peerDependencies": { "postcss": "^8" } }, "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ=="],
+
+ "postcss-place": ["postcss-place@10.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw=="],
+
+ "postcss-preset-env": ["postcss-preset-env@10.4.0", "", { "dependencies": { "@csstools/postcss-alpha-function": "^1.0.1", "@csstools/postcss-cascade-layers": "^5.0.2", "@csstools/postcss-color-function": "^4.0.12", "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", "@csstools/postcss-color-mix-function": "^3.0.12", "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", "@csstools/postcss-content-alt-text": "^2.0.8", "@csstools/postcss-contrast-color-function": "^2.0.12", "@csstools/postcss-exponential-functions": "^2.0.9", "@csstools/postcss-font-format-keywords": "^4.0.0", "@csstools/postcss-gamut-mapping": "^2.0.11", "@csstools/postcss-gradients-interpolation-method": "^5.0.12", "@csstools/postcss-hwb-function": "^4.0.12", "@csstools/postcss-ic-unit": "^4.0.4", "@csstools/postcss-initial": "^2.0.1", "@csstools/postcss-is-pseudo-class": "^5.0.3", "@csstools/postcss-light-dark-function": "^2.0.11", "@csstools/postcss-logical-float-and-clear": "^3.0.0", "@csstools/postcss-logical-overflow": "^2.0.0", "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", "@csstools/postcss-logical-resize": "^3.0.0", "@csstools/postcss-logical-viewport-units": "^3.0.4", "@csstools/postcss-media-minmax": "^2.0.9", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", "@csstools/postcss-nested-calc": "^4.0.0", "@csstools/postcss-normalize-display-values": "^4.0.0", "@csstools/postcss-oklab-function": "^4.0.12", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/postcss-random-function": "^2.0.1", "@csstools/postcss-relative-color-syntax": "^3.0.12", "@csstools/postcss-scope-pseudo-class": "^4.0.1", "@csstools/postcss-sign-functions": "^1.1.4", "@csstools/postcss-stepped-value-functions": "^4.0.9", "@csstools/postcss-text-decoration-shorthand": "^4.0.3", "@csstools/postcss-trigonometric-functions": "^4.0.9", "@csstools/postcss-unset-value": "^4.0.0", "autoprefixer": "^10.4.21", "browserslist": "^4.26.0", "css-blank-pseudo": "^7.0.1", "css-has-pseudo": "^7.0.3", "css-prefers-color-scheme": "^10.0.0", "cssdb": "^8.4.2", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^7.0.12", "postcss-color-hex-alpha": "^10.0.0", "postcss-color-rebeccapurple": "^10.0.0", "postcss-custom-media": "^11.0.6", "postcss-custom-properties": "^14.0.6", "postcss-custom-selectors": "^8.0.5", "postcss-dir-pseudo-class": "^9.0.1", "postcss-double-position-gradients": "^6.0.4", "postcss-focus-visible": "^10.0.1", "postcss-focus-within": "^9.0.1", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^6.0.0", "postcss-image-set-function": "^7.0.0", "postcss-lab-function": "^7.0.12", "postcss-logical": "^8.1.0", "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^6.0.0", "postcss-page-break": "^3.0.4", "postcss-place": "^10.0.0", "postcss-pseudo-class-any-link": "^10.0.1", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^8.0.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw=="],
+
+ "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@10.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q=="],
+
+ "postcss-reduce-initial": ["postcss-reduce-initial@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q=="],
+
+ "postcss-reduce-transforms": ["postcss-reduce-transforms@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g=="],
+
+ "postcss-replace-overflow-wrap": ["postcss-replace-overflow-wrap@4.0.0", "", { "peerDependencies": { "postcss": "^8.0.3" } }, "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw=="],
+
+ "postcss-reporter": ["postcss-reporter@7.1.0", "", { "dependencies": { "picocolors": "^1.0.0", "thenby": "^1.3.4" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA=="],
+
+ "postcss-selector-not": ["postcss-selector-not@8.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
+
+ "postcss-svgo": ["postcss-svgo@7.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w=="],
+
+ "postcss-unique-selectors": ["postcss-unique-selectors@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ=="],
+
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
+ "pretty-hrtime": ["pretty-hrtime@1.0.3", "", {}, "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="],
+
+ "purgecss": ["purgecss@7.0.2", "", { "dependencies": { "commander": "^12.1.0", "glob": "^11.0.0", "postcss": "^8.4.47", "postcss-selector-parser": "^6.1.2" }, "bin": { "purgecss": "bin/purgecss.js" } }, "sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ=="],
+
+ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
+
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "stylehacks": ["stylehacks@7.0.6", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg=="],
+
+ "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
+
+ "thenby": ["thenby@1.3.4", "", {}, "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
+
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "@csstools/postcss-cascade-layers/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "@csstools/postcss-is-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "@csstools/postcss-scope-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "@csstools/selector-resolve-nested/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "@csstools/selector-specificity/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+
+ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+
+ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "css-blank-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "css-has-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
+
+ "postcss-attribute-case-insensitive/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-calc/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-custom-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-dir-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-discard-comments/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-focus-visible/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-focus-within/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-merge-rules/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-minify-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-pseudo-class-any-link/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-selector-not/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-unique-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
+
+ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
+ }
+}
diff --git a/go.mod b/go.mod
index 6f62166..9cceb57 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.25.0
require (
github.com/HugoSmits86/nativewebp v1.2.0
+ github.com/NarmadaWeb/gonify/v3 v3.0.0-beta
github.com/caarlos0/env/v11 v11.3.1
github.com/disintegration/imaging v1.6.2
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
@@ -19,6 +20,7 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
+ github.com/tdewolff/parse/v2 v2.8.3 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
@@ -44,6 +46,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/tdewolff/minify/v2 v2.24.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.66.0 // indirect
golang.org/x/sys v0.36.0 // indirect
diff --git a/go.sum b/go.sum
index 941ae82..deb3a4f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/HugoSmits86/nativewebp v1.2.0 h1:XJtXeTg7FsOi9VB1elQYZy3n6VjYLqofSr3gGRLUOp4=
github.com/HugoSmits86/nativewebp v1.2.0/go.mod h1:YNQuWenlVmSUUASVNhTDwf4d7FwYQGbGhklC8p72Vr8=
+github.com/NarmadaWeb/gonify/v3 v3.0.0-beta h1:tNj6Rq9S3UUnF2800h6Ns7wmx+q7MwoZBVD24fPCSlo=
+github.com/NarmadaWeb/gonify/v3 v3.0.0-beta/go.mod h1:AoLhZCGC/9XGqOE+0amArp/dFIZSfZSvbyPI/IbQ7Q0=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
@@ -64,6 +66,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tdewolff/minify/v2 v2.24.3 h1:BaKgWSFLKbKDiUskbeRgbe2n5d1Ci1x3cN/eXna8zOA=
+github.com/tdewolff/minify/v2 v2.24.3/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
+github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
+github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
+github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
+github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..86b5a72
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "passport-css-compiler",
+ "private": true,
+ "description": "A manifest to acquire CLI tools for passport. Not a Node app, only required for compiling CSS.",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "build": "postcss src/styles/*.css --dir src/assets/styles"
+ },
+ "devDependencies": {
+ "@fullhuman/postcss-purgecss": "^7.0.2",
+ "cssnano": "^7.1.1",
+ "postcss": "^8.4.35",
+ "postcss-cli": "^11.0.0",
+ "postcss-preset-env": "^10.4.0"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..ffaffd1
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,16 @@
+import { purgeCSSPlugin } from "@fullhuman/postcss-purgecss";
+import postcssPresetEnv from "postcss-preset-env";
+import cssnano from "cssnano";
+
+export default {
+ plugins: [
+ purgeCSSPlugin({
+ content: ["./src/**/*.hbs", "./src/**/*.js"],
+ }),
+ postcssPresetEnv({
+ browsers: "last 4 versions",
+ autoprefixer: {},
+ }),
+ cssnano,
+ ],
+};
diff --git a/src/assets/fonts/InstrumentSans-Regular.woff2 b/src/assets/fonts/InstrumentSans-Regular.woff2
new file mode 100644
index 0000000..2934f16
Binary files /dev/null and b/src/assets/fonts/InstrumentSans-Regular.woff2 differ
diff --git a/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.ttf b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.ttf
new file mode 100644
index 0000000..d26c8f0
Binary files /dev/null and b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.ttf differ
diff --git a/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.woff2 b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.woff2
new file mode 100644
index 0000000..b8b338e
Binary files /dev/null and b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght-subset.woff2 differ
diff --git a/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght.ttf b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..153c5ae
Binary files /dev/null and b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght.ttf differ
diff --git a/src/assets/foo.css b/src/assets/foo.css
new file mode 100644
index 0000000..efd579b
--- /dev/null
+++ b/src/assets/foo.css
@@ -0,0 +1,2 @@
+aaaaaaaaaa
+ssssssssssssss
\ No newline at end of file
diff --git a/src/main.go b/src/main.go
index 10d5a19..a4566a4 100644
--- a/src/main.go
+++ b/src/main.go
@@ -1,4 +1,4 @@
-//go:generate tailwindcss -i styles/main.scss -o assets/tailwind.css --minify
+//go:generate npm run build
package main
@@ -17,7 +17,6 @@ import (
"log/slog"
"mime/multipart"
"net/http"
- "net/url"
"os"
"os/signal"
"path/filepath"
@@ -27,10 +26,13 @@ import (
"time"
"github.com/HugoSmits86/nativewebp"
+ "github.com/NarmadaWeb/gonify/v3"
"github.com/caarlos0/env/v11"
"github.com/disintegration/imaging"
"github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/middleware/compress"
"github.com/gofiber/fiber/v3/middleware/helmet"
+ "github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/gofiber/template/handlebars/v2"
"github.com/google/uuid"
@@ -43,7 +45,7 @@ import (
_ "modernc.org/sqlite"
)
-//go:embed assets/** templates/** schema.sql
+//go:embed assets/** templates/** schema.sql scripts/**.js
var embeddedAssets embed.FS
var devContent = `", content)
+ case ".css":
+ return fmt.Sprintf("", content)
+ default:
+ return string(content)
+ }
})
engine.AddFunc("devContent", func() string {
@@ -717,10 +746,6 @@ func main() {
return ""
})
- engine.AddFunc("eq", func(a, b any) bool {
- return a == b
- })
-
router := fiber.New(fiber.Config{
Views: engine,
})
@@ -732,6 +757,19 @@ func main() {
return c.Redirect().To("/assets/favicon.ico")
})
+ router.Use(logger.New())
+
+ router.Use(compress.New(compress.Config{
+ Level: compress.LevelBestSpeed,
+ }))
+
+ router.Use(gonify.New(gonify.Config{
+ MinifySVG: !app.DevMode,
+ MinifyCSS: !app.DevMode,
+ MinifyJS: !app.DevMode,
+ MinifyHTML: !app.DevMode,
+ }))
+
router.Use("/", static.New("./public", static.Config{
Browse: false,
MaxAge: 31536000,
@@ -739,10 +777,13 @@ func main() {
router.Use("/assets", static.New("", static.Config{
FS: assetsDir,
+ Browse: false,
MaxAge: 31536000,
}))
router.Get("/", func(c fiber.Ctx) error {
+ c.Response().Header.Set("Link", "; rel=preload; as=font; type=font/woff2; crossorigin, ; rel=preload; as=style; type=text/css")
+
renderData := fiber.Map{
"SearchProviderURL": app.Config.SearchProvider.URL,
"SearchParam": app.Config.SearchProvider.Query,
@@ -822,7 +863,7 @@ func main() {
return c.Render("views/admin/index", fiber.Map{
"Categories": app.CategoryManager.GetCategories(),
- }, "layouts/main")
+ }, "layouts/admin")
})
api := router.Group("/api")
@@ -879,9 +920,7 @@ func main() {
})
}
- filename := fmt.Sprintf("%d_%s.svg", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
-
- iconPath, err := UploadFile(file, filename, contentType, c)
+ iconPath, err := UploadFile(file, contentType, c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Failed to upload file, please try again!",
@@ -974,9 +1013,7 @@ func main() {
})
}
- filename := fmt.Sprintf("%d_%s.webp", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
-
- iconPath, err := UploadFile(file, filename, contentType, c)
+ iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@@ -1068,9 +1105,7 @@ func main() {
oldIconPath := category.Icon
- filename := fmt.Sprintf("%d_%s.svg", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
-
- iconPath, err := UploadFile(file, filename, contentType, c)
+ iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@@ -1188,9 +1223,7 @@ func main() {
oldIconPath := link.Icon
- filename := fmt.Sprintf("%d_%s.webp", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
-
- iconPath, err := UploadFile(file, filename, contentType, c)
+ iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
diff --git a/src/scripts/admin.js b/src/scripts/admin.js
new file mode 100644
index 0000000..11fe07d
--- /dev/null
+++ b/src/scripts/admin.js
@@ -0,0 +1,1106 @@
+// idfk what this variable capitalization is, it's a mess
+let modalContainer = document.getElementById("modal-container");
+let modal = modalContainer.querySelector("div");
+let pageElement = document.getElementById("blur-target");
+let iconUploadInput = document.getElementById("icon-upload");
+let targetCategoryID = null;
+let activeModal = null;
+
+let teleportStorage = document.getElementById("teleport-storage");
+let confirmActions = document.getElementById("confirm-actions");
+let selectIconButton = document.getElementById("select-icon-button");
+
+document.addEventListener("DOMContentLoaded", () => {
+ modalContainer.classList.remove("hidden");
+ modalContainer.classList.add("flex");
+});
+
+/**
+ * Submits a form to the given URL
+ * @param {Event} event - The event that triggered the function
+ * @param {string} url - The URL to submit the form to
+ * @param {"category" | "link"} target - The target to close the modal for
+ * @returns {Promise}
+ */
+async function submitRequest(event, url, target) {
+ event.preventDefault();
+ let data = new FormData(event.target);
+
+ let res = await fetch(url, {
+ method: "POST",
+ body: data,
+ });
+
+ if (res.status === 201) {
+ closeModal(target);
+ document.getElementById(`${target}-form`).reset();
+ location.reload();
+ } else {
+ let json = await res.json();
+ document.getElementById(`${target}-message`).innerText = json.message;
+ }
+}
+
+/**
+ * Adds an event listener for the given from to error check after the first submit
+ * @param {"category" | "link"} form - The form to initialize
+ * @returns {void}
+ */
+function addErrorListener(form) {
+ document
+ .getElementById(`${form}-form`)
+ .querySelector("button")
+ .addEventListener("click", (event) => {
+ event.target.parentElement
+ .querySelectorAll("[required]")
+ .forEach((el) => {
+ el.classList.add("invalid:border-[#861024]!");
+ });
+ });
+}
+
+/**
+ * Currently editing link or category
+ * @typedef {Object} actionButtonObj
+ * @property {string} clickAction - The function to be called when this button is clicked
+ * @property {string} label - The label of the button
+ */
+
+/**
+ * Clones the edit actions template and returns it
+ * @param {[actionButtonObj, actionButtonObj]} primaryActions - The primary actions to clone
+ * @returns {HTMLElement} The cloned edit actions element
+ */
+function cloneEditActions(primaryActions) {
+ let editActions = document
+ .getElementById("template-edit-actions")
+ .cloneNode(true);
+ editActions.removeAttribute("id");
+ editActions.classList.remove("hidden");
+
+ let i = 0;
+ for (i = 0; i < primaryActions.length; i++) {
+ let actionButtonObj = primaryActions[i];
+
+ let actionButton = editActions.querySelector(
+ `div[data-primary-actions] button:nth-child(${i + 1})`
+ );
+ actionButton.setAttribute("onclick", actionButtonObj.clickAction);
+ actionButton.setAttribute("aria-label", actionButtonObj.label);
+ }
+
+ return editActions;
+}
+
+addErrorListener("link");
+document
+ .getElementById("link-form")
+ .addEventListener("submit", async (event) => {
+ event.preventDefault();
+ let data = new FormData(event.target);
+
+ let res = await fetch(`/api/category/${targetCategoryID}/link`, {
+ method: "POST",
+ body: data,
+ });
+
+ if (res.status === 201) {
+ let json = await res.json();
+
+ let category = document.getElementById(
+ `${targetCategoryID}_category`
+ );
+ let linkGrid = category.nextElementSibling;
+
+ let newLinkCard = document
+ .getElementById("template-link-card")
+ .cloneNode(true);
+
+ newLinkCard.classList.remove("hidden");
+ newLinkCard.classList.add("link-card", "admin", "relative");
+
+ let newLinkImgElement = newLinkCard.querySelector(
+ "div[data-img-container] img"
+ );
+
+ newLinkImgElement.src = await processFile(data.get("icon"));
+ newLinkImgElement.alt = data.get("name");
+
+ newLinkCard.querySelector("h3").textContent = data.get("name");
+ newLinkCard.querySelector("p").textContent =
+ data.get("description");
+
+ newLinkCard.setAttribute("id", `${json.link.id}_link`);
+
+ let editActions = cloneEditActions([
+ {
+ clickAction: "editLink(this)",
+ label: "Edit link",
+ },
+ {
+ clickAction: "deleteLink(this)",
+ label: "Delete link",
+ },
+ ]);
+
+ editActions.classList.add("absolute", "right-1", "top-1");
+
+ newLinkCard.appendChild(editActions);
+
+ // append the card as the second to last element
+ linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
+ closeModal("link");
+
+ // after the close animation plays
+ setTimeout(() => {
+ document.getElementById(`link-form`).reset();
+ }, 300);
+ } else {
+ let json = await res.json();
+ document.getElementById(`link-message`).innerText = json.message;
+ }
+ });
+
+addErrorListener("category");
+document
+ .getElementById("category-form")
+ .addEventListener("submit", async (event) => {
+ event.preventDefault();
+ let data = new FormData(event.target);
+
+ let res = await fetch(`/api/category`, {
+ method: "POST",
+ body: data,
+ });
+
+ if (res.status === 201) {
+ let json = await res.json();
+
+ let newCategory = document
+ .getElementById("template-category")
+ .cloneNode(true);
+
+ let linkGrid = newCategory.querySelector("div:nth-child(2)");
+ let categoryHeader = newCategory.querySelector(".category-header");
+ categoryHeader.setAttribute("id", `${json.category.id}_category`);
+ categoryHeader.querySelector("h2").textContent = json.category.name;
+
+ let editActions = cloneEditActions([
+ {
+ clickAction: "editCategory(this)",
+ label: "Edit category",
+ },
+ {
+ clickAction: "deleteCategory(this)",
+ label: "Delete category",
+ },
+ ]);
+
+ editActions.classList.add("pl-2");
+
+ categoryHeader.appendChild(editActions);
+
+ let categoryImg = categoryHeader.querySelector(".category-img");
+
+ categoryImg.querySelector("img").src = await processFile(
+ data.get("icon")
+ );
+
+ linkGrid
+ .querySelector("div")
+ .setAttribute(
+ "onclick",
+ `openModal('link', ${json.category.id})`
+ );
+
+ let addCategoryButton = document.getElementById(
+ "add-category-button"
+ );
+ addCategoryButton.parentElement.insertBefore(
+ categoryHeader,
+ addCategoryButton
+ );
+ addCategoryButton.parentElement.insertBefore(
+ linkGrid,
+ addCategoryButton
+ );
+
+ closeModal("category");
+
+ // after the close animation plays
+ setTimeout(() => {
+ document.getElementById(`category-form`).reset();
+ }, 300);
+ } else {
+ let json = await res.json();
+ document.getElementById(`category-message`).innerText =
+ json.message;
+ }
+ });
+
+// when the background is clicked, close the modal
+modalContainer.addEventListener("click", (event) => {
+ if (event.target === modalContainer) {
+ closeModal();
+ }
+});
+
+function selectIcon() {
+ iconUploadInput.click();
+}
+
+/**
+ * Processes a file and returns a data URL.
+ * @param {File} file The file to process.
+ * @returns {Promise} A promise that resolves to a data URL.
+ */
+async function processFile(file) {
+ let reader = new FileReader();
+ return new Promise((resolve) => {
+ if (file.type === "image/svg+xml") {
+ reader.addEventListener("load", async (event) => {
+ let svgString = event.target.result;
+
+ svgString = svgString.replaceAll(
+ "currentColor",
+ "oklch(87% 0.015 286)"
+ );
+
+ // turn svgString into a data URL
+ resolve(
+ "data:image/svg+xml;base64," +
+ btoa(unescape(encodeURIComponent(svgString)))
+ );
+ });
+
+ reader.readAsText(file);
+ } else {
+ // these should be jpg, png, or webp
+ // make a DataURL out of it
+ reader.addEventListener("load", async (event) => {
+ resolve(event.target.result);
+ });
+
+ reader.readAsDataURL(file);
+ }
+ });
+}
+
+let targetedImageElement = null;
+iconUploadInput.addEventListener("change", async (event) => {
+ let file = event.target.files[0];
+ if (file === null) {
+ return;
+ }
+
+ if (targetedImageElement === null) {
+ throw new Error(
+ "icon upload element was clicked, but no target image element was set"
+ );
+ }
+
+ let dataURL = await processFile(file);
+ targetedImageElement.src = dataURL;
+});
+
+function openModal(modalKind, categoryID) {
+ activeModal = modalKind;
+ targetCategoryID = categoryID;
+
+ pageElement.style.filter = "blur(20px)";
+ document.getElementById(modalKind + "-contents").classList.remove("hidden");
+
+ modalContainer.classList.add("is-visible");
+ modal.classList.add("is-visible");
+
+ if (document.getElementById(modalKind + "-form") !== null) {
+ document.getElementById(modalKind + "-form").reset();
+ }
+}
+
+function closeModal() {
+ pageElement.style.filter = "";
+
+ modalContainer.classList.remove("is-visible");
+ modal.classList.remove("is-visible");
+
+ setTimeout(() => {
+ document
+ .getElementById(activeModal + "-contents")
+ .classList.add("hidden");
+ activeModal = null;
+ }, 300);
+
+ if (document.getElementById(activeModal + "-form") !== null) {
+ document
+ .getElementById(activeModal + "-form")
+ .querySelectorAll("[required]")
+ .forEach((el) => {
+ el.classList.remove("invalid:border-[#861024]!");
+ });
+ }
+
+ targetCategoryID = null;
+}
+
+/**
+ * Currently editing link or category
+ * @typedef {Object} currentlyEditingObj
+ * @property {"link" | "category" | undefined} type - The type of the currently editing element
+ * @property {string | undefined} linkID - The ID of the link we are currently editing if we are editing a link
+ * @property {string | undefined} categoryID - The ID of the category we are currently editing, or that the link belongs to
+ * @property {string | undefined} originalText - The original text of the currently editing element
+ * @property {string | undefined} originalDescription - The original description of the currently editing element
+ * @property {string | undefined} icon - The original icon of the currently editing element
+ * @property {Function | undefined} cleanup - The cleanup function for the currently editing element
+ */
+
+/** @type {currentlyEditingObj} */
+let currentlyEditing = {};
+
+/**
+ * Teleports the upload overlay to the given image node
+ * @param {HTMLElement} element The node to teleport into the destination
+ * @param {HTMLElement} destination The image node to teleport the upload overlay into
+ * @returns {HTMLElement} A reference to the teleported element
+ */
+function teleportElement(element, destination) {
+ destination.appendChild(element);
+}
+
+function unteleportElement(element) {
+ teleportElement(element, teleportStorage);
+}
+
+function confirmEdit() {
+ if (currentlyEditing.cleanup !== undefined) {
+ // this function could be called via deleting something, which doesn't have a cleanup function
+ currentlyEditing.cleanup();
+ }
+
+ switch (currentlyEditing.type) {
+ case "link":
+ confirmLinkEdit();
+ break;
+ case "category":
+ confirmCategoryEdit();
+ break;
+ default:
+ console.error("Unknown currentlyEditing type");
+ break;
+ }
+}
+
+function cancelEdit() {
+ if (currentlyEditing.cleanup !== undefined) {
+ // this function could be called via deleting something, which doesn't have a cleanup function
+ currentlyEditing.cleanup();
+ }
+
+ switch (currentlyEditing.type) {
+ case "link":
+ cancelLinkEdit();
+ break;
+ case "category":
+ cancelCategoryEdit(currentlyEditing.originalText);
+ break;
+ default:
+ console.error("Unknown currentlyEditing type");
+ break;
+ }
+
+ currentlyEditing = {};
+}
+
+/**
+ * Edits the link with the given html element
+ * @param {HTMLElement} target The target element that was clicked
+ */
+function editLink(target) {
+ let startTime = performance.now();
+
+ // we do it in this dynamic way so that if we add a new link without refreshing the page, it still works
+ let linkEl = target.closest(".link-card");
+ let linkID = parseInt(linkEl.id);
+ let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
+
+ if (currentlyEditing.linkID !== undefined) {
+ // cancel the edit if it's already in progress
+ cancelEdit();
+ }
+
+ let linkImg = linkEl.querySelector("div[data-img-container] img");
+ let linkName = linkEl.querySelector("div[data-text-container] h3");
+ let linkDesc = linkEl.querySelector("div[data-text-container] p");
+ let editActions = linkEl.querySelector("[data-edit-actions]");
+
+ currentlyEditing = {
+ type: "link",
+ linkID: linkID,
+ categoryID: categoryID,
+ originalText: linkName.textContent,
+ originalDescription: linkDesc.textContent,
+ icon: linkImg.src,
+ };
+
+ if (!currentlyEditing.linkID || !currentlyEditing.categoryID) {
+ throw new Error("failed to find link ID or category ID");
+ }
+
+ iconUploadInput.accept = "image/*";
+ targetedImageElement = linkImg;
+
+ teleportElement(selectIconButton, linkImg.parentElement);
+ teleportElement(confirmActions, editActions);
+
+ editActions.querySelector("div[data-primary-actions]").style.display =
+ "none";
+
+ requestAnimationFrame(() => {
+ currentlyEditing.cleanup = replaceWithResizableTextarea([
+ { targetEl: linkName, fill: false },
+ { targetEl: linkDesc },
+ ]);
+ // by adding a delay, we dont block the UI
+ setTimeout(() => {
+ linkEl.querySelector("textarea").focus();
+ }, 0);
+ });
+}
+
+async function confirmLinkEdit() {
+ let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
+ let linkNameInput = linkEl.querySelector("textarea");
+ let linkDescInput = linkNameInput.nextElementSibling;
+
+ linkNameInput.value = linkNameInput.value.trim();
+ linkDescInput.value = linkDescInput.value.trim();
+ if (linkNameInput.value === "") {
+ return;
+ }
+
+ let formData = new FormData();
+ if (linkNameInput.value !== currentlyEditing.originalText) {
+ formData.append("name", linkNameInput.value);
+ }
+
+ if (linkDescInput.value !== currentlyEditing.originalDescription) {
+ formData.append("description", linkDescInput.value);
+ }
+
+ if (iconUploadInput.files.length > 0) {
+ formData.append("icon", iconUploadInput.files[0]);
+ }
+
+ // nothing to update
+ if (
+ formData.get("name") === null &&
+ formData.get("description") === null &&
+ formData.get("icon") === null
+ ) {
+ return;
+ }
+
+ let res = await fetch(
+ `/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
+ {
+ method: "PATCH",
+ body: formData,
+ }
+ );
+
+ if (res.status === 200) {
+ iconUploadInput.value = "";
+
+ currentlyEditing.icon = undefined;
+ cancelLinkEdit(linkNameInput.value, linkDescInput.value);
+ currentlyEditing = {};
+ } else {
+ console.error("Failed to edit category");
+ }
+}
+
+function cancelLinkEdit(
+ text = currentlyEditing.originalText,
+ description = currentlyEditing.originalDescription
+) {
+ let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
+ let linkInput = linkEl.querySelector("textarea");
+ let linkTextarea = linkInput.nextElementSibling;
+ let linkImg = linkEl.querySelector("div[data-img-container] img");
+ let editActions = linkEl.querySelector("[data-edit-actions]");
+
+ if (currentlyEditing.icon !== undefined) {
+ linkImg.src = currentlyEditing.icon;
+ }
+
+ editActions.querySelector("div[data-primary-actions]").style.display = "";
+
+ // teleport the teleported elements back to the body for literally safe keeping
+ unteleportElement(selectIconButton);
+ unteleportElement(confirmActions);
+
+ restoreElementFromInput(linkInput, text);
+ restoreElementFromInput(linkTextarea, description);
+
+ currentlyEditing = {};
+ targetedImageElement = null;
+}
+
+/**
+ * Deletes the link with the given html element
+ * @param {HTMLElement} target The target element that was clicked
+ */
+function deleteLink(target) {
+ // we do it in this dynamic way so that if we add a new link without refreshing the page, it still works
+ let linkEl = target.closest(".link-card");
+ let linkID = parseInt(linkEl.id);
+ let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
+
+ if (currentlyEditing.linkID !== undefined) {
+ // cancel the edit if it's already in progress
+ cancelEdit();
+ }
+
+ currentlyEditing.linkID = linkID;
+ currentlyEditing.categoryID = categoryID;
+
+ let linkNameSpan = document.getElementById("link-name");
+ linkNameSpan.textContent = linkEl.querySelector("h3").textContent;
+
+ openModal("link-delete");
+}
+
+async function confirmDeleteLink() {
+ let res = await fetch(
+ `/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
+ {
+ method: "DELETE",
+ }
+ );
+
+ if (res.status === 200) {
+ let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
+ linkEl.remove();
+
+ closeModal();
+ currentlyEditing = {};
+ }
+}
+
+/**
+ * Edits the category with the given html element
+ * @param {HTMLElement} target The target element that was clicked
+ */
+function editCategory(target) {
+ let categoryEl = target.closest(".category-header");
+ let categoryID = parseInt(categoryEl.id);
+
+ if (currentlyEditing.linkID !== undefined) {
+ // cancel the edit if it's already in progress
+ cancelEdit();
+ }
+
+ let categoryName = categoryEl.querySelector("h2");
+ let categoryIcon = categoryEl.querySelector("div[data-img-container] img");
+ let editActions = categoryEl.querySelector("[data-edit-actions]");
+
+ currentlyEditing = {
+ type: "category",
+ categoryID: categoryID,
+ originalText: categoryName.textContent,
+ icon: categoryIcon.src,
+ };
+
+ if (!currentlyEditing.categoryID) {
+ throw new Error("failed to find category ID");
+ }
+
+ iconUploadInput.accept = "image/svg+xml";
+ targetedImageElement = categoryIcon;
+
+ teleportElement(selectIconButton, categoryIcon.parentElement);
+ teleportElement(confirmActions, editActions);
+
+ editActions.querySelector("div[data-primary-actions]").style.display =
+ "none";
+
+ requestAnimationFrame(() => {
+ currentlyEditing.cleanup = replaceWithResizableTextarea([
+ { targetEl: categoryName, fill: false },
+ ]);
+ // by adding a delay, we dont block the UI
+ setTimeout(() => {
+ categoryEl.querySelector("textarea").focus();
+ }, 0);
+ });
+}
+
+async function confirmCategoryEdit() {
+ let categoryEl = document.getElementById(
+ `${currentlyEditing.categoryID}_category`
+ );
+ let categoryInput = categoryEl.querySelector("textarea");
+
+ if (categoryInput.value === "") {
+ return;
+ }
+
+ categoryInput.value = categoryInput.value.trim();
+
+ let formData = new FormData();
+ if (categoryInput.value !== currentlyEditing.originalText) {
+ formData.append("name", categoryInput.value);
+ }
+
+ if (iconUploadInput.files.length > 0) {
+ formData.append("icon", iconUploadInput.files[0]);
+ }
+
+ // nothing to update
+ if (formData.get("name") === null && formData.get("icon") === null) {
+ return;
+ }
+
+ let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
+ method: "PATCH",
+ body: formData,
+ });
+
+ if (res.status === 200) {
+ iconUploadInput.value = "";
+
+ currentlyEditing.icon = undefined;
+
+ cancelCategoryEdit(categoryInput.value);
+
+ currentlyEditing = {};
+ } else {
+ console.error("Failed to edit category");
+ }
+}
+
+function cancelCategoryEdit(text = currentlyEditing.originalText) {
+ let categoryEl = document.getElementById(
+ `${currentlyEditing.categoryID}_category`
+ );
+
+ let categoryInput = categoryEl.querySelector("textarea");
+ let categoryIcon = categoryEl.querySelector(".category-img img");
+ let editActions = categoryEl.querySelector("[data-edit-actions]");
+
+ if (currentlyEditing.icon !== undefined) {
+ categoryIcon.src = currentlyEditing.icon;
+ }
+
+ unteleportElement(selectIconButton);
+ unteleportElement(confirmActions);
+
+ editActions.querySelector("div[data-primary-actions]").style.display = "";
+
+ restoreElementFromInput(categoryInput, text);
+
+ currentlyEditing = {};
+ targetedImageElement = null;
+}
+
+/**
+ * Deletes the category with the given html element
+ * @param {HTMLElement} target The target element that was clicked
+ */
+function deleteCategory(target) {
+ let categoryEl = target.closest(".category-header");
+
+ if (currentlyEditing.categoryID !== undefined) {
+ // cancel the edit if it's already in progress
+ cancelEdit();
+ }
+
+ let categoryID = parseInt(categoryEl.id);
+
+ currentlyEditing.categoryID = categoryID;
+
+ let categoryNameSpan = document.getElementById("category-name");
+ categoryNameSpan.textContent = categoryEl.querySelector("h2").textContent;
+
+ openModal("category-delete");
+}
+
+async function confirmDeleteCategory() {
+ let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
+ method: "DELETE",
+ });
+
+ if (res.status === 200) {
+ let categoryEl = document.getElementById(
+ `${currentlyEditing.categoryID}_category`
+ );
+ // get the next element and remove it (its the link grid)
+ let nextEl = categoryEl.nextElementSibling;
+ nextEl.remove();
+ categoryEl.remove();
+
+ closeModal();
+ currentlyEditing = {};
+ }
+}
+
+function roundToNearestHundredth(num) {
+ return Math.round(num * 100) / 100;
+}
+
+const stylesToCopy = [
+ "font-family",
+ "font-size",
+ "font-weight",
+ "font-style",
+ "color",
+ "line-height",
+ "letter-spacing",
+ "text-transform",
+ "text-align",
+];
+
+let _textMeasuringSpan,
+ _textMeasuringDiv = null;
+
+/**
+ * @typedef {Object} ResizeableTextareaOptions
+ * @property {HTMLElement} targetEl The element to replace.
+ * @property {boolean} [fill=true] Whether to make the textarea fill the available space, or grow with the text inside.
+ */
+
+/**
+ * Replaces an element with a resizable textarea containing the same text.
+ * @param {ResizeableTextareaOptions[]} targetEls The elements to replace.
+ * @returns (() => void) A cleanup function to remove event listeners
+ */
+function replaceWithResizableTextarea(targetEls) {
+ let startTime = performance.now();
+
+ /**
+ * @typedef {Object} TargetInfo
+ * @property {HTMLElement} targetEl The element to replace.
+ * @property {boolean} fill Whether to make the textarea fill the available space, or grow with the text inside.
+ * @property {string} originalText The original text of the element
+ * @property {CSSStyleDeclaration} computedStyle The computed style of the element
+ * @property {DOMRect} boundingRect The bounding rect of the element
+ * @property {number} borderWidth The border width of the element
+ * @property {number} borderHeight The border height of the element
+ * @property {number} maxWidth The maximum width of the element
+ */
+
+ /**
+ * @type {TargetInfo[]}
+ */
+ let targetInfos = [];
+
+ targetEls.forEach((target) => {
+ let targetEl = target.targetEl;
+ let fill = target.fill === undefined ? true : target.fill;
+ // step 1: batch reads
+ const originalText = targetEl.textContent;
+ const computedStyle = window.getComputedStyle(targetEl);
+ const boundingRect = targetEl.getBoundingClientRect();
+ const parentBoundingRect =
+ targetEl.parentElement.getBoundingClientRect();
+
+ const borderWidth =
+ parseFloat(computedStyle.borderLeftWidth) +
+ parseFloat(computedStyle.borderRightWidth);
+ const borderHeight =
+ parseFloat(computedStyle.borderTopWidth) +
+ parseFloat(computedStyle.borderBottomWidth);
+
+ let maxWidth = parentBoundingRect.width - borderWidth;
+ // take care of category headers specifically because the parent bounding box contains two other elements
+ if (targetEl.tagName === "H2") {
+ let imageWidth =
+ targetEl.previousElementSibling.getBoundingClientRect().width;
+ let actionButtonWidth =
+ targetEl.nextElementSibling.getBoundingClientRect().width;
+
+ maxWidth -= imageWidth + actionButtonWidth;
+ }
+
+ maxWidth = roundToNearestHundredth(maxWidth);
+
+ targetInfos.push({
+ targetEl,
+ fill,
+ originalText,
+ computedStyle,
+ boundingRect,
+ borderWidth,
+ borderHeight,
+ maxWidth,
+ });
+ });
+
+ const caretBuffer = 10;
+
+ // step 2: calculate styles
+ let elsInitialStyles = [];
+
+ targetInfos.forEach((targetInfo) => {
+ let fill = targetInfo.fill;
+
+ let initialStyles = {};
+ initialStyles.width = "";
+ initialStyles.height = `${parseFloat(
+ roundToNearestHundredth(targetInfo.boundingRect.height)
+ )}px`;
+ if (fill) {
+ initialStyles.width = `100%`;
+ } else {
+ if (!_textMeasuringSpan) {
+ _textMeasuringSpan = document.createElement("span");
+ // Keep it off-screen and static once appended
+ Object.assign(_textMeasuringSpan.style, {
+ position: "absolute",
+ left: "-9999px",
+ top: "0",
+ visibility: "hidden",
+ whiteSpace: "nowrap",
+ });
+ document.body.appendChild(_textMeasuringSpan);
+ }
+
+ stylesToCopy.forEach((prop) => {
+ _textMeasuringSpan.style[prop] = targetInfo.computedStyle[prop];
+ });
+
+ _textMeasuringSpan.textContent =
+ targetInfo.originalText === ""
+ ? targetInfo.boundingRect.placeholder || "W"
+ : targetInfo.originalText;
+
+ let measuredTextWidth = roundToNearestHundredth(
+ _textMeasuringSpan.getBoundingClientRect().width
+ );
+
+ let finalWidth = Math.min(
+ measuredTextWidth + caretBuffer,
+ targetInfo.maxWidth
+ );
+ initialStyles.width = `${finalWidth}px`;
+ }
+
+ elsInitialStyles.push({
+ originalText: targetInfo.originalText,
+ targetEl: targetInfo.targetEl,
+ targetElComputedStyle: targetInfo.computedStyle,
+ fill: fill,
+ initialStyles,
+ });
+ });
+
+ // step 3: batch writes
+ let inputElements = [];
+
+ elsInitialStyles.forEach((elInfo) => {
+ const inputElement = document.createElement("textarea");
+ inputElement.value = elInfo.originalText;
+ inputElement.className = "resizable-input";
+ inputElement.placeholder = elInfo.targetEl.dataset.placeholder;
+ inputElement.dataset.originalElementType = elInfo.targetEl.tagName;
+ inputElement.dataset.originalClassName = elInfo.targetEl.className;
+
+ let computedStyles = {};
+ // Apply inherited styles
+ stylesToCopy.forEach((prop) => {
+ computedStyles[prop] = elInfo.targetElComputedStyle[prop];
+ });
+
+ // Apply custom styles and calculated dimensions
+ Object.assign(inputElement.style, {
+ backgroundColor: "var(--color-base)",
+ border: `1px solid var(--color-highlight-sm)`,
+ borderRadius: "0.375rem",
+ resize: "none",
+ overflow: "hidden",
+ outline: "none",
+ ...computedStyles, // Apply calculated width and height
+ ...elInfo.initialStyles, // Apply calculated width and height
+ });
+
+ inputElement.setAttribute(
+ "maxlength",
+ elInfo.targetEl.tagName[0] === "H" ? 50 : 150
+ );
+
+ inputElements.push({
+ targetEl: elInfo.targetEl,
+ fill: elInfo.fill,
+ element: inputElement,
+ });
+ });
+
+ function resize(inputElement, fill = false) {
+ const currentInputComputedStyle = window.getComputedStyle(inputElement);
+ const currentInputBorderWidth =
+ parseFloat(currentInputComputedStyle.borderLeftWidth) +
+ parseFloat(currentInputComputedStyle.borderRightWidth);
+
+ const currentParentElBoundingRectWidth =
+ inputElement.parentElement.getBoundingClientRect().width;
+
+ let maxWidth = roundToNearestHundredth(
+ currentParentElBoundingRectWidth
+ );
+
+ // is it maybe a bit of some math that doesnt entirely make sense to me? you bet. But does it work? Hell yeah it does
+ if (inputElement.dataset.originalElementType === "H2") {
+ let imageWidth =
+ inputElement.previousElementSibling.getBoundingClientRect()
+ .width;
+ let actionButtonWidth =
+ inputElement.nextElementSibling.getBoundingClientRect().width;
+
+ // the brain cells rub together and this vaguely makes sense to me I think but I cant explain it
+ maxWidth -= imageWidth + actionButtonWidth + caretBuffer;
+ maxWidth += currentInputBorderWidth;
+ }
+
+ let currentContentWidth;
+
+ if (!fill) {
+ if (!_textMeasuringSpan) {
+ // Should already be created, but just in case
+ _textMeasuringSpan = document.createElement("span");
+ Object.assign(_textMeasuringSpan.style, {
+ position: "absolute",
+ left: "-9999px",
+ top: "0",
+ visibility: "hidden",
+ whiteSpace: "nowrap",
+ });
+ document.body.appendChild(_textMeasuringSpan);
+ }
+
+ stylesToCopy.forEach((prop) => {
+ _textMeasuringSpan.style[prop] =
+ currentInputComputedStyle[prop];
+ });
+
+ _textMeasuringSpan.textContent =
+ inputElement.value === ""
+ ? inputElement.placeholder || "W"
+ : inputElement.value;
+
+ let measuredTextWidth =
+ _textMeasuringSpan.getBoundingClientRect().width;
+
+ currentContentWidth = Math.min(
+ roundToNearestHundredth(
+ measuredTextWidth + currentInputBorderWidth
+ ) + caretBuffer,
+ maxWidth
+ );
+ } else {
+ // if fill is true, width is flexible, but for measuring we need to know the *actual* width of the content
+ currentContentWidth = maxWidth;
+ }
+
+ if (!_textMeasuringDiv) {
+ _textMeasuringDiv = document.createElement("div");
+ Object.assign(_textMeasuringDiv.style, {
+ position: "absolute",
+ left: "-9999px",
+ top: "0",
+ visibility: "hidden",
+ // Allow wrapping exactly like a textarea
+ whiteSpace: "pre-wrap",
+ wordWrap: "break-word",
+ });
+ document.body.appendChild(_textMeasuringDiv);
+ }
+
+ [
+ "borderLeftWidth",
+ "borderRightWidth",
+ "borderTopWidth",
+ "borderBottomWidth",
+ ...stylesToCopy,
+ ].forEach((prop) => {
+ _textMeasuringDiv.style[prop] = currentInputComputedStyle[prop];
+ });
+
+ _textMeasuringDiv.style.width = `${currentContentWidth}px`;
+ _textMeasuringDiv.textContent =
+ inputElement.value === ""
+ ? inputElement.placeholder || "W"
+ : inputElement.value;
+ let measuredContentHeight =
+ _textMeasuringDiv.getBoundingClientRect().height;
+
+ // we set the height = 0 so that if a row is deleted, the height will be recalculated correctly
+ inputElement.style.width = `${currentContentWidth}px`;
+ inputElement.style.height = "0px";
+ inputElement.style.height = `${measuredContentHeight}px`;
+ }
+
+ function resizeAll() {
+ inputElements.forEach((inputEl) => {
+ resize(inputEl.element, inputEl.fill);
+ });
+ }
+
+ // step 4: append
+ inputElements.forEach((inputEl) => {
+ inputEl.targetEl.parentNode.replaceChild(
+ inputEl.element,
+ inputEl.targetEl
+ );
+ inputEl.element.addEventListener("input", () => {
+ resize(inputEl.element, inputEl.fill);
+ });
+ });
+
+ let resizeScheduled = false;
+
+ function windowResize() {
+ if (!resizeScheduled) {
+ resizeScheduled = true;
+ requestAnimationFrame(() => {
+ resizeAll();
+ resizeScheduled = false;
+ });
+ }
+ }
+
+ window.addEventListener("resize", windowResize);
+
+ // if the caller wants to focus the textarea, they can do it themselves
+
+ return () => {
+ window.removeEventListener("resize", windowResize);
+ };
+}
+
+/**
+ * Restores an element from a textarea
+ * @param {HTMLElement} inputEl The textarea to restore
+ * @param {string} originalText The original text of the textarea
+ */
+function restoreElementFromInput(inputEl, originalText) {
+ const computedStyle = window.getComputedStyle(inputEl);
+ let styles = {};
+
+ let elementType = inputEl.dataset.originalElementType;
+ const newElement = document.createElement(elementType);
+ newElement.textContent = originalText;
+ newElement.className = inputEl.dataset.originalClassName;
+ newElement.dataset.placeholder = inputEl.placeholder;
+
+ stylesToCopy.forEach((prop) => {
+ styles[prop] = computedStyle[prop];
+ });
+
+ Object.assign(newElement.style, {
+ ...styles,
+ border: "1px solid #0000",
+ });
+
+ inputEl.parentNode.replaceChild(newElement, inputEl);
+}
diff --git a/src/styles/adminUi.css b/src/styles/adminUi.css
new file mode 100644
index 0000000..ffbb209
--- /dev/null
+++ b/src/styles/adminUi.css
@@ -0,0 +1,70 @@
+.modal-bg {
+ visibility: hidden;
+ opacity: 0;
+}
+
+.modal-bg.is-visible {
+ visibility: visible;
+ opacity: 1;
+}
+
+.modal {
+ opacity: 0;
+}
+
+.modal.is-visible {
+ opacity: 1;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .modal-bg {
+ visibility: hidden;
+ opacity: 0;
+
+ transition: opacity 0.3s ease, visibility 0s 0.3s;
+ transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
+ }
+
+ .modal-bg.is-visible {
+ visibility: visible;
+ opacity: 1;
+ transition-delay: 0s;
+ }
+
+ .modal {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
+ }
+
+ .modal.is-visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0) scale(1);
+ transition-delay: 0s;
+ }
+}
+
+.action-button {
+ display: flex;
+ width: fit-content;
+ height: fit-content;
+ padding: 0.25rem;
+ background-color: var(--color-highlight-sm);
+ border: 1px solid color-mix(in srgb, var(--color-highlight) 70%, #0000);
+ border-radius: 9999px;
+ box-shadow: var(--shadow-sm);
+ cursor: pointer;
+ transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
+ contain: layout style paint;
+
+ &:hover {
+ filter: brightness(125%);
+ }
+
+ &:active {
+ filter: brightness(95%);
+ }
+}
diff --git a/src/styles/main.css b/src/styles/main.css
new file mode 100644
index 0000000..3430f82
--- /dev/null
+++ b/src/styles/main.css
@@ -0,0 +1,348 @@
+@layer reset, base, components, utilities;
+@font-face {
+ font-family: "Instrument Sans";
+ src: url("/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2")
+ format("woff2");
+ font-display: swap;
+}
+
+@layer reset {
+ /*
+ Josh's Custom CSS Reset slightly Modified
+ https://www.joshwcomeau.com/css/custom-css-reset/
+ */
+
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ * {
+ line-height: calc(1em + 0.5rem);
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ -webkit-font-smoothing: antialiased;
+ }
+
+ img,
+ picture,
+ video,
+ canvas,
+ svg {
+ display: block;
+ max-width: 100%;
+ }
+
+ input,
+ button,
+ textarea,
+ select {
+ font: inherit;
+ }
+
+ button {
+ cursor: pointer;
+ }
+
+ p,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ word-break: break-word;
+ }
+
+ p {
+ text-wrap: pretty;
+ hyphens: auto;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ text-wrap: balance;
+ }
+
+ a {
+ color: #fff;
+ }
+}
+
+@layer base {
+ :root {
+ --family-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
+ --color-accent: oklch(57.93% 0.258 294.12);
+ --color-success: oklch(70.19% 0.158 160.44);
+ --color-error: oklch(53% 0.251 28.48);
+
+ --color-base: oklch(11% 0.007 285);
+ --color-surface: oklch(19% 0.007 285.66);
+ --color-overlay: oklch(26% 0.008 285.66);
+
+ --color-muted: oklch(63% 0.015 286);
+ --color-subtle: oklch(72% 0.015 286);
+ --color-text: oklch(87% 0.015 286);
+
+ --color-highlight-sm: oklch(30.67% 0.007 286);
+ --color-highlight: oklch(39.26% 0.01 286);
+ --color-highlight-lg: oklch(47.72% 0.011 286);
+
+ --spacing-1: 0.125rem;
+ --spacing-2: 0.25rem;
+ --spacing-3: 0.375rem;
+ --spacing-4: 0.5rem;
+ --spacing-5: 0.625rem;
+ --spacing-6: 0.75rem;
+ --spacing-7: 0.875rem;
+ --spacing-8: 1rem;
+ }
+
+ html {
+ font-family: var(--family-sans);
+ color-scheme: dark;
+ color: var(--color-text);
+ background-color: var(--color-surface);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-weight: 600;
+ }
+
+ h1 {
+ font-size: clamp(42px, 10vw, 64px);
+ }
+
+ h2 {
+ font-size: clamp(30px, 6vw, 36px);
+ }
+
+ h3 {
+ font-size: 1.25rem;
+ }
+
+ input:not(.search) {
+ @apply px-4 py-2 rounded-md w-full bg-surface border border-highlight/70 placeholder:text-highlight text-text focus-visible:outline-none transition-colors duration-300 ease-out overflow-hidden;
+
+ &[type="file"] {
+ @apply p-0 cursor-pointer;
+
+ &::file-selector-button {
+ @apply px-2 py-2 mr-1 bg-highlight text-subtle cursor-pointer;
+ }
+ }
+ }
+}
+
+@layer components {
+ .link-card {
+ background: var(--color-overlay);
+ display: flex;
+ flex-direction: row;
+ text-decoration: none;
+ border-radius: var(--spacing-8);
+ padding: var(--spacing-5);
+ align-items: center;
+ transition-property: box-shadow, transform, translate;
+ transition-duration: 150ms;
+ transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
+ 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ contain: layout style paint;
+
+ &:not(.admin) {
+ &:hover {
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ transform: translateY(-4px);
+ }
+
+ &:active {
+ box-shadow: 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ transform: translateY(2px);
+ }
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .link-card {
+ transition: none;
+
+ &:hover {
+ transform: none;
+ }
+
+ &:active {
+ transform: none;
+ }
+ }
+ }
+
+ /* Div that holds the image */
+ .link-card div[data-img-container] {
+ flex-shrink: 0;
+ padding-right: var(--spacing-4);
+ }
+
+ .link-card div[data-img-container] img {
+ user-select: none;
+ border-radius: var(--spacing-3);
+ aspect-ratio: 1/1;
+ object-fit: cover;
+ }
+
+ /* Div that holds the text */
+ .link-card div[data-text-container] {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ row-gap: 1px;
+ overflow: hidden;
+ word-break: break-all;
+ }
+
+ .link-card div[data-text-container] p {
+ color: var(--color-subtle);
+ white-space: pre-wrap;
+ border: 1px solid #0000;
+ min-height: calc(1em + 0.5rem);
+ }
+
+ .new-link-card {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: var(--spacing-5);
+ border: var(--spcaing-1) dashed var(--color-subtle);
+ border-radius: var(--spacing-8);
+ transition: box-shadow, transofrm 150ms cubic-bezier(0.45, 0, 0.55, 1);
+ cursor: pointer;
+ user-select: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .categoy-header {
+ display: flex;
+ align-items: center;
+ }
+
+ .category-header div[data-img-container] {
+ display: flex;
+ flex-shrink: 0;
+ position: relative;
+ margin-right: var(--spacing-4);
+ align-items: center;
+ justify-content: center;
+ width: 8rem;
+ height: 8rem;
+ }
+
+ .categoy-header div[data-img-container] img {
+ user-select: none;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .category-header h2 {
+ text-transform: capitalize;
+ word-break: break-all;
+ border-width: 1px;
+ border-color: #0000;
+ }
+
+ .link-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr));
+ gap: var(--spacing-4);
+ padding: var(--spacing-5);
+ contain: layout style paint;
+ }
+
+ .hero {
+ /* grid grid-rows-3 grid-cols-[1fr] justify-center items-center h-screen bg-base */
+ display: grid;
+ grid-template: repeat(3, minmax(0, 1fr)) / 1fr;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ background-color: var(--color-base);
+ }
+
+ .glance-container {
+ display: flex;
+ color: var(--color-subtle);
+ height: 100%;
+ padding: var(--spacing-5);
+ justify-content: space-between;
+ }
+
+ .primary-hero-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ grid-row-start: 2;
+ padding: var(--spacing-6);
+ }
+
+ .weather-data {
+ display: flex;
+ height: fit-content;
+ align-items: center;
+ font-weight: 600;
+ }
+
+ .weather-data span {
+ margin-right: var(--spacing-4);
+ }
+
+ .uptime-data {
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+
+ & > div {
+ display: flex;
+ align-items: center;
+
+ & > span {
+ margin-right: var(--spacing-4);
+ }
+ }
+ }
+
+ .uptime-status {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: var(--spacing-4);
+ height: var(--spacing-4);
+ }
+}
+
+@layer utilities {
+ .flex {
+ display: flex;
+ }
+
+ .leading-condensed {
+ line-height: normal;
+ }
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
deleted file mode 100644
index 2ebfdd7..0000000
--- a/src/styles/main.scss
+++ /dev/null
@@ -1,159 +0,0 @@
-@import "tailwindcss";
-
-
-@theme {
- --color-accent: oklch(57.93% 0.258 294.12);
- --color-success: oklch(70.19% 0.158 160.44);
- --color-error: oklch(53% 0.251 28.48);
-
- --color-base: oklch(11% .007 285);
- --color-surface: oklch(19% 0.007 285.66);
- --color-overlay: oklch(26% 0.008 285.66);
-
- --color-muted: oklch(63% 0.015 286);
- --color-subtle: oklch(72% 0.015 286);
- --color-text: oklch(87% 0.015 286);
-
- --color-highlight-sm: oklch(30.67% 0.007 286);
- --color-highlight: oklch(39.26% 0.010 286);
- --color-highlight-lg: oklch(47.72% 0.011 286);
-}
-
-@font-face {
- font-family: "Instrument Sans";
- src: url("/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2") format("woff2");
- font-display: swap;
-}
-
-:root {
- --default-font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-}
-
-html {
- line-height: normal;
- color-scheme: dark;
- color: var(--color-text);
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- @apply font-semibold;
-}
-
-h1 {
- font-size: clamp(42px, 10vw, 64px);
-}
-
-h2 {
- font-size: clamp(30px, 6vw, 36px);
-}
-
-h3 {
- font-size: 1.25rem;
-}
-
-button {
- cursor: pointer;
-}
-
-input:not(.search) {
- @apply px-4 py-2 rounded-md w-full bg-surface border border-highlight/70 placeholder:text-highlight text-text focus-visible:outline-none transition-colors duration-300 ease-out overflow-hidden;
-
- &[type="file"] {
- @apply p-0 cursor-pointer;
-
- &::file-selector-button {
- @apply px-2 py-2 mr-1 bg-highlight text-subtle cursor-pointer;
- }
- }
-}
-
-.link-card {
- background: var(--color-overlay);
- display: flex;
- flex-direction: row;
- text-decoration: none;
- border-radius: 1rem;
- padding: 0.625rem;
- align-items: center;
- transition-property: box-shadow, transform, translate;
- transition-duration: 150ms;
- transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
- box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
- contain: layout style paint;
-
- &:not(.admin) {
- &:hover {
- box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
- transform: translateY(-4px);
- }
-
- &:active {
- box-shadow: 0 2px 4px -2px rgb(0 0 0 / 0.1);
- transform: translateY(2px);
- }
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .link-card {
- transition: none;
-
- &:hover {
- transform: none;
- }
-
- &:active {
- transform: none;
- }
- }
-}
-
-/* Div that holds the image */
-.link-card div[data-img-container] {
- flex-shrink: 0;
- margin-right: 0.5rem;
-}
-
-.link-card div[data-img-container] img {
- user-select: none;
- border-radius: 0.375rem;
- aspect-ratio: 1/1;
- object-fit: cover;
-}
-
-/* Div that holds the text */
-.link-card div[data-text-container] {
- word-break: break-all;
-}
-
-.link-card div[data-text-container] p {
- color: var(--color-subtle);
-}
-
-
-.categoy-header {
- display: flex;
- align-items: center;
-}
-
-.category-header div[data-img-container] {
- @apply shrink-0 relative mr-2 h-full flex items-center justify-center size-8;
-}
-
-.categoy-header div[data-img-container] img {
- user-select: none;
- object-fit: cover;
- aspect-ratio: 1/1;
-}
-
-.category-header h2 {
- text-transform: capitalize;
- word-break: break-all;
- border-width: 1px;
- border-color: #0000;
-}
\ No newline at end of file
diff --git a/src/templates/layouts/admin.hbs b/src/templates/layouts/admin.hbs
new file mode 100644
index 0000000..0aaaafd
--- /dev/null
+++ b/src/templates/layouts/admin.hbs
@@ -0,0 +1,20 @@
+
+
+
+
+ Passport
+
+
+
+ {{{embedFile "assets/styles/main.css"}}}
+ {{{embedFile "assets/styles/adminUi.css"}}}
+
+
+
+ {{embed}}
+
+
+{{{devContent}}}
+
+
\ No newline at end of file
diff --git a/src/templates/layouts/main.hbs b/src/templates/layouts/main.hbs
index 8ab1f0d..959b4cd 100644
--- a/src/templates/layouts/main.hbs
+++ b/src/templates/layouts/main.hbs
@@ -3,14 +3,29 @@
Passport
-
-
-
-
+
+
+
+
+
+ {{{embedFile "assets/styles/main.css"}}}
{{embed}}
+
{{{devContent}}}
+