diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index fc1e2602da..4dc41e143e 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -71,6 +71,7 @@ services:
       - ../web:/usr/src/app
       - ../i18n:/usr/src/i18n
       - ../open-api/:/usr/src/open-api/
+      # - ../../ui:/usr/ui
       - /usr/src/app/node_modules
     ulimits:
       nofile:
diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md
index 9dbaf157b5..f341c3e9cb 100644
--- a/docs/docs/developer/setup.md
+++ b/docs/docs/developer/setup.md
@@ -63,6 +63,17 @@ If you only want to do web development connected to an existing, remote backend,
 IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
 ```
 
+#### `@immich/ui`
+
+To see local changes to `@immich/ui` in Immich, do the following:
+
+1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
+1. Build the `@immich/ui` project via `npm run build`
+1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
+1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
+1. Start up the stack via `make dev`
+1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
+
 ### Mobile app
 
 The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
diff --git a/web/package-lock.json b/web/package-lock.json
index 426df0acd7..14d0928731 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -11,6 +11,7 @@
       "dependencies": {
         "@formatjs/icu-messageformat-parser": "^2.9.8",
         "@immich/sdk": "file:../open-api/typescript-sdk",
+        "@immich/ui": "^0.11.0",
         "@mapbox/mapbox-gl-rtl-text": "0.2.3",
         "@mdi/js": "^7.4.47",
         "@photo-sphere-viewer/core": "^5.11.5",
@@ -104,7 +105,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
       "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
-      "dev": true,
       "engines": {
         "node": ">=10"
       },
@@ -793,6 +793,31 @@
         "npm": ">=9.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+      "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.9"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.13",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+      "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.9"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+      "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+      "license": "MIT"
+    },
     "node_modules/@formatjs/ecma402-abstract": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz",
@@ -1279,11 +1304,34 @@
       "resolved": "../open-api/typescript-sdk",
       "link": true
     },
+    "node_modules/@immich/ui": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.11.0.tgz",
+      "integrity": "sha512-zRQFHCVt6BstNkGuVt27rLUAurOpZ0djfaZYDeqHuc8H97XXXk+hsbXzvADlVa9xAPHetUM3JuusPseJ+Hr23g==",
+      "license": "GNU Affero General Public License version 3",
+      "dependencies": {
+        "@mdi/js": "^7.4.47",
+        "bits-ui": "^1.0.0-next.46",
+        "tailwind-merge": "^2.5.4",
+        "tailwind-variants": "^0.3.0"
+      },
+      "peerDependencies": {
+        "svelte": "^5.0.0"
+      }
+    },
+    "node_modules/@internationalized/date": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz",
+      "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@swc/helpers": "^0.5.0"
+      }
+    },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
       "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-      "dev": true,
       "dependencies": {
         "string-width": "^5.1.2",
         "string-width-cjs": "npm:string-width@^4.2.0",
@@ -1300,7 +1348,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
       "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1312,7 +1359,6 @@
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
       "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1323,14 +1369,12 @@
     "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
     },
     "node_modules/@isaacs/cliui/node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
       "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-      "dev": true,
       "dependencies": {
         "eastasianwidth": "^0.2.0",
         "emoji-regex": "^9.2.2",
@@ -1347,7 +1391,6 @@
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^6.0.1"
       },
@@ -1362,7 +1405,6 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
       "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^6.1.0",
         "string-width": "^5.0.1",
@@ -1542,7 +1584,6 @@
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-      "dev": true,
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
         "run-parallel": "^1.1.9"
@@ -1555,7 +1596,6 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-      "dev": true,
       "engines": {
         "node": ">= 8"
       }
@@ -1564,7 +1604,6 @@
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "dev": true,
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
         "fastq": "^1.6.0"
@@ -1605,7 +1644,6 @@
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
       "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-      "dev": true,
       "optional": true,
       "engines": {
         "node": ">=14"
@@ -2017,6 +2055,15 @@
         "vite": "^5.0.0"
       }
     },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
     "node_modules/@testing-library/dom": {
       "version": "10.2.0",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.2.0.tgz",
@@ -2826,7 +2873,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -2846,14 +2892,12 @@
     "node_modules/any-promise": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
-      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
-      "dev": true
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
     },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
       "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-      "dev": true,
       "dependencies": {
         "normalize-path": "^3.0.0",
         "picomatch": "^2.0.4"
@@ -2865,8 +2909,7 @@
     "node_modules/arg": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
-      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
-      "dev": true
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
     },
     "node_modules/argparse": {
       "version": "2.0.1",
@@ -2967,18 +3010,40 @@
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
     "node_modules/binary-extensions": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
       "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
     },
+    "node_modules/bits-ui": {
+      "version": "1.0.0-next.77",
+      "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.77.tgz",
+      "integrity": "sha512-IV0AyVEvsRkXv4s/fl4iea5E9W2b9EBf98s9mRMKMc1xHxM9MmtM2r6MZMqftHQ/c+gHTIt3A9EKuTlh7uay8w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.4",
+        "@floating-ui/dom": "^1.6.7",
+        "@internationalized/date": "^3.5.6",
+        "esm-env": "^1.1.2",
+        "runed": "^0.22.0",
+        "svelte-toolbelt": "^0.7.0"
+      },
+      "engines": {
+        "node": ">=18",
+        "pnpm": ">=8.7.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/huntabyte"
+      },
+      "peerDependencies": {
+        "svelte": "^5.11.0"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2993,7 +3058,6 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
@@ -3093,7 +3157,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
       "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
-      "dev": true,
       "engines": {
         "node": ">= 6"
       }
@@ -3164,7 +3227,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "anymatch": "~3.1.2",
@@ -3189,7 +3251,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
       "dependencies": {
         "is-glob": "^4.0.1"
       },
@@ -3350,7 +3411,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
       "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
-      "dev": true,
       "engines": {
         "node": ">= 6"
       }
@@ -3388,7 +3448,6 @@
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
       "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
@@ -3416,7 +3475,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
       "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
-      "dev": true,
       "bin": {
         "cssesc": "bin/cssesc"
       },
@@ -3580,14 +3638,12 @@
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
-      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
-      "dev": true
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
     },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
-      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
-      "dev": true
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
     },
     "node_modules/dom-accessibility-api": {
       "version": "0.5.16",
@@ -3621,8 +3677,7 @@
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-      "dev": true
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
     },
     "node_modules/electron-to-chromium": {
       "version": "1.5.74",
@@ -3634,8 +3689,7 @@
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/engine.io-client": {
       "version": "6.5.4",
@@ -4146,9 +4200,9 @@
       }
     },
     "node_modules/esm-env": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
-      "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+      "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
       "license": "MIT"
     },
     "node_modules/esniff": {
@@ -4306,7 +4360,6 @@
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
       "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
@@ -4323,7 +4376,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
       "dependencies": {
         "is-glob": "^4.0.1"
       },
@@ -4347,7 +4399,6 @@
       "version": "1.15.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
       "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
-      "dev": true,
       "dependencies": {
         "reusify": "^1.0.4"
       }
@@ -4374,7 +4425,6 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -4424,7 +4474,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
       "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
-      "dev": true,
       "dependencies": {
         "cross-spawn": "^7.0.0",
         "signal-exit": "^4.0.1"
@@ -4469,7 +4518,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
@@ -4482,8 +4530,7 @@
     "node_modules/function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "node_modules/geojson-vt": {
       "version": "3.2.1",
@@ -4527,7 +4574,6 @@
       "version": "10.4.5",
       "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
       "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "foreground-child": "^3.1.0",
@@ -4548,7 +4594,6 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
       "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
       "dependencies": {
         "is-glob": "^4.0.3"
       },
@@ -4560,7 +4605,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
@@ -4570,7 +4614,6 @@
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
       "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
@@ -4666,7 +4709,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "dependencies": {
         "function-bind": "^1.1.1"
       },
@@ -4852,6 +4894,12 @@
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
       "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
     },
+    "node_modules/inline-style-parser": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
+      "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
+      "license": "MIT"
+    },
     "node_modules/internmap": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -4882,7 +4930,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
       "dependencies": {
         "binary-extensions": "^2.0.0"
       },
@@ -4909,7 +4956,6 @@
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
       "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
-      "dev": true,
       "dependencies": {
         "has": "^1.0.3"
       },
@@ -4944,7 +4990,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -4953,7 +4998,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -4962,7 +5006,6 @@
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
       "dependencies": {
         "is-extglob": "^2.1.1"
       },
@@ -4974,7 +5017,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
@@ -5113,7 +5155,6 @@
       "version": "3.4.3",
       "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
       "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
-      "dev": true,
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
@@ -5128,7 +5169,6 @@
       "version": "1.21.6",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
       "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "jiti": "bin/jiti.js"
@@ -5303,8 +5343,7 @@
     "node_modules/lines-and-columns": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
     },
     "node_modules/locate-character": {
       "version": "3.0.0",
@@ -5546,7 +5585,6 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
       "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true,
       "engines": {
         "node": ">= 8"
       }
@@ -5555,7 +5593,6 @@
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
       "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "braces": "^3.0.3",
@@ -5623,7 +5660,6 @@
       "version": "7.1.2",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
       "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-      "dev": true,
       "engines": {
         "node": ">=16 || 14 >=14.17"
       }
@@ -5660,7 +5696,6 @@
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
       "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
-      "dev": true,
       "dependencies": {
         "any-promise": "^1.0.0",
         "object-assign": "^4.0.1",
@@ -5671,7 +5706,6 @@
       "version": "3.3.8",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
       "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -5734,7 +5768,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -5760,7 +5793,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -5769,7 +5801,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
       "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
-      "dev": true,
       "engines": {
         "node": ">= 6"
       }
@@ -5850,8 +5881,7 @@
     "node_modules/package-json-from-dist": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
-      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
-      "dev": true
+      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
     },
     "node_modules/parent-module": {
       "version": "1.0.1",
@@ -5910,7 +5940,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
       "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -5918,14 +5947,12 @@
     "node_modules/path-parse": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
     "node_modules/path-scurry": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
       "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-      "dev": true,
       "dependencies": {
         "lru-cache": "^10.2.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -5940,8 +5967,7 @@
     "node_modules/path-scurry/node_modules/lru-cache": {
       "version": "10.4.3",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
-      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
-      "dev": true
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
     },
     "node_modules/pathe": {
       "version": "1.1.2",
@@ -5976,14 +6002,12 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
       "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
       "engines": {
         "node": ">=8.6"
       },
@@ -5995,7 +6019,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
       "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -6004,7 +6027,6 @@
       "version": "4.0.6",
       "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
       "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
-      "dev": true,
       "engines": {
         "node": ">= 6"
       }
@@ -6031,7 +6053,6 @@
       "version": "8.5.0",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.0.tgz",
       "integrity": "sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -6060,7 +6081,6 @@
       "version": "15.1.0",
       "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
       "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
-      "dev": true,
       "dependencies": {
         "postcss-value-parser": "^4.0.0",
         "read-cache": "^1.0.0",
@@ -6077,7 +6097,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
       "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
-      "dev": true,
       "dependencies": {
         "camelcase-css": "^2.0.1"
       },
@@ -6125,7 +6144,6 @@
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
       "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -6194,7 +6212,6 @@
       "version": "6.1.2",
       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
       "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "cssesc": "^3.0.0",
@@ -6207,8 +6224,7 @@
     "node_modules/postcss-value-parser": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
-      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
-      "dev": true
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
     },
     "node_modules/potpack": {
       "version": "2.0.0",
@@ -6341,7 +6357,6 @@
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -6372,7 +6387,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
       "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
-      "dev": true,
       "dependencies": {
         "pify": "^2.3.0"
       }
@@ -6483,7 +6497,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "dev": true,
       "dependencies": {
         "picomatch": "^2.2.1"
       },
@@ -6561,7 +6574,6 @@
       "version": "1.22.8",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
       "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-core-module": "^2.13.0",
@@ -6587,7 +6599,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true,
       "engines": {
         "iojs": ">=1.0.0",
         "node": ">=0.10.0"
@@ -6697,7 +6708,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
       "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -6716,6 +6726,21 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/runed": {
+      "version": "0.22.0",
+      "resolved": "https://registry.npmjs.org/runed/-/runed-0.22.0.tgz",
+      "integrity": "sha512-ZWVXWhOr0P5xdNgtviz6D1ivLUDWKLCbeC5SUEJ3zBkqLReVqWHenFxMNFeFaiC5bfxhFxyxzyzB+98uYFtwdA==",
+      "funding": [
+        "https://github.com/sponsors/huntabyte",
+        "https://github.com/sponsors/tglide"
+      ],
+      "dependencies": {
+        "esm-env": "^1.0.0"
+      },
+      "peerDependencies": {
+        "svelte": "^5.7.0"
+      }
+    },
     "node_modules/rw": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
@@ -6843,7 +6868,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
       "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-      "dev": true,
       "dependencies": {
         "shebang-regex": "^3.0.0"
       },
@@ -6855,7 +6879,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
       "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -6870,7 +6893,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-      "dev": true,
       "engines": {
         "node": ">=14"
       },
@@ -6979,7 +7001,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
       "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-      "dev": true,
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
@@ -7078,7 +7099,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -7093,7 +7113,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -7107,7 +7126,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -7120,7 +7138,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -7152,11 +7169,19 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/style-to-object": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
+      "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
+      "license": "MIT",
+      "dependencies": {
+        "inline-style-parser": "0.2.4"
+      }
+    },
     "node_modules/sucrase": {
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
       "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.2",
@@ -7199,7 +7224,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -7841,6 +7865,41 @@
         "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
       }
     },
+    "node_modules/svelte-toolbelt": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.0.tgz",
+      "integrity": "sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==",
+      "funding": [
+        "https://github.com/sponsors/huntabyte"
+      ],
+      "dependencies": {
+        "clsx": "^2.1.1",
+        "runed": "^0.20.0",
+        "style-to-object": "^1.0.8"
+      },
+      "engines": {
+        "node": ">=18",
+        "pnpm": ">=8.7.0"
+      },
+      "peerDependencies": {
+        "svelte": "^5.0.0"
+      }
+    },
+    "node_modules/svelte-toolbelt/node_modules/runed": {
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/runed/-/runed-0.20.0.tgz",
+      "integrity": "sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==",
+      "funding": [
+        "https://github.com/sponsors/huntabyte",
+        "https://github.com/sponsors/tglide"
+      ],
+      "dependencies": {
+        "esm-env": "^1.0.0"
+      },
+      "peerDependencies": {
+        "svelte": "^5.7.0"
+      }
+    },
     "node_modules/svelte/node_modules/aria-query": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -7858,11 +7917,36 @@
       "optional": true,
       "peer": true
     },
+    "node_modules/tailwind-merge": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+      "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
+    "node_modules/tailwind-variants": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.0.tgz",
+      "integrity": "sha512-ho2k5kn+LB1fT5XdNS3Clb96zieWxbStE9wNLK7D0AV64kdZMaYzAKo0fWl6fXLPY99ffF9oBJnIj5escEl/8A==",
+      "license": "MIT",
+      "dependencies": {
+        "tailwind-merge": "^2.5.4"
+      },
+      "engines": {
+        "node": ">=16.x",
+        "pnpm": ">=7.x"
+      },
+      "peerDependencies": {
+        "tailwindcss": "*"
+      }
+    },
     "node_modules/tailwindcss": {
       "version": "3.4.17",
       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
       "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -7900,7 +7984,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
       "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14"
@@ -7913,7 +7996,6 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
       "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -7949,7 +8031,6 @@
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
       "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
-      "dev": true,
       "license": "ISC",
       "bin": {
         "yaml": "bin.mjs"
@@ -8000,7 +8081,6 @@
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
       "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
-      "dev": true,
       "dependencies": {
         "any-promise": "^1.0.0"
       }
@@ -8009,7 +8089,6 @@
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
       "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
-      "dev": true,
       "dependencies": {
         "thenify": ">= 3.1.0 < 4"
       },
@@ -8098,7 +8177,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
@@ -8163,8 +8241,7 @@
     "node_modules/ts-interface-checker": {
       "version": "0.1.13",
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
-      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
-      "dev": true
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
     },
     "node_modules/tslib": {
       "version": "2.8.1",
@@ -8308,8 +8385,7 @@
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
@@ -8581,7 +8657,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
       "dependencies": {
         "isexe": "^2.0.0"
       },
@@ -8635,7 +8710,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -8652,7 +8726,6 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^2.0.1"
       },
@@ -8667,7 +8740,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -8678,8 +8750,7 @@
     "node_modules/wrap-ansi-cjs/node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
     "node_modules/wrap-ansi/node_modules/ansi-styles": {
       "version": "4.3.0",
diff --git a/web/package.json b/web/package.json
index b843f6c13a..fc936efdc4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -67,6 +67,7 @@
   "dependencies": {
     "@formatjs/icu-messageformat-parser": "^2.9.8",
     "@immich/sdk": "file:../open-api/typescript-sdk",
+    "@immich/ui": "^0.11.0",
     "@mapbox/mapbox-gl-rtl-text": "0.2.3",
     "@mdi/js": "^7.4.47",
     "@photo-sphere-viewer/core": "^5.11.5",
diff --git a/web/src/app.css b/web/src/app.css
index d1af865bca..00cefc5ce6 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -22,6 +22,30 @@
     --immich-dark-success: 56 142 60;
     --immich-dark-warning: 245 124 0;
   }
+
+  :root {
+    /* light */
+    --immich-ui-primary: 66 80 175;
+    --immich-ui-dark: 0 0 0;
+    --immich-ui-light: 255 255 255;
+    --immich-ui-success: 34 197 94;
+    --immich-ui-danger: 180 0 0;
+    --immich-ui-warning: 255 170 0;
+    --immich-ui-info: 14 165 233;
+    --immich-ui-default-border: 209 213 219;
+  }
+
+  .dark {
+    /* dark */
+    --immich-ui-primary: 172 203 250;
+    --immich-ui-light: 0 0 0;
+    --immich-ui-dark: 229 231 235;
+    /* --immich-success: 56 142 60; */
+    --immich-ui-danger: 239 68 68;
+    --immich-ui-warning: 255 170 0;
+    --immich-ui-info: 14 165 233;
+    --immich-ui-default-border: 55 65 81;
+  }
 }
 
 @font-face {
diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte
index c470f809a6..3a61a1671c 100644
--- a/web/src/lib/components/layouts/AuthPageLayout.svelte
+++ b/web/src/lib/components/layouts/AuthPageLayout.svelte
@@ -1,36 +1,25 @@
 <script lang="ts">
-  import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
+  import { Card, CardBody, CardHeader, Heading, Logo, VStack } from '@immich/ui';
   import type { Snippet } from 'svelte';
 
   interface Props {
     title: string;
-    message?: Snippet;
-    showMessage?: boolean;
     children?: Snippet;
   }
 
-  let { title, message, showMessage = message != undefined, children }: Props = $props();
+  let { title, children }: Props = $props();
 </script>
 
-<section class="min-w-screen flex min-h-screen place-content-center place-items-center p-4">
-  <div
-    class="flex w-full max-w-lg flex-col gap-4 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
-  >
-    <div class="flex flex-col place-content-center place-items-center gap-4 py-4">
-      <ImmichLogo noText class="h-24 w-24" />
-      <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
-        {title}
-      </h1>
-    </div>
-
-    {#if showMessage}
-      <div
-        class="w-full rounded-xl border-2 border-immich-primary bg-immich-primary/5 p-4 text-sm font-medium text-immich-primary dark:border-immich-dark-bg dark:text-immich-dark-primary"
-      >
-        {@render message?.()}
-      </div>
-    {/if}
-
-    {@render children?.()}
-  </div>
+<section class="min-w-screen flex min-h-screen items-center justify-center">
+  <Card color="secondary" class="w-full max-w-xl border m-2">
+    <CardHeader class="mt-6">
+      <VStack>
+        <Logo variant="icon" size="giant" />
+        <Heading size="large" class="font-semibold" color="primary">{title}</Heading>
+      </VStack>
+    </CardHeader>
+    <CardBody>
+      {@render children?.()}
+    </CardBody>
+  </Card>
 </section>
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index fa1351ab20..2706ead46e 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -19,12 +19,22 @@
   import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation';
   import { onDestroy, onMount, type Snippet } from 'svelte';
   import { run } from 'svelte/legacy';
+  import { setTranslations } from '@immich/ui';
   import '../app.css';
+  import { t } from 'svelte-i18n';
 
   interface Props {
     children?: Snippet;
   }
 
+  $effect(() => {
+    setTranslations({
+      close: $t('close'),
+      showPassword: $t('show_password'),
+      hidePassword: $t('hide_password'),
+    });
+  });
+
   let { children }: Props = $props();
 
   let showNavigationLoadingBar = $state(false);
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index 68a5deb0f9..b3ac52bd7c 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -1,17 +1,16 @@
 <script lang="ts">
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
   import { AppRoute } from '$lib/constants';
+  import { Heading, Button, Logo } from '@immich/ui';
   import { t } from 'svelte-i18n';
 </script>
 
 <section class="flex h-screen w-screen place-content-center place-items-center">
-  <div class="flex max-w-[350px] flex-col place-items-center gap-8 text-center">
+  <div class="flex max-w-[350px] flex-col place-items-center gap-10 text-center">
     <div class="flex place-content-center place-items-center">
-      <ImmichLogo noText class="text-center" height="200" width="200" />
+      <Logo variant="icon" class="text-center" size="landing" />
     </div>
-    <h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('welcome_to_immich')}</h1>
-    <Button href={AppRoute.AUTH_REGISTER} size="lg" rounded="lg">
+    <Heading size="giant" color="primary">{$t('welcome_to_immich')}</Heading>
+    <Button href={AppRoute.AUTH_REGISTER} size="giant" shape="round">
       <span class="px-2 font-bold">{$t('getting_started')}</span>
     </Button>
   </div>
diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte
index ea340ff600..6b91118475 100644
--- a/web/src/routes/auth/change-password/+page.svelte
+++ b/web/src/routes/auth/change-password/+page.svelte
@@ -1,11 +1,10 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
-  import Button from '$lib/components/elements/buttons/button.svelte';
   import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
-  import PasswordField from '$lib/components/shared-components/password-field.svelte';
   import { AppRoute } from '$lib/constants';
   import { resetSavedUser, user } from '$lib/stores/user.store';
   import { logout, updateMyUser } from '@immich/sdk';
+  import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
 
@@ -35,32 +34,31 @@
 </script>
 
 <AuthPageLayout title={data.meta.title}>
-  {#snippet message()}
-    <p>
-      {$t('hi_user', { values: { name: $user.name, email: $user.email } })}
-      <br />
-      <br />
-      {$t('change_password_description')}
-    </p>
-  {/snippet}
+  <div class="m-4">
+    <Alert color="primary" size="small">
+      <Stack gap={4}>
+        <Text>{$t('hi_user', { values: { name: $user.name, email: $user.email } })}</Text>
+        <Text>{$t('change_password_description')}</Text>
+      </Stack>
+    </Alert>
+  </div>
 
-  <form onsubmit={onSubmit} method="post" class="mt-5 flex flex-col gap-5">
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="password">{$t('new_password')}</label>
-      <PasswordField id="password" bind:password autocomplete="new-password" />
-    </div>
+  <form onsubmit={onSubmit} method="post" class="mx-4 mt-6">
+    <Stack gap={4} class="mt-4">
+      <Field label={$t('new_password')} required>
+        <PasswordInput bind:value={password} autocomplete="new-password" />
+      </Field>
 
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
-      <PasswordField id="confirmPassword" bind:password={passwordConfirm} autocomplete="new-password" />
-    </div>
+      <Field label={$t('confirm_password')} required>
+        <PasswordInput bind:value={passwordConfirm} autocomplete="new-password" />
+        {#if errorMessage}
+          <HelperText color="danger">{errorMessage}</HelperText>
+        {/if}
+      </Field>
 
-    {#if errorMessage}
-      <p class="text-sm text-red-400">{errorMessage}</p>
-    {/if}
-
-    <div class="my-5 flex w-full">
-      <Button type="submit" size="lg" fullwidth disabled={!valid}>{$t('to_change_password')}</Button>
-    </div>
+      <div class="my-5 flex w-full">
+        <Button type="submit" size="large" shape="round" fullWidth disabled={!valid}>{$t('to_change_password')}</Button>
+      </div>
+    </Stack>
   </form>
 </AuthPageLayout>
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte
index 63346a6abf..f52face78e 100644
--- a/web/src/routes/auth/login/+page.svelte
+++ b/web/src/routes/auth/login/+page.svelte
@@ -1,17 +1,14 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
-  import Button from '$lib/components/elements/buttons/button.svelte';
   import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
-  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import PasswordField from '$lib/components/shared-components/password-field.svelte';
   import { AppRoute } from '$lib/constants';
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { oauth } from '$lib/utils';
   import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
   import { login } from '@immich/sdk';
+  import { Alert, Button, Field, Input, PasswordInput } from '@immich/ui';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
-  import { fade } from 'svelte/transition';
   import type { PageData } from './$types';
 
   interface Props {
@@ -103,90 +100,63 @@
 </script>
 
 {#if $featureFlags.loaded}
-  <AuthPageLayout title={data.meta.title} showMessage={!!$serverConfig.loginPageMessage}>
-    {#snippet message()}
-      <p>
+  <AuthPageLayout title={data.meta.title}>
+    {#if $serverConfig.loginPageMessage}
+      <Alert color="primary">
         <!-- eslint-disable-next-line svelte/no-at-html-tags -->
         {@html $serverConfig.loginPageMessage}
-      </p>
-    {/snippet}
+      </Alert>
+    {/if}
 
     {#if !oauthLoading && $featureFlags.passwordLogin}
-      <form {onsubmit} class="mt-5 flex flex-col gap-5">
+      <form {onsubmit} class="mt-5 flex flex-col gap-5 text-dark mx-4">
         {#if errorMessage}
-          <p class="text-red-400" transition:fade>
-            {errorMessage}
-          </p>
+          <Alert color="danger" title={errorMessage} closable />
         {/if}
 
-        <div class="flex flex-col gap-2">
-          <label class="immich-form-label" for="email">{$t('email')}</label>
-          <input
-            class="immich-form-input"
-            id="email"
-            name="email"
-            type="email"
-            autocomplete="email"
-            bind:value={email}
-            required
-          />
-        </div>
+        <Field label={$t('email')}>
+          <Input name="email" type="email" autocomplete="email" bind:value={email} />
+        </Field>
 
-        <div class="flex flex-col gap-2">
-          <label class="immich-form-label" for="password">{$t('password')}</label>
-          <PasswordField id="password" bind:password autocomplete="current-password" />
-        </div>
+        <Field label={$t('password')}>
+          <PasswordInput bind:value={password} autocomplete="current-password" />
+        </Field>
 
-        <div class="my-5 flex w-full">
-          <Button type="submit" size="lg" fullwidth disabled={loading}>
-            {#if loading}
-              <span class="h-6">
-                <LoadingSpinner />
-              </span>
-            {:else}
-              {$t('to_login')}
-            {/if}
-          </Button>
-        </div>
+        <Button type="submit" size="large" shape="round" fullWidth {loading} class="mt-6">{$t('to_login')}</Button>
       </form>
     {/if}
 
     {#if $featureFlags.oauth}
       {#if $featureFlags.passwordLogin}
-        <div class="inline-flex w-full items-center justify-center">
+        <div class="inline-flex w-full items-center justify-center mt-4">
           <hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
           <span
             class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white"
           >
-            {$t('or')}
+            {$t('or').toUpperCase()}
           </span>
         </div>
       {/if}
-      <div class="my-5 flex flex-col gap-5">
+      <div class="my-5 flex flex-col gap-5 mx-4">
         {#if oauthError}
-          <p class="text-center text-red-400" transition:fade>{oauthError}</p>
+          <Alert color="danger" title={oauthError} closable />
         {/if}
         <Button
-          type="button"
+          shape="round"
+          loading={loading || oauthLoading}
           disabled={loading || oauthLoading}
-          size="lg"
-          fullwidth
+          size="large"
+          fullWidth
           color={$featureFlags.passwordLogin ? 'secondary' : 'primary'}
           onclick={handleOAuthLogin}
         >
-          {#if oauthLoading}
-            <span class="h-6">
-              <LoadingSpinner />
-            </span>
-          {:else}
-            {$serverConfig.oauthButtonText}
-          {/if}
+          {$serverConfig.oauthButtonText}
         </Button>
       </div>
     {/if}
 
     {#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
-      <p class="p-4 text-center dark:text-immich-dark-fg">{$t('login_has_been_disabled')}</p>
+      <Alert color="warning" title={$t('login_has_been_disabled')} />
     {/if}
   </AuthPageLayout>
 {/if}
diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte
index 43e28d5964..50551358ea 100644
--- a/web/src/routes/auth/register/+page.svelte
+++ b/web/src/routes/auth/register/+page.svelte
@@ -1,12 +1,11 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
-  import Button from '$lib/components/elements/buttons/button.svelte';
   import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
-  import PasswordField from '$lib/components/shared-components/password-field.svelte';
   import { AppRoute } from '$lib/constants';
   import { retrieveServerConfig } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { signUpAdmin } from '@immich/sdk';
+  import { Alert, Button, Field, Input, PasswordInput, Text } from '@immich/ui';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
 
@@ -48,39 +47,35 @@
 </script>
 
 <AuthPageLayout title={data.meta.title}>
-  {#snippet message()}
-    <p>
-      {$t('admin.registration_description')}
-    </p>
-  {/snippet}
+  <div class="mx-4 mt-4">
+    <Alert color="primary">
+      <Text>{$t('admin.registration_description')}</Text>
+    </Alert>
+  </div>
 
-  <form onsubmit={onSubmit} method="post" class="mt-5 flex flex-col gap-5">
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="email">{$t('admin_email')}</label>
-      <input class="immich-form-input" id="email" bind:value={email} type="email" autocomplete="email" required />
-    </div>
+  <form onsubmit={onSubmit} method="post" class="mt-5 flex flex-col gap-5 text-dark p-4">
+    <Field label={$t('admin_email')} required>
+      <Input bind:value={email} type="email" autocomplete="email" />
+    </Field>
 
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="password">{$t('admin_password')}</label>
-      <PasswordField id="password" bind:password autocomplete="new-password" />
-    </div>
+    <Field label={$t('admin_password')} required>
+      <PasswordInput bind:value={password} autocomplete="new-password" />
+    </Field>
 
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="confirmPassword">{$t('confirm_admin_password')}</label>
-      <PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
-    </div>
+    <Field label={$t('confirm_admin_password')} required>
+      <PasswordInput bind:value={confirmPassword} autocomplete="new-password" />
+    </Field>
 
-    <div class="flex flex-col gap-2">
-      <label class="immich-form-label" for="name">{$t('name')}</label>
-      <input class="immich-form-input" id="name" bind:value={name} type="text" autocomplete="name" required />
-    </div>
+    <Field label={$t('name')} required>
+      <Input bind:value={name} type="text" autocomplete="name" />
+    </Field>
 
     {#if errorMessage}
-      <p class="text-red-400">{errorMessage}</p>
+      <Alert color="danger" title={errorMessage} size="medium" class="mt-4" />
     {/if}
 
     <div class="my-5 flex w-full">
-      <Button type="submit" size="lg" fullwidth disabled={!valid}>{$t('sign_up')}</Button>
+      <Button type="submit" size="giant" shape="round" fullWidth disabled={!valid}>{$t('sign_up')}</Button>
     </div>
   </form>
 </AuthPageLayout>
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
index eb1ea78fae..12bfd7c604 100644
--- a/web/tailwind.config.js
+++ b/web/tailwind.config.js
@@ -2,7 +2,7 @@ import plugin from 'tailwindcss/plugin';
 
 /** @type {import('tailwindcss').Config} */
 export default {
-  content: ['./src/**/*.{html,js,svelte,ts}'],
+  content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/@immich/ui/dist/**/*.{svelte,js}'],
   darkMode: 'class',
   theme: {
     extend: {
@@ -24,7 +24,20 @@ export default {
         'immich-dark-error': 'rgb(var(--immich-dark-error) / <alpha-value>)',
         'immich-dark-success': 'rgb(var(--immich-dark-success) / <alpha-value>)',
         'immich-dark-warning': 'rgb(var(--immich-dark-warning) / <alpha-value>)',
+
+        primary: 'rgb(var(--immich-ui-primary) / <alpha-value>)',
+        light: 'rgb(var(--immich-ui-light) / <alpha-value>)',
+        dark: 'rgb(var(--immich-ui-dark) / <alpha-value>)',
+        success: 'rgb(var(--immich-ui-success) / <alpha-value>)',
+        danger: 'rgb(var(--immich-ui-danger) / <alpha-value>)',
+        warning: 'rgb(var(--immich-ui-warning) / <alpha-value>)',
+        info: 'rgb(var(--immich-ui-info) / <alpha-value>)',
+        subtle: 'rgb(var(--immich-gray) / <alpha-value>)',
       },
+      borderColor: ({ theme }) => ({
+        ...theme('colors'),
+        DEFAULT: 'rgb(var(--immich-ui-default-border) / <alpha-value>)',
+      }),
       fontFamily: {
         'immich-mono': ['Overpass Mono', 'monospace'],
       },
diff --git a/web/vite.config.js b/web/vite.config.js
index 266312e137..5d134beab0 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -19,6 +19,7 @@ export default defineConfig({
       'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
       // eslint-disable-next-line unicorn/prefer-module
       '@test-data': path.resolve(__dirname, './src/test-data'),
+      // '@immich/ui': path.resolve(__dirname, '../../ui'),
     },
   },
   server: {